Comparing with the "Framework Design Guidelines"
About
"Framework Design Guidelines" is a book written by Krzysztof Cwalina and Brad Adams on the subject of framework design. Both Krzysztof and Brad where employed by Microsoft when they helped with the design of the many .NET frameworks. They wrote a book about their (in my personal opinion excellent) work.
This document, which compares Tinymail with their guidelines, is unfinished and a bit rough at this moment.
Naming considerations
Although I prefer and like the exact type naming standards of the .NET framework, I decided to go for the naming standards of the Gtk+ library. This means that PascalCasing? is only used in the type's name, not in the method names. For method names all is written as lowercase, the type's name too, and everywhere you would have started the capital letter in a PascalCase? word except for the first letter, you add an underscore.
Namespaces in C are not possible, so unlike recommended in the book did I add prefixes to the type names. In language bindings to higher programming languages shouldn't those prefixes occur in the public proxy types.
Various considerations
Properties versus methods
About
A property in Tinymail is not done using the GObject instrumentation. A property is a method like any other that starts with get. For example tny_list_get_length.
The book (page 118) mentions several guidelines that you should take into consideration. For example when to use a a method rather than a property:
- The operation is orders of magnitude slower than a field access would be
- If there's an asynchronous version of the operation
- The operation returns a different result each time
- The operation has a significant observable side effect (changes the state, other than the setting of the field)
Although not all these cases are applicable to Tinymail's case, at least in future I'd like to stick to these guidelines too. There's not a big difference between a method and a property in Tinymail (in fact, there are no differences)., Therefore to differentiate between a method and a property the method wont use the word "get". It could, for example use "perform" or "perform_get". The reason for this is clarity for the application developer (a property will always get him a result fast) and language bindings (some languages, like .NET languages, have support for properties).
Events, callbacks, delegates (function pointers)
In Tinymail are events implemented using either TnyFolderObserver, TnyFolderStoreObserver or glib signals. Perhaps it would indeed be better to get rid of the TnyFolderObserver and TnyFolderStoreObserver and stick with only glib signals, although I'm not 100% sure about that yet. For documentation the book suggest (page 134) to use "raise" when an event happens, in glib the term is "to emit the signal". Let's stick to "emit".
When calling an event or delegate (a callback or a function pointer) there is a very interesting piece about locking at page 168 in the book. It discusses not to surround the call with a lock, as this can introduce a deadlock depending on the code that the application developer invokes in his implementation of the callback. This was early-on spotted in Tinymail too and has been dealt with in the exact same way as suggested in the book. If new such cases occur special care should be taken to do the same thing.
Enums vs. boolean parameters
The book (page 150) suggest to use enums rather than booleans. This is indeed not consistent within Tinymail and in future I would like to make this consistent to enums too.
Sealing classes, data encapsulation
Although I agree with the reasons for extensibility (page 163 in the book), these techniques are often quite hard to do in C. Usually you can "hide" the structure of memory from the application developer by not including the .h files of private structs (like a so-called priv variable in the instance of a class). Using such a priv of course makes debugging a lot harder too but in Tinymail all private members are already in such a variable. This is not the same as sealing though, but rather data encapsulation.
For sealing we can for example decide not to make methods virtual (don't provide a vfunc function pointer in the class struct of the implementation of an interface, so that you can't override a method). In sharp contrast did I make nearly all of the methods virtual. My reason for this is flexibility for the application developer. It indeed means that I loose security, certainties (about the performance of an overridden method), the possibility of contract breaking by the application developer, etc etc. My defense however is that none of the virtuals are part of any of the API nor ABI promises. This means that once an application developer overrides an existing method, he is on his own for porting to future versions of Tinymail. I know it's a poor defense. I'm limited by the capabilities of the GObject type system and the possibilities of what the application developer can do in C with memory.
Classes vs. structs
In the book the discussion is mostly about value types versus reference types. In C this is comparable to types that are allocated on the stack versus types that are allocated on the heap. Because there's a clear distinction between the two and because usually C developers have a very good knowledge about the difference, isn't this as unclear as on certain higher programming languages. Although the distinction is, indeed, more or less the same and the reasons for preferring the one over the other too.
In Tinymail is nearly every public type a class that'll get allocated on the heap and passed to methods as references. Heap allocations are allocated using the GSlice slab allocator in glib and should therefore be not as slow as the normal malloc vs. free syscalls. They also use less heap admin memory space.
Interface vs class design considerations
In the book starting page 77 Krzyztof Cwalina gives the good argument that a class is more easy to extend in future than a fixed interface.
My reason for nevertheless usually picking interfaces is that most of the Tinymail types are fully implemented within the framework and are not always interfaces so that the application developer will create implementations.
I also agree with Jeffrey Richter on page 81 that it's a good practise to define an interface first, and provide an abstract base class that implements it. You'll notice that a lot of Tinymail's types both have an interface and a fully implemented abstract or concrete class that can be inherited and further implemented.
The consideration on page 82 at the bottom (achieving a similar effect to multiple inheritance) is also often (but not always) used in Tinymail's types.
I strongly agree with Brian Pepin on page 83 that a well-defined interface does exactly one thing. I also agree with Jeffrey on the same page that a derived class has a IS-A relationship with the base, whereas an implementation of an interface has a CAN-DO relationship with the interface.
I conclude that some of the types can probably be refactored to be abstract classes in stead. For example the Folder, Msg, MimePart? types. Maybe in future I will refactor them indeed. Types like FolderStore?, however, feel like interfaces to me: some folders CAN-DO (can be) a folder store. Some accounts, usually store accounts, CAN-DO (can be) a folder store. The FolderStore? type is one that needs this multiple inheritance like feature.
Another reason why Tinymail uses interfaces as much is the flexibility to implement a complete new version of a type in a new library and make it possible to for example dynamically or at compile time switch between the different possible implementations. Although I agree that a high level abstract class makes this possible too.
Finally I disagree, for Tinymail (not for a framework like .NET, where I'd fully agree) that you can't ever add members to an interface that has previously shipped, unless in the same major version of Tinymail (as you'd break the API). You should consider adding the interface to the type that implements the interface: for example to TnyCamelMsg in stead of TnyMsg. Later, for example when the major version number changes, this API can be merged to the interface if needed and generic enough.
I'd like to highlight these items from the book:
Consider defining an interface if you need to support its functionality on types that already inherit from some other type
Avoid using marker interfaces (interfaces using no members)
Do provide at least one type that is an implementation of an interface
Do provide at least one API consuming each interface you define
Enum considerations
Just like strongly recommended in the book do I require all enumerations and flags to be as typed as possible in Tinymail. This means that no API should, in case the parameter is either a flag or an enum, accept an int. It must accept a typedef-ed version of the int. Although this, indeed, makes no difference for the compiler, it does make a big difference for the developer and the development tools being used.
Do use an enum to strongly type parameters, properties and return values that represent sets of values
In stead of the "Do provide a value of zero on simple enums" I require all fields in the enum to have their exact value be specified. I don't allow enums in Tinymail that let the value be chosen by the compiler. There's no specific reason for this but simple API clarity: it's by far more clear to the application developer want the value will be. Although this doesn't mean that an application developer should also use that value as an integer (the developer should use the enum's label, never the integer).
Do name the zero-value of flag enums None. The reason being that a zero-value can't easily be used when OR-ing it with other flags (so that the application developer knows this). Avoid using the zero-value for an actual meaningful member of the list. Especially when the enumeration is a flags one.
On page 101 the authors warn that adding values can mean that certain code paths might not cope with the newly added item. For example man switch-case constructs might drop in the default handler or not execute any code in the switch statement. Therefore: always make sure the default handler will deal with values that got added to the enum. For example state or warn that the current version of the component can't cope with that potential future value.
