Classes, Structs, and Objects—Delegation and Composition vs. Inheritance
Delegation and Composition vs. Inheritance
Another very important aspect of inheritance is unfavorable: Inheritance can break encapsulation and always increases coupling. I’m sure we all agree, or at least we should all agree, that encapsulation is the most fundamental and important object-oriented concept. If that’s the case, then why would you want to break it? Yet, any time you use encapsulation where the base type contains protected fields, you’re cracking the shell of encapsulation and exposing the internals of the base class. This cannot be all that good. Let me explain why it’s not and what sorts of alternatives you have at your disposal that can create better designs.
Many describe inheritance as white-box reuse. A better form of reuse is black-box reuse, meaning the internals of the object are not exposed to you. You can achieve this by using containment. Yes, that’s correct. Instead of inheriting your new class from another, you can contain an instance of the other class in your new class, thus reusing the class of the contained type without cracking the encapsulation. The downside to this technique is that most languages, including C#, require a little more coding work, but not too much. In the end, it can provide a much more adaptable design.
For a simple example of what I’m talking about, consider a problem domain where a class handles some sort of custom network communications. Let’s call this class NetworkCommunicator, and let’s say it looks like this:
public class NetworkCommunicator { public void SendData( DataObject obj ) { // Send the data over the wire. } public DataObject ReceiveData() { // Receive data over the wire. } }
Now, let’s say that you come along later and decide it would be nice to have an EncryptedNetworkCommunicator object, where the data transmission is encrypted before it is sent. A common approach would be to derive EncryptedNetworkCommunicator from NetworkCommunicator. Then, the implementation would look like this:
public class EncryptedNetworkCommunicator : NetworkCommunicator { public override void SendData( DataObject obj ) { // Encrypt the data. base.SendData( obj ); } public override DataObject ReceiveData() { DataObject obj = base.ReceiveData(); // Decrypt data. return obj; } }
There is a major drawback here. First of all, good design dictates that if you’re going to modify the functionality of the base class methods, you should override them. To override them properly, you need to declare them as virtual in the first place. This requires you to be able to tell the future when you design the NetworkCommunicator class and mark the methods as virtual. Yes, you can hide them in C# using the new keyword when you define the method on the derived class. But if you do that, you’re breaking the tenets of the inheritance relationship modeling an is-a relationship. Now, let’s look at the containment solution:
public class EncryptedNetworkCommunicator { public EncryptedNetworkCommunicator() { contained = new NetworkCommunicator(); } public void SendData( DataObject obj ) { // Encrypt the data. contained.SendData( obj ); } public DataObject ReceiveData() { DataObject obj = contained.ReceiveData(); // Decrypt data return obj; } private NetworkCommunicator contained; }
As you can see, it’s only a slight more bit of work. But the good thing is, you’re able to reuse the NetworkCommunicator as if it were a black box. The designer of NetworkCommunicator could have created the thing sealed, and you would still be able to reuse it. Had it been sealed, you definitely could not have inherited from it.
Another downfall of using inheritance is that it is not dynamic in nature. It is static by the very fact that it is determined at compile time. This can be very limiting, to say the least. You can remove this limitation by using containment. However, in order to do that, you have to also employ your good friend, polymorphism. By doing so, the contained type can be, say, an interface type. Then, the contained object merely has to support the contract of that interface in order to be reused by the container. Moreover, you can change this object at run time. Think about this for a moment and let it sink in. Consider an object that represents a container of sortable objects. Let’s say that this container type comes with a default sort algorithm. If you implement this default algorithm as a contained type that you can swap at run time, then if the problem domain required it, you could replace it with a custom sort algorithm as long as the new sort algorithm object implements the required interface that the container type expects. This technique is known as the Strategy design pattern. You’ve just seen an excellent use of a design pattern.
In conclusion, you can see that designs are much more flexible if you favor dynamic rather than static constructs. This includes favoring containment over inheritance in many reuse cases. This type of reuse is also known as delegation, since the work is delegated to the contained type. Containment also preserves encapsulation, whereas inheritance breaks encapsulation. One word of caution is in order, though. As with just about anything, you can overdo containment. For smaller utility classes, it may not make sense to go to too much effort to favor containment. And in some cases, you need to use inheritance to implement specialization. But, in the grand scheme of things, designs that favor containment over inheritance as a reuse mechanism are magnitudes more flexible and stand the test of time much better. Always respect the power of inheritance, including the damage it can cause through its misuse.
|

