It's probably about time to bring my "Design Principles To Live By" series to a close. This is the last scheduled topic although I have one or two more I may post.
Let's begin with some definitions:
Composition - Functionality of an object is made up of an aggregate of different classes. In practice, this means holding a pointer to another class to which work is deferred.
Inheritance - Functionality of an object is made up of it's own functionality plus functionality from its parent classes.
For most non-trivial problems, there will be similar code needed by multiple classes. It is not a wise idea to put the same code in more than one place (a topic for another day). There are two strategies in object-oriented programming which attempt to solve the problem of duplicate code. The one most popular in the early days was inheritance. Shared functionality was implemented in a base class which allowed each child class to inherit that functionality. A child would just not implement foo() and the parent would do the work. This works, but it is not very flexible.
Suppose that the shared functionality is some kind of encryption algorithm. Each child class will only inherit from one base class. What if there is a for different encryption algorithms? It would be possible to have multiple base classes, say AESEncryptionBase and DESEncryptionBase, but this necessitates multiple copies of the child classes--one for each base class. With more than 2 base classes, this become untenable. It also becomes very difficult to change out the encryption routine at runtime. Doing so means creating a new object and copying the contents of the old object to it.
Another difficulty is the distortion of otherwise clean class hierarchies. Each child should have an "is-a" relationship with its parent. Is a music file and AESEncryptionBase? No. Here is a particularly telling examples from Smalltalk. In Squeak (the dominant open-source Smalltalk implementation), Semaphore inherits from LinkedList. Is Semaphore a linked list? No. A linked list is used in the implementation, but a sempahore is not a specialization of linked lists.
A better approach is to contain the new functionality via composition. A class should contain instances of objects it needs to utilize functionality from. In the music file case, it would have a pointer to an EncryptionImpl class which might be AES, DES, or ROT13. The class hierarchy will stay smaller and the music file implementation does not even need to be aware of which encryption method it is using. In the Semaphore case, Semaphore would contain a LinkedList object which it would use to do the work. Clients of Semaphore would not be expecting LinkedList functionality. Extraneous methods would not need to be disabled. Composition would also allow for more flexibility later. If an implementation based on a heap or a prioritized queue were found to be advantageous, they could be without clients of Semaphore knowing.
Think twice before inheriting functionality. There are times when it is a good idea such as when there is a logical default behavior and only some child classes need to over-ride it, but if the intent is to utilize the functionality rather than expose it to child class callers, composition is almost always the right decision.
Object-oriented design and design patterns can seem complex. There are a lot of ideas and cases to consider.ReplyDelete
I think people have either forgotten, or never learned, what the type system is for.
It does, however, point to people's desire to reuse code in a way that is independent of the type system. Something like strongly-typed macros, I suppose.