Introduction
In object Oriented programming we use types and data structures to represent real world concepts. Being able to reuse those types and data is essential so that we can have ways of connecting them to larger systems.
C# offers three techniques for creating dependencies between them: Inheritance, Composition and Parameterization.
Each of those can be further separated in sub-techniques. In the rest of this post I will try to give an overview of each one, along with some pros and cons when someone tries to decide which one is appropriate to use depending on his requirements.
Inheritance
Inheritance and composition are by far the two most common techniques out of the three. At first Inheritance was considered so revolutionary for code reuse that was overused. That made a lot of programs too rigid and many people started talking for the need of composition.
Eventually that led to the extreme opposite: Many people started overusing composition. Some of them went as far as saying that inheritance should never be used, but composition should always be preferred.
First let’s see the two types of inheritance that exist:
Implementation Inheritance
We have a class that provides some functionality. This class has some data and defines some behaviour on how that data can be used. This is is our base class or parent class. By using inheritance and creating a class that is called derived or child class, this class can reuse, extend and modify the behavior of our base class.
The derived class can be thought as a specialized case of the base class. For example an enemy class can provide behaviour on how an enemy attacks and a goblin child class can reuse that attack behavior, extend it with additional behaviour or completely modify it so that the attack behaviour of a goblin will be completely different from that of a general enemy.
In C#, all types implicitly inherit from the Object type or a type derived from it.
Interface Inheritance
In many OOP languages, there is a need to define a common interface for some classes. An interface is a way that defines what a class can do but does not define how to do it.
A pure abstract class, is a class that only defines the methods but no implementations and acts as an interface.
In C# multiple inheritance is not allowed, but because interfaces are an integral part of OOP, C# has a mechanism that allows us to do that. That mechanism is called (no surprise) interfaces.
The importance of interfaces is that they define what an object can do, not what it is. For example an interface that is called IWaterContainer
can define behaviour on how to add or remove water from an object, how much water it has or what is the maximum amount of water that it can contain. This does not tell us anything about what that object actually is.
An object that implements the IWaterContainer
interface, can be a bucket, a cup or even a robe. If for example we are making a game that when it rains the player character can gather water and we want a mechanism for his clothes to get wet, by implementing the IWaterContainer
in the class that represents the robe of the character, then the system that uses this interface can use the robe without even knowing that it is a robe.
If we want the player to be able to put out small fires, that system can check for all the items that implement the IWaterContainer
interface and the amount of water they currently hold. That way, the player can use any item, and the programmers can easily add or remove functionality from items that exist by implementing/removing the interface and most importantly without touching the system that is responsible, as it will only have a dependency with our IWaterContainer
interface.
This is true for everything else. A system that is responsible for the attack mechanisms of our game are dependent on an IAttack
interface, a system that is responsible for the movement on the IMovable
interface and so on.
In C# a class can have only one parent class and structs do not support inheritance, although this is true only for inheritance that the parent is a class. Classes and structs support implementation of many interfaces.
Inheritance pros and cons
Before OOP, or even now in languages that are not object oriented, many times we find the need to reuse some data or behaviour that exists in another part of our program. That, can be done by providing information in our code where that data resides in memory. In C for example we provide that information by using a pointer.
Inheritance automates that procedure. By using inheritance the language itself provides mechanisms that point to the specific area in memory that the relevant piece of code resides.
This is both a good and a bad thing. It is a good thing, because the language does most of the work and is fast to implement. It is bad, because these dependencies are resolved at compile time. This makes the program impossible to change at runtime.
An advantage of inheritance is that it makes easy to modify the implementation that is common, as we only have to modify the base class.
Here is an example: If we create an employee class that has three children: part-time employee, full-time employee and freelance contractor. Every child inherits the common behaviours of the employee and extends it or modifies it according to its needs. Any common behaviour to all employees is easy to modify, by changing the relevant piece of code in the employee class, but what happens if the company that uses our program decides to hire a part-time employee full time? We cannot change the employee type, we will have to create a new full-time employee and a mechanism that copies the relevant data to our new type.
The same can be true for the enemy class in a game. If we have goblins and orcs for example, that derive from a base enemy class, that may be fine and even desirable as the common behaviour is organized in one place: the enemy class. But if the requirements for our game also have zombie goblins and zombie orcs that goblins and orcs transform when they die, then creating new objects will be time consuming and more prone to bugs than just changing our enemy types with composition.
Another problem with inheritance is that the parent class doesn’t know anything about the type of its children. Without that knowledge, specific behaviour of the children classes cannot be called.
Finally inheritance breaks encapsulation. By changing the behaviour on the base class, we can also change the behaviour of the derived classes. That can lead to unexpected bugs, especially if we were not careful in following the Liskov Substitution Principle.
From the above it is obvious that the pros and cons of inheritance are not specific to the mechanism of inheritance but depend on our use case. The things that can be good in a specific situation, can be considered bad in another.
The ‘IS A’ relationship
Since I mentioned the Liskov Substitution Principle, I also have to talk about the common misconception that inheritance is an ‘IS A’ relationship. Although that can be true for 95% of the time, when it isn’t, we will find ourselves violating the Liskov Substitution Principle and messing up our code architecture.
The problem with the ‘IS A’ relationship is that the objects we create in code are not actual objects but representations of the objects in the real world. The relationships between objects do not transfer as relationships between their representatives. There are cases, as is the common example with the square and the rectangle that an object ‘IS A’ specific case of another object, but this relationship does not imply inheritance.
Inheritance is about reusable behaviour, it is a language mechanism that automatically helps with that and for that reason composition can always substitute inheritance, but that doesn’t mean that it always should. It is a case by case decision, that depends on the level of rigidity and fragility that we feel is proper for that part of our code depending on the use cases and the requirements.
Composition
Composition is another way of creating dependencies between our types. Instead of depending in our language to create automatic connection between types, we provide variables that are of a type that contains the specific behaviour we need.
With composition we have to pass the relevant data as parameters, either at the creation of that type or later as parameters to one of its methods.
With composition the internals of our type are not visible to the composing object, as is the case with inheritance where the internals of the base class can be visible to all of its children.
Composition can be further separated in two different ways, depending on the data of the composed objects and their life cycles:
Delegation
With Inheritance the child type can have access to the internals of its parent class, the common functionality exists on the parent class and is being used by all its children. Delegation is a composition method that can emulate that behaviour.
In composition we can pass as parameters variables to an object either at the time it is created through its constructor or to its methods. With delegation, the type with the common behaviour is the delegate.
For example:
In Inheritance we can have an enemy parent type and two children types, a Goblin and a Skeleton. In composition, the enemy type can have a variable of type IEnemyType that is an interface, which can be implemented by a Goblin or a Skeleton class. That variable will use the specific behaviours of the Goblin and the Skeleton types. By using delegation, the Goblin and the Skeleton types can have a variable that is of the type Enemy. Now the Goblin and the Skeleton, can delegate all the common functionality they have to the Enemy class, by passing their internals as parameters to the Enemy.
Aggregation and Acquaintance
With aggregation an object at runtime has the same lifetime as its owner. With acquaintance an object uses the behaviours that are defined in another object, but their lifetimes are different.
For example:
In aggregation a car object may have an engine object. The engine object is useful to us, as long as the car object exists. The car uses the specific behaviours of the engine and when the car is not needed anymore, the engine is also not needed anymore.
In acquaintance, a car object may have a passenger of the type Person. That person object has a lifetime that is different from the car object. We may not need the car anymore, but the person is being used by other types and its lifetime is unrelated to the car object’s lifetime.
Aggregation and acquaintance describe intend. Generally in a program, aggregation relationships are less in number than acquaintance relationships and more permanent. On the other hand, acquaintance relationships tend to be more dynamic. Ultimately they are not different language mechanics but are useful as concepts so that the runtime structure of a program can be easier to understand.
Pros and Cons of composition
Object composition can be used as an alternative to inheritance. Because of the use of interfaces in composition, the internals of the behaviours of the composed objects are not visible to the object that uses it. That means that with composition we are concerned with what an object does and not how it does it. In contrast with inheritance the implementation details can affect the child classes.
Another benefit of composition is that the composed object can change at runtime. As long as the composed object follows an interface, it can be substituted dynamically and that makes the program itself more dynamic.
Finally with object composition, the programmer can have greater encapsulation of data and functionality, as each class has its behaviours contained inside the class’ methods implementations.
Composition has its problems too. Generally it is harder to modify the implementation being used, in contrast with inheritance where the common functionality is all in the base class and is more straightforward to implement.
Composition can also lead to a very loosely decoupled program. Programs that make heavy use of composition are generally harder to understand, as someone has to follow all the dependencies to get an overview of the code’s behaviour.
An extension of the above, is that composition requires for a new programmer in a project to look inside each class’ implementation, which takes more time and effort. In contrast, with inheritance the dependencies between the parent and child classes are obvious, just by looking the child class declaration.
Ultimately, inheritance and composition work hand to hand, there are different cases where each one should be used, that can not be defined by general rules, but depend on the program’s use cases. Each has its pros and cons and experience makes the choice easier.
As a personal opinion, when in doubt use composition.
Parameterization
Parameterization is the third technique that a programer has in his arsenal, to create dependencies between the programs’ types. In C#, it is known as generics. The benefit of generics is that it allows to have a type that doesn’t specify the types it uses.
As with inheritance, parameterization cannot change at run-time, but in contrast with inheritance it can change the types a class uses at compile time.
A benefit of parameterization in C#, is that when used with a type instead of composition we can avoid boxing. See How to avoid boxing structs that implement interfaces in C#
Parameterization when combined with inheritance, can also be used for the implementation of the Curiously Recurring Template Pattern, as we can also take advantage of static polymorphism if performance of dynamic polymorphism is a concern.
Generally the biggest benefit of generics, is that they can be used without the need to have knowledge of the types they use.
Conclusion
That was an overview of the three different ways that we can create dependencies between our types. Each one offers different pros and cons: composition can change at runtime, inheritance is easier to implement and can make our code easier to understand and parameterization doesn’t need knowledge of the types it uses at compile time.
A combination of the above makes a program easier to use, understand and change and is preferred from the exclusive use of only one of them.
I hope you found this post useful. Thank you for reading and as always, if you have any questions or comments you can use the comments section or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.