Inheritance is the most common approach for code re-usability in Object Oriented Programming. Sometimes we may need only one function to be used from a class but still inherit the entire class. This results in all the base class members, with public and protected access modifiers, also being exposed to the derived classes. Containment provides an alternative to the use of inheritance along with the ability to control which members of a class should be available to the other classes.
What is Containment ?
Containment is the simple approach, in which a class B, that needs access to the members of class A, contains an instance/object of class A, rather then inheriting the class A.
Although controlling access to the members of a class may be the reason why we may not prefer inheritance for code re-usability, there are a few points that may help us to decide whether we should use inheritance or containment. So let's discuss these points.
Point 1 : Do we need entire functionality of the base class or just some part of it
Let's take an example of a class named LivingThings, used to classify all the living things in the world, having basic functions like CanWalk(), CanTalk(), CanRun() and so on. Now with these functions, it will be meaningful, if we create a class named Mankind (representing the humans) and derive it from the LivingThings. This is because, a human being can walk, talk and run. So we can inherit common functions from the LivingThings class and extra add functionality in the derived class.
However, will it make sense to have a class named Animals, and derive it from the LivingThings class. The answer is No, since an animal can walk and run but an animal cannot talk. Inheritance will still expose this method to the derived class. So if we really want to use inheritance with this concept then it will be better to break the LivingThings class into further components (depending on the requirements) and further use them as base classes.
If we have a situation where we need to access all functions of the base class, in the derived class then this kind of inheritance code is in sync with the Liskov Substitution Principle of the S.O.L.I.D. principles.
Point 2 : Do we need a coupled or decoupled system ?
When we inherit from a base class to create a derived class, it increases the coupling between the two classes since the derived class directly uses the base class and its member functions. On the other hand, using containment, via interfaces, promotes loose coupling since we are programming against the interfaces and not the concrete classes. See the following points.
Point 3 : Do we need to change the base class at run-time
This might sound a bit complex. Let's discuss it this way. If we have multiple base classes, implementing a common interface, then we can switch between the multiple base classes, at run-time, based on some kind of condition. See the code below:
Here, we have an interface IBaseClass, with a method named GetText to return a string value. Two base classes, BaseClassA and BaseClassB implement the same interface. Now a third class, named DerivedClass, uses containment and calls the required method at run time, in other words changes the base class at run-time, based on some kind of business logic. Of course, we could have done it using inheritance, but then we will loose the advantage of loose coupling.
Point 4 : Do we need to control access to the public members class?
This is perhaps the most important point to be considered for designing the application architectur,e in other words which component of a class should be accessible to the other classes. Using inheritance, it will only control restriction to the private members. Protected and public members will be still accessible to the other classes. See the code below:
This can be controlled with containment, using interfaces. Now see the following code, where we have containment using the interfaces.
We have altered the code and added 2 public methods in the Base classes, Method1() and Method2(). Despite being public in nature, still the two methods are not accessible by the derived class. This is because, we are using the interfaces for containment, that has only one method GetText().
Point 5 : Avoid change in client code due to base class code change
Consider the following code snippets. We have a situation where we need to use a class function in another class. We have case 1, where we will use inheritance, and case 2 where we will use containment.
Here, we have a class ClassOne with a method to return a sum of 2 numbers. A derived class, ClassTwo, inherits this class and adds its own functionality to it. The client code accesses the base class method directly, using a reference of the derived class, in other words "_classTwo".
- Now, using containment, the same implementation is changed to:
Here, ClassTwo contains the ClassOne instance and adds another method to itself to create a wrapper for the client code to access the ClassOne method.
With these two implementations, we have the following two cases:
- If we change the return type of the function ReturnSum in the base class from Int32 to Double then using inheritance will directly break the client code. Consider the same kind of change when there are multiple derived classes using the same function. All will be affected by this change. Or the client code uses some kind of DLL for reference. It will directly break their code. But had we used cotainment, we would require our ClassTwo function to adjust the change and the rest of the client code will be fine, even if it uses a DLL reference.
- Now suppose your ClassOne is a third-party component and you combine it with your own ClassTwo, to generate the complete system. Here, if the third-party adds a function to its ClassOne, it will be directly available to client code. But we used containment here, it would require you to modify your ClassTwo and add another wrapper method in your class, in other words ClassTwo. This method will in turn call the newly added method in the base class or ClassOne. The client code can use the new method in the ClassOne, indirectly, by calling the wrapper method that you added to the ClassTwo.
By these points that we discussed above, containment might seem a better option then inheritance. But in reality, it's not easy to decide which one is the better approach to be used. It depends entirely on our requirements whether we should use inheritance or containment. These are some of the common situations that we encounter in our daily routine code. The one the closest to our requirements should be our preference. I hope you enjoyed reading this article!