Would it be sensible to have a language that statically checks mutex correctness? Ie,
var m
var x guarded_by(m)
func f1() {
lock(m)
x = 42
unlock(m)
}
func f2() {
x = 42 // error, accessing x w/o holding its mutex
}
func f3() assumes_locked(m) {
x = 42
}
func b1() {
f3() // error
}
func b2() {
lock(m)
f3()
unlock(m)
}
Is this possible? Ie, can correctness of mutex uses be statically verified with a few simple annotations like this?
Is this sensible? Is there a reason this would be a bad idea?
Are there any languages that have this built-in?
Static race condition detection
As delnan said in a comment, "mutex correctness" can mean a variety of things. From your comment, it sounds like you're talking about race condition detection, which is convenient for me, as this was the subject of my undergraduate thesis :-)¹ You want to know (1) if this is possible, (2) if this is sensible, and (3) if this is done. As with any static analysis (including lightweight type-or-effect-system–like ones such as this), the first two questions boil down to four things:
Soundness: Does it allow any incorrect programs? If so, how many/which ones?
Completeness: Does it rule out any correct programs? If so, how many/which ones?
Usability: How is the programmer overhead? E.g., what annotations are required, are the error messages comprehensible, etc.?
Goals: Where in the design space delimited by the above three constraints are we trying to end up?
Note that we can never get 100% accuracy for both 1 and 2, thanks to the halting problem (and its more general big sibling Rice's theorem). For example, most type systems² aim for soundness and usability (#1 and #3); that is, they aim to be be easy to use while allowing no type-unsafe programs, and try to rule out only unrealistic type-safe programs. (Their advocates, like me, would claim that they've generally succeeded at this goal.)
A simple lock-checking type system: RaceFreeJava
Your example seems like it's trying to maximize usability (#3): you've provided minimal annotations, which correspond to what you'd hope the programmer would have in mind anyway, and then want an analysis which checks them. The overall scheme sounds pretty similar to RaceFreeJava (Abadi et al., 2006). RaceFreeJava is an extension of Java where:
Classes can be parametrized by locks (called ghost locks);
Fields can have guarded_by annotations specifying which locks must protect them; and
Methods can have requires annotations specifying which locks must be held to call them.
As you may have surmised, the idea is that every field has an associated lock, which must be held every time the field is accessed; the analysis tracks which locks are held at every program point, and if a variable is accessed when its lock is not in the current lock set, an error is reported. This gets slightly subtler with ghost lock parameters; for instance, you can have
class RunningTotals<ghost Object m> {
private int sum = 0 guarded_by m;
private int product = 1 guarded_by m;
public void include(int x) requires m {
sum += x;
product *= x;
}
public int getSum() requires m {
return sum;
}
public int getProduct() requires m {
return product;
}
}
Now I can embed a RunningTotals object in some other object o and protect it with o's lock, which is handy; however, the analysis thens need to be careful about which lock m really is. Also, note that the methods in RunningTotals cannot synchronize on m, since m isn't in scope and is simply a type-level ghost; synchronization must be handled by the client. Similarly to how we can declare lock-parametrized classes, another feature of RaceFreeJava is the ability to declare thread_local classes; these classes are never allowed to be shared between threads, and consequently don't need to guard their fields with locks.
As you noted in a comment, figuring out if a program is correct with these annotations would, in general, be undecidable, particularly since Java allows you to synchronize on arbitrary expressions. RaceFreeJava restricts synchronization to final expressions, for sanity's sake; then, just as with all type-like annotations, it uses a conservative syntactic check for lock identity. Abadi et al. found that most correct programs could be successfully checked in this manner. Thus, they cleave strongly to soundness (indeed, the analysis is sound modulo some escape hatches they put in) and to usability, at the expense of completeness. In order to check usability – which strongly relates to completeness, since too many spurious warnings make a static analysis nigh-unusable – Abadi et al. also wrote a tool (rccjava) for checking these annotations and doing small amounts of annotation inference, and also used it to build a larger tool (Houdini/rcc) for performing even more inference. When evaluating these (Tables I–IV), they found real bugs without too many necessary annotations or spurious warnings. However, note that it's not perfect; there will be some valid Java programs that this analysis will complain about.
Other static race detectors
Because I have a natural tendency to be long-winded, and because (as I mentioned) I wrote my undergraduate thesis on this, I'll also talk about a few other analyses, but it sounds to me like RaceFreeJava is probably the most relevant. If you want to just skip ahead to the end, there's a summing-up after this section.
Another type-system–based analysis is that presented by Boyapati et al. (2002). This type system detects both race conditions and deadlocks, but using a different technique; it's based on ownership types, which track what threads are allowed to access what variables; variables can only be accessed if the locks on their owners are held. Again, methods can annotate needed accesses or needed locks. For checking deadlocks, Boyapati et al. also add the notion of lock levels, which place all the locks in a partial order. By ensuring that locks are always acquired in this order, deadlocks are automatically ruled out. Again, this algorithm is sound (although the proof is unfortunately omitted) but not complete, and tries to be usable. In order for this, they have to do extensive inference work, as writing down the annotations would be far too much work.
There are other analyses which aren't type systems. These have to model the execution of the program rather than just perform syntactic checks, and tend, as a rule, to be neither sound nor complete, but go all the way for usability by requiring no annotations (even if they allow them) and trying to report useful errors. RacerX (Engler and Ashcraft, 2003) does this, in a sense, by generating enough static information to approximate running a dynamic Lockset-type algorithm, such as that used by Eraser (Savage et al., 1997). This requires plenty of tuning, since approximating a run-time checker statically is not easy, and Lockset is already an approximation to the actual truth. RacerX also performs deadlock checking using a highly tuned constraint-propagation approach, where the constraints are lock orderings. Engler and Ashcraft managed to use RacerX to find some bugs in real operating systems, although they do get false positives and find benign races (Table 5).
One thing that RacerX doesn't handle, by design, is aliasing information: knowing when two references point to the same value. This is important for concurrent code; consider the following faux-C:
// Thread 1:
acquire(superman_lock);
*superman_bank_account += 100; // Saved Metropolis and got rewarded!
release(superman_lock);
// Thread 2:
acquire(clark_kent_lock);
*clark_kent_bank_account -= 100; // More bills...
release(clark_kent_lock);
Presuming that superman_lock and clark_kent_lock are distinct, knowing whether or not this code is safe requires knowing whether superman_bank_account is the same as clark_kent_bank_account. And that's a very difficult thing to figure out!
The Chord algorithm (Naik et al., 2006)³ is a different whole-program race detector which has an alias-detector as a key step; again, though, we run into Rice's theorem, and such an analysis cannot be both sound and complete. Chord tracks might-be-aliased information (equivalently, definitely-unaliased information), and so is unsound.
Summing up
So. You wanted to know three things: is your scheme (1) possible, (2) sensible, and (3) done in the real world? All of these analyses demonstrate that this is a perfectly sensible and possible idea; RaceFreeJava in particular demonstrates that the annotation ideas you had are pretty near a proven-sound type system. However, I don't know of any language that has this built in. I believe every paper I cited here did implement their algorithm, but I haven't looked at all to see if those tools are freely available, commercially available, bitrotted, under active development, or what. Certainly, however, real-world static analyses do exist; for instance, the Coverity's (commercial) static analyzer finds, among many other things, race conditions and deadlocks. They do come with downsides, namely the potential for spurious complaints about good code or no complaints about bad code, but that comes with the territory, and often isn't a big problem in practice.
Footnotes
¹ Well, dynamic race condition detection, anyway. This all means I'm cribbing from the related work section I wrote at the time, which is particularly convenient! It also means this might be missing particularly recent developments (I wrote the thesis in the 2011–2012 academic year). Relatedly, this means it's been a while since I thought about all of this, so it's possible there are egregiously missing citations or (hopefully small) mistakes above; if anybody notices such a thing, please let me know or just edit this post!
² Most sensible type systems, says the Haskell partisan :-) C doesn't care at all, for instance, and Java gets most of the way there but has to resort to dynamic checks thanks to things like downcasting and (*shudder*) array covariance.
³ Sorry about the ACM Digital Library link; I couldn't find a freely-available PDF version of this paper, but I did find a PostScript version.
Bibliography
Martin Abadi, Cormac Flanagan, and Stephen N. Freund. "Types for Safe Locking: Static Race Detection for Java." In ACM Transactions on Programming Languages and Systems (TOPLAS) 28(2), June 2006, pp. 207–255.
Chandrasekhar Boyapati, Robert Lee, and Martin Rinard. "Ownership Types for Safe Programming: Preventing Data Races and Deadlocks." In In Proceedings of the 17th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA), Volume 37, November 2002, pp. 211–230.
Dawson Engler and Ken Ashcraft. "RacerX: Effective, Static Detection of Race Conditions and Deadlocks." In Proceedings of the 19th ACM Symposium on Operating Systems Principles (SOSP), Volume 37, 2003, pp. 237–252.
Mayur Naik, Alex Aiken, and John Whaley. "Effective Static Race Detection for Java." In Proceedings of the 2006 ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), Volume 41, 2006, pp. 308–319. Available at the ACM Digital Library and as a PostScript file.
Stefan Savage, Michael Burrows, Greg Nelson, Patrick Sobalvarro, and Thomas Anderson. "Eraser: A Dynamic Data Race Detector for Multithreaded Programs." ACM Transactions on Computer Systems 15(4), 1997, pp. 391–411.
Related
I know SOLID principles were written for object oriented languages.
I found in the book: "Test driven development for embedded C" by Robert Martin, the following sentence in the last chapter of the book:
"Applying the Open-Closed Principle and the Liskov Substitution Principle makes more flexible designs."
As this is a book of C (no c++ or c#), there should be a way of way of implementing this principles.
There exists any standard way for implementing this principles in C?
Open-closed principle states that a system should be designed so that it's open to extension while keeping it closed from modification, or that it can be used and extended without modifying it. An I/O subsystem, as mentioned by Dennis, is a fairly common example: in a reusable system the user should be able to specify how data is read and written instead of assuming that data can only be written to files for example.
The way to implement this depends on your needs: You may allow the user to pass in an open file descriptor or handle, which already enables the use of sockets or pipes in addition to files. Or you may allow the user to pass in pointers to the functions that should be used for reading and writing: this way your system could be used with encrypted or compressed data streams in addition to what the OS permits.
The Liskov substitution principle states that it should always be possible to replace a type with a subtype. In C you don't often have subtypes, but you can apply the principle in the module level: code should be designed so that using an extended version of a module, like a newer version, should not break it. An extended version of a module may use a struct that has more fields than the original, more fields in an enum, and similar things, so your code shouldn't assume that a struct that is passed in has a certain size, or that enum values have a certain maximum.
One example of this is how socket addresses are implemented in the BSD socket API: there an "abstract" socket type struct sockaddr that can stand for any socket address type, and a concrete socket type for each implementation, such as struct sockaddr_un for Unix domain sockets and struct sockaddr_in for IP sockets. Functions that work on socket addresses have to be passed a pointer to the data and the size of the concrete address type.
First, it helps to think about why we have these design principles. Why does following the SOLID principles make software better? Work to understand the goals of each principle, and not just the specific implementation details required to use them with a specific language.
The Single Responsibility Principle improves modularity by increasing
cohesion; better modularity leads to improved testability,
usability, and reusability.
The Open/Closed Principle enables asynchronous deployment by
decoupling implementations from each other.
The Liskov Substitution Principle promotes modularity and reuse of modules by
ensuring the compatibility of their interfaces.
The Interface Segregation Principle reduces coupling between
unrelated consumers of the interface, while increasing readability and
understandability.
The Dependency Inversion Principle reduces coupling, and it strongly
enables testability.
Notice how each principle drives an improvement in a certain attribute of the system, whether it be higher cohesion, looser coupling, or modularity.
Remember, your goal is to produce high quality software. Quality is made up of many different attributes, including correctness, efficiency, maintainability, understandability, etc. When followed, the SOLID principles help you get there. So once you've got the "why" of the principles, the "how" of the implementation gets a lot easier.
EDIT:
I'll try to more directly answer your question.
For the Open/Close Principle, the rule is that both the signature and the behavior of the old interface must remain the same before and after any changes. Don't disrupt any code that is calling it. That means it absolutely takes a new interface to implement the new stuff, because the old stuff already has a behavior. The new interface must have a different signature, because it offers the new and different functionality. So you meet those requirements in C just the same way as you'd do it in C++.
Let's say you have a function int foo(int a, int b, int c) and you want to add a version that's almost exactly the same, but it takes a fourth parameter, like this: int foo(int a, int b, int c, int d). It's a requirement that the new version be backward compatible with the old version, and that some default (such as zero) for the new parameter would make that happen. You'd move the implementation code from old foo into new foo, and in your old foo you'd do this: int foo(int a, int b, int c) { return foo(a, b, c, 0);} So even though we radically transformed the contents of int foo(int a, int b, int c), we preserved its functionality. It remained closed to change.
The Liskov substitution principle states that different subtypes must work compatibly. In other words, the things with common signatures that can be substituted for each other must behave rationally the same.
In C, this can be accomplished with function pointers to functions that take identical sets of parameters. Let's say you have this code:
#include <stdio.h>
void fred(int x)
{
printf( "fred %d\n", x );
}
void barney(int x)
{
printf( "barney %d\n", x );
}
#define Wilma 0
#define Betty 1
int main()
{
void (*flintstone)(int);
int wife = Betty;
switch(wife)
{
case Wilma:
flintstone = &fred;
case Betty:
flintstone = &barney;
}
(*flintstone)(42);
return 0;
}
fred() and barney() must have compatible parameter lists for this to work, of course, but that's no different than subclasses inheriting their vtable from their superclasses. Part of the behavior contract would be that both fred() and barney() should have no hidden dependencies, or if they do, they must also be compatible. In this simplistic example, both functions rely only on stdout, so it's not really a big deal. The idea is that you preserve correct behavior in both situations where either function could be used interchangeably.
The closest thing I can think of off the top of my head (and it's not perfect, so if someone has a much better idea they are welcome to one-up me) is mostly for when I'm writing functions for some sort of library.
For Liskov substitution, if you have a header file that defines a number of functions, you do not want the functionality of that library to depend on which implementation you have of the functions; you ought to be able to use any reasonable implementation and expect your program to do its thing.
As for the Open/Closed principle, if you want to implement an I/O library, you want to have functions that do the bare minimum (like read and write). At the same time, you may want to use those to develop more sophisticated I/O functions (like scanf and printf), but you aren't going to modify the code that did the bare minimum.
I see it has been a while since the question was opened, but I think it worth some newer sight.
The five SOLID principles refer to the five aspects of a software entities, as it shown in the SOLID diagram. Although this is a class diagram, it can basically serve other types of SW identities. Interfaces exposed for callers (the left arrow, stands for Interface Segregation) and interfaced requested as callees (the right arrow, stands for Dependency Inversion) can just as well be classical C functions and arguments interfaces.
The top arrow (extension arrow, stands for the Liskov Substitution Principle) works for any other implementation of a similar entity. E.g., if you have an API for a Linked List, you can change the implementation of its functions and even the structure of the vector "object" (assuming, for example, that it preserve the structure of the original one, as in the BSD Sockets example, or it is an opaque type). Sure, this is not as elegant as an Object in an OOP language, but it follows the same principle, and can be used, for example, using dynamic linkage.
In a similar way, the bottom arrow (generalization arrow, stands for the Open/Close Principle), defines whet is defined by your entity and what is open. For example, some functionality might be defined in one file and should not be replaced, while other functionality might call another set of APIs, which allows using different implementations.
This way you can write SOLID SW with C as well, although it will probably be done using higher level entities and might require some more engineering.
When dealing with microcontrollers there are things that are inherently global - I'm thinking about peripherals like serial ports or other interfaces. There are also peripherals that are not only global but there is only one (and there will never be more) - like peripheral controlling core clocks or interrupt controller. These peripherals do have some kind of global state (for example - core clock is set to something) and it's inefficient to reverse-calculate these values.
If I'd like my program to be nicely object-oriented, I'm having hard time deciding how to deal with such objects... Global variables are not nice and this is obvious, but I just don't know (not enough experience) whether I should try to "hide" the fact that these things ARE global... For example "cin" or "stdout" are globals too (let's ignore the fact that in multithreaded apps these are usually thread-specific) and noone is hiding that... Let's stick with the clock generator peripheral - there is only one, so I could use the singleton anti-pattern (; or make the class static or just have the single object global (that's what I have usually done), as this object has the current clock setting stored and this value is needed for LOTS of other things - it's needed to set system timer used by RTOS, it's needed to set clocks for other peripherals (UART baudrate, SPI bitrate, ...), it's needed to set correct clock for external memories or configure memory wait states. That's why I think that creating one object in main() and passing it around everywhere would be a bit cumbersome...
I could write the methods so that all "global" information would come from the peripheral registers (for example the core frequency could be reverse-calculated from current PLL settings), but this also seems like a wrong idea, not to mention that creating object for clock generator peripheral everywhere would look funny...
Current clock setting could be stored in static member of the class, but from here there's only one small step towards a fully static class (as "this" pointer will be useless for a class that has no state)...
The solution usually found in not-object-oriented programs is closest to fully static class - there are only functions that operate on global variables.
Anyone has some nice idea how to deal with such scenario nicely or whether this problem is worth the time? Maybe I should just use one global object and be done with it? (;
If I'd like my program to be nicely object-oriented, I'm having hard time deciding how to deal with such objects... Global variables are not nice and this is obvious, but I just don't know (not enough experience) whether I should try to "hide" the fact that these things ARE global...
When I read that, I wonder if you know why you are using OOP and why you don't use globals.
Firstly, OOP is a tool, not a goal. In your case, the interrupt controller doesn't need things like derivation and virtual functions. All you will need is an interface to program it, wrapped in a single class. You could even use a set of plain functions that do that (C=style modular programming) without giving up on maintainability. In your case, making the single instance global is even clearer. Imagine the alternative, where different parts of the program could instantiate a class that is used to access the same UART underneath. If you're using globals, the code (or rather the author) is aware of this and will think about how to coordinate access.
Now, concerning the globals, here's an example why not to use them:
int a, b, c;
void f1()
{
c = a;
f3();
}
void f2()
{
c = b;
f3();
}
void f3()
{
// use c
}
int main()
{
a = 4;
f1();
b = 5;
f2();
}
The point here is that parameters are stored in globals instead of passing them around as actual parameters and making it difficult to see where and when they are used. Also, the use above totally rules out any recursive calls. Concerning your environment, there are things that are inherently global though, because they are unique parts of the environment, like the interrupt controller (similar to cin/cout/cerr/clog). Don't worry about those. There have to be really many of them used all over the place until you need to think about restricting access.
There are two guidelines to make this easier:
The larger the scope of an object, the more it needs a speaking name (compare to a, b, c above). In your case, I'd store all hardware-specific objects in a common namespace, so it is obvious that some code is accessing globals. This also allows separate testing and reuse of this namespace. Many hardware vendors even provide something like this in the form of a library, in order to help application development.
In the code wrapping/modelling the hardware, perform requests but don't make decisions that are specific to the application on top of it. Compare to e.g. the standard streams, that are provided but never used by the standard library itself, except maybe for dumping fatal error information before aborting after a failed assertion.
You have pretty much outlined your options:
globals
static data members of classes / singletons
It's ultimately up to you to decide between the two, and choose which you like more from the aesthetic or other prospective.
At the end of the day, just as you have said, you'll still have one clock generator, one UART, etc.
One thing to keep in mind is that too much abstraction solely for the purpose of abstraction isn't going to buy you much if anything. What it may do, however, is make it harder for someone unfamiliar to your code figure out how things really work behind the layers of classes. So, take into account your team, if any.
The Singleton pattern is a source of debate, for sure. Some people simply say it's "bad", but I would disagree; it is just widely misused and misunderstood. It is definitely useful in certain situations. In fact, it applies to exactly the situation you've described.
If you have a class that needs to be available globally and by its very nature cannot have multiple instances, then a singleton is the best option.
Singletons, global objects, static classes, they are all the same. Dress the evil global state in whatever sauce you want, it's still global state. The problem is in the salad, not in the dressing, so to speak.
A little-explored path is monadic code, Haskell-style (yes in C++). I have never tried it myself, but from the looks of it, this option should be fun. See e.g. here for an example implementation of Monad interface in C++.
From: http://www.parashift.com/c++-faq-lite/basics-of-inheritance.html#faq-19.9
Three keys: ROI, ROI and ROI.
Every interface you build has a cost and a benefit. Every reusable
component you build has a cost and a benefit. Every test case, every
cleanly structured thing-a-ma-bob, every investment of any sort. You
should never invest any time or any money in any thing if there is not
a positive return on that investment. If it costs your company more
than it saves, don't do it!
Not everyone agrees with me on this; they have a right to be wrong.
For example, people who live sufficiently far from the real world act
like every investment is good. After all, they reason, if you wait
long enough, it might someday save somebody some time. Maybe. We hope.
That whole line of reasoning is unprofessional and irresponsible. You
don't have infinite time, so invest it wisely. Sure, if you live in an
ivory tower, you don't have to worry about those pesky things called
"schedules" or "customers." But in the real world, you work within a
schedule, and you must therefore invest your time only where you'll
get good pay-back.
Back to the original question: when should you invest time in building
a protected interface? Answer: when you get a good return on that
investment. If it's going to cost you an hour, make sure it saves
somebody more than an hour, and make sure the savings isn't "someday
over the rainbow." If you can save an hour within the current project,
it's a no-brainer: go for it. If it's going to save some other project
an hour someday maybe we hope, then don't do it. And if it's in
between, your answer will depend on exactly how your company trades
off the future against the present.
The point is simple: do not do something that could damage your
schedule. (Or if you do, make sure you never work with me; I'll have
your head on a platter.) Investing is good if there's a pay-back for
that investment. Don't be naive and childish; grow up and realize that
some investments are bad because they, in balance, cost more than they
return.
Well, I didn't understand how to correlate this to C++ protected interface.
Please give any real C++ examples to show what this FAQ is talking about.
First off, do not ever treat any programming reference as definitive. Ever. Everything is somebody's opinion, and in the end you should do what works best for you.
So, that said, what this text is basically trying to say is "don't use techniques that cost you more time than they save". One example of the "protected interface" they're describing is the following:
class C {
public:
int x;
};
Now, in Java, all the Java EE programming books will tell you to always implement that class like this:
class C {
public:
int getX() { return x; }
void setX(int x) { this.x = x; }
private:
int x;
};
... that's an implementation of proper encapsulation (technical term: simplifying a little, it means minimizing sharing between discrete parts). The classes using your code are concerned that you have some way to get and set an integer, not that it's actually stored as an int inside the class. So if you use accessor methods, you're better able to change the underlying implementation later: maybe you want it to read that variable from the network?
However, that was a large amount of extra code (in terms of characters) and some extra complexity to implement that. Doing things properly actually has a cost! It's not a cost in terms of correctness of the code - directly - but you spent some number of minutes doing it "better" that you could have spent doing something else, and there is a nonzero amount of work involved in maintaining everything you write, no matter how trivial.
So, what is being said in this passage is in my mind good advice: always double-check that when you go to do something, you're going to get more out of it than what you put in. Sanity check that you are not following an ideal to the detriment of your actual effectiveness as a programmer or a human being.
That's advice that will serve you well in any programming language, and in any walk of life.
From your quote above, the guy sounds like a pedantic jerk :)
Looking at the previous entries in his FAQ, he's really saying the following:
1) A class has two distinct interfaces for two distinct sets of clients:
It has a public interface that serves unrelated classes
It has a protected interface that serves derived classes
2) Should you always go to the trouble of creating two different interfaces for each class?
3) Answer: "no, not necessarily"
Sometimes it's worth the extra effort to create protected getter and setter methods, and make all data "private"
Other times - he says - it's "good enough" to make the data itself "protected". Without doing all the extra work of writing a bunch of extra code, and incurring the consequent size and performance penalties.
Sounds reasonable to me. Do what you need to do - but don't go overboard and do a bunch of unnecessary stuff in the name of "theory".
That's all he's saying - use good judgement, and don't go overboard.
You can't argue with that :)
PS:
FAQ's 19.5 through 19.9 in your link deal with "derived classes". None of this discussion is relevant outside of the question "how should I structure base classes for inheritance?" In other words, it's not a discussion about "classes" in general - only about "how should a super class best make things visible to it's subclasses?".
Ciao, I work in movie industry to simulate and apply studio effects. May I ask what is a fat interface as I hear someone online around here stating it ?
Edit: It is here said by Nicol Bolas (very good pointer I believe)
fat interface - an interface with more member functions and friends than are logically necessary. TC++PL 24.4.3
source
very simple explanation is here:
The Fat Interface approach [...]: in addition to the core services (that are part of the thin interface) it also offers a rich set of services that satisfy common needs of client code. Clearly, with such classes the amount of client code that needs to be written is smaller.
When should we use fat interfaces? If a class is expected to have a long life span or if a class is expected to have many clients it should offer a fat interface.
Maxim quotes Stroustrup's glossary:
fat interface - an interface with more member functions and friends than are logically necessary. TC++PL 24.4.3
Maxim provides no explanation, and other existing answers to this question misinterpret the above - or sans the Stroustrup quote the term itself - as meaning an interface with an arguably excessive number of members. It's not.
It's actually not about the number of members, but whether the members make sense for all the implementations.
That subtle aspect that doesn't come through very clearly in Stroustrup's glossary, but at least in the old version of TC++PL I have - is clear where the term's used in the text. Once you understand the difference, the glossary entry is clearly consistent with it, but "more member functions and friends than are logically necessary" is a test that should be applied from the perspective of each of the implementations of a logical interface. (My understanding's also supported by Wikipedia, for whatever that's worth ;-o.)
Specifically when you have an interface over several implementations, and some of the interface actions are only meaningful for some of the implementations, then you have a fat interface in which you can ask the active implementation to do something that it has no hope of doing, and you have to complicate the interface with some "not supported" discovery or reporting, which soon adds up to make it harder to write reliable client code.
For example, if you have a Shape base class and derived Circle and Square classes, and contemplate adding a double get_radius() const member: you could do so and have it throw or return some sentinel value like NaN or -1 if called on a Square - you'd then have a fat interface.
"Uncle Bob" puts a different emphasis on it below (boldfacing mine) in the context of the Interface Segregation Principle (ISP) (a SOLID principle that says to avoid fat interfaces):
[ISP] deals with the disadvantages of “fat” interfaces. Classes that have “fat” interfaces are classes whose interfaces are not cohesive. In other words, the interfaces of the class can be broken up into groups of member functions. Each group serves a different set of clients. Thus some clients use one group of member functions, and other clients use the other groups.
This implies you could have e.g. virtual functions that all derived classes do implementation with non-noop behaviours, but still consider the interface "fat" if typically any given client using that interface would only be interested in one group of its functions. For example: if a string class provided regexp functions and 95% of client code never used any of those, and especially if the 5% that did didn't tend to use the non-regexp string functions, then you should probably separate the regexp functionality from the normal textual string functionality. In that case though, there's a clear distinction in member function functionality that forms 2 groups, and when you were writing your code you'd have a clear idea whether you wanted regexp functionality or normal text-handling functionality. With the actual std::string class, although it has a lot of functions I'd argue that there's no clear grouping of functions where it would be weird to evolve a need to use some functions (e.g. begin/end) after having initially needed only say insert/erase. I don't personally consider the interface "fat", even though it's huge.
Of course, such an evocative term will have been picked up by other people to mean whatever they think it should mean, so it's no surprise that the web contains examples of the simpler larger-than-necessary-interface usage, as evidenced by the link in relaxxx's answer, but I suspect that's more people guessing at a meaning than "educated" about prior usage in Computing Science literature....
An interface with more methods or friends than is really necessary.
Whether as members, whether perhaps static, separate namespaces, via friend-s, via overloads even, or any other C++ language feature...
When facing the problem of supporting multiple/varying formats, maybe protocols or any other kind of targets for your types, what was the most flexible and maintainable approach?
Were there any conventions or clear cut winners?
A brief note why a particular approach helped would be great.
Thanks.
[ ProtoBufs like suggestions should not cut it for an upvote, no matter how flexible that particular impl might be :) ]
Reading through the already posted responses, I can only agree with a middle-tier approach.
Basically, in your original problem you have 2 distinct hierarchies:
n classes
m protocols
The naive use of a Visitor pattern (as much as I like it) will only lead to n*m methods... which is really gross and a gateway towards maintenance nightmare. I suppose you already noted it otherwise you would not ask!
The "obvious" target approach is to go for a n+m solution, where the 2 hierarchies are clearly separated. This of course introduces a middle-tier.
The idea is thus ObjectA -> MiddleTier -> Protocol1.
Basically, that's what Protocol Buffers does, though their problematic is different (from one language to another via a protocol).
It may be quite difficult to work out the middle-tier:
Performance issues: a "translation" phase add some overhead, and here you go from 1 to 2, this can be mitigated though, but you will have to work on it.
Compatibility issues: some protocols do not support recursion for example (xml or json do, edifact does not), so you may have to settle for a least-common approach or to work out ways of emulating such behaviors.
Personally, I would go for "reimplementing" the JSON language (which is extremely simple) into a C++ hierarchy:
int
strings
lists
dictionaries
Applying the Composite pattern to combine them.
Of course, that is the first step only. Now you have a framework, but you don't have your messages.
You should be able to specify a message in term of primitives (and really think about versionning right now, it's too late once you need another version). Note that the two approaches are valid:
In-code specification: your message is composed of primitives / other messages
Using a code generation script: this seems overkill there, but... for the sake of completion I thought I would mention it as I don't know how many messages you really need :)
On to the implementation:
Herb Sutter and Andrei Alexandrescu said in their C++ Coding Standards
Prefer non-member non-friend methods
This applies really well to the MiddleTier -> Protocol step > creates a Protocol1 class and then you can have:
Protocol1 myProtocol;
myProtocol << myMiddleTierMessage;
The use of operator<< for this kind of operation is well-known and very common. Furthermore, it gives you a very flexible approach: not all messages are required to implement all protocols.
The drawback is that it won't work for a dynamic choice of the output protocol. In this case, you might want to use a more flexible approach. After having tried various solutions, I settled for using a Strategy pattern with compile-time registration.
The idea is that I use a Singleton which holds a number of Functor objects. Each object is registered (in this case) for a particular Message - Protocol combination. This works pretty well in this situation.
Finally, for the BOM -> MiddleTier step, I would say that a particular instance of a Message should know how to build itself and should require the necessary objects as part of its constructor.
That of course only works if your messages are quite simple and may only be built from few combination of objects. If not, you might want a relatively empty constructor and various setters, but the first approach is usually sufficient.
Putting it all together.
// 1 - Your BOM
class Foo {};
class Bar {};
// 2 - Message class: GetUp
class GetUp
{
typedef enum {} State;
State m_state;
};
// 3 - Protocl class: SuperProt
class SuperProt: public Protocol
{
};
// 4 - GetUp to SuperProt serializer
class GetUp2SuperProt: public Serializer
{
};
// 5 - Let's use it
Foo foo;
Bar bar;
SuperProt sp;
GetUp getUp = GetUp(foo,bar);
MyMessage2ProtBase.serialize(sp, getUp); // use GetUp2SuperProt inside
If you need many output formats for many classes, I would try to make it a n + m problem instead of an n * m problem. The first way I come to think of is to have the classes reductible to some kind of dictionary, and then have a method to serlalize those dictionarys to each output formats.
Assuming you have full access to the classes that must be serialized. You need to add some form of reflection to the classes (probably including an abstract factory). There are two ways to do this: 1) a common base class or 2) a "traits" struct. Then you can write your encoders/decoders in relation to the base class/traits struct.
Alternatively, you could require that the class provide a function to export itself to a container of boost::any and provide a constructor that takes a boost::any container as its only parameter. It should be simple to write a serialization function to many different formats if your source data is stored in a map of boost::any objects.
That's two ways I might approach this. It would depend highly on the similarity of the classes to be serialized and on the diversity of target formats which of the above methods I would choose.
I used OpenH323 (famous enough for VoIP developers) library for long enough term to build number of application related to VoIP starting from low density answering machine and up to 32xE1 border controller. Of course it had major rework so I knew almost anything about this library that days.
Inside this library was tool (ASNparser) which converted ASN.1 definitions into container classes. Also there was framework which allowed serialization / de-serialization of these containers using higher layer abstractions. Note they are auto-generated. They supported several encoding protocols (BER,PER,XER) for ASN.1 with very complex ASN sntax and good-enough performance.
What was nice?
Auto-generated container classes which were suitable enough for clear logic implementation.
I managed to rework whole container layer under ASN objects hierarchy without almost any modification for upper layers.
It was relatively easy to do refactoring (performance) for debug features of that ASN classes (I understand, authors didn't intended to expect 20xE1 calls signalling to be logged online).
What was not suitable?
Non-STL library with lazy copy under this. Refactored by speed but I'd like to have STL compatibility there (at least that time).
You can find Wiki page of all the project here. You should focus only on PTlib component, ASN parser sources, ASN classes hierarchy / encoding / decoding policies hierarchy.
By the way,look around "Bridge" design pattern, it might be useful.
Feel free to comment questions if something seen to be strange / not enough / not that you requested actuall.