by Zoran Horvat
Derivation is a common method of code reuse in object-oriented programming. In this article I do not plan to explain derivation itself in more than just a few words. Instead, what you will read will have to do with correctness of code which involves class derivation.
As already said, inheritance is the method of reusing code. We have a base class, which implements some features. And then we are deriving a class so that it inherits all the features of its base. Of course, we are doing all that because we want to tweak implementation or to add new features to the derived class. Image below shows principles of class inheritance. Tweaking implementation comes in form of method overriding. Adding new features comes in form of methods which do not clash with methods of the base class. There we can recognize method overloading, where method has the same name as another method of the same class or its base, but it differs in number and/or types of arguments. The other option is to simply introduce an entirely new method with new, unique name.
In either case, objects of derived class will share at least part of the public interface with objects of the base class. We are relying on this sameness when substituting objects. Thanks to inheritance, we are free to substitute an object of a base class with an object of a derived class without disturbing the object which holds the reference. That is the famous substitution principle, and it is depicted below.
If you happened to start object-oriented programming today, this could as well end the discussion on theory behind class inheritance. However, in this article I want to put an emphasis on long-term maintainability of code which incorporates class inheritance. As it turns, having a base class comes not only as an opportunity to reuse code and develop more rapidly later, but it also comes as a liability. Whenever writing a class with intention to derive from it later, we are putting our future self in danger of producing incorrect code. Let me show you one simple example and you will see how derived class can in fact break its base.
And so we come to the theme of this article. How do we write base class so that deriving from it will not jeopardize code correctness?
Below you can see one very simple class, named Vehicle. It’s only public method is named Drive() and it is supposed to make the vehicle go.
class Vehicle
{
public void Drive()
{
this.TurnEngineOn();
}
private void TurnEngineOn() =>
Console.WriteLine("Turning engine on.");
}
Internally, the Drive() method is only turning the engine on. Now, what happens if we decide to modify this behavior?
Suppose that we also have to model a bus, as a special kind of a vehicle. Bus is a kind of a vehicle, and therefore I will derive it from the Vehicle class. And then, the bus will have to be able to modify the way it is driven. It’s not the same to drive a general case of a vehicle and to drive a bus. So, what we may normally do will be to make the Drive() method virtual and then let the derived class override it with its own implementation:
class Vehicle
{
public void Drive()
{
this.TurnEngineOn();
}
private void TurnEngineOn() =>
Console.WriteLine("Turning engine on.");
}
class Bus: Vehicle
{
public override void Drive()
{
base.Drive();
this.TurnAirConditionerOn();
}
private void TurnAirConditionerOn() =>
Console.WriteLine("Turning air conditioner on.");
}
This implementation supposes that the bus driver must still turn the engine on, and then turn the air conditioner on as well. The first requirement, turning the engine on, is implemented by delegating the call to the base implementation. Then, the second part is specific to buses and therefore starting the air conditioner is modeled inside the Bus class.
The code above may be all that there is when talking about inheriting. However, that precise implementation is what I would recognize as danger to both the base and the derived class.
First of all, let’s look at the method override in the derived class:
public override void Drive()
{
base.Drive();
this.TurnAirConditionerOn();
}
This method seems to know precisely what the base implementation is doing. Otherwise, how would it know that the base implementation should be invoked before starting the air conditioner? What would change if we decided to call base implementation after starting the air conditioner?
public override void Drive()
{
this.TurnAirConditionerOn();
base.Drive();
}
An interesting question here is whether the derived class has any idea what this change has caused. Does the derived class think of base implementation like some magic and then decides when to unleash that magic relative to its own part of the business? You see, questions like this one are among the reasons for avoiding class derivation altogether.
Do you understand the consequences of this uncertainty? We don’t want uncertainty in code, because every once in a while we’ll be wrong. In terms of code execution, positioning the call to the base implementation incorrectly would cause a bug.
Anyway, there are greater dangers here, which are even easier ways to plant a bug in code. What if the one who wrote the Bus class simply forgot to call the base implementation?
public override void Drive()
{
this.TurnAirConditionerOn();
}
Knowing implementation of the base Vehicle class, we can definitely call this a bug. Bus would start driving without starting the engine. Real bus wouldn’t go anywhere, but an in-memory object representing a bus could easily travel a thousand miles without ever turning the engine on. You can imagine someone figuring that something is wrong only much later after reading some monthly report and finding that buses are quite cheap to operate since they obviously don’t burn any fuel.
Now, you can forget this artificial example, and me trying to be funny, but still you can see where this is going. Virtual functions with non-empty body in the base class can be tricky to override at times. They are tricky because mistakes in derivation may only cause indirect effects, effects which may slip to you while testing the derived class. And then, when the failure occurs, it will probably not be the object of a derived class to fail. It is likely to cause failure in some completely unrelated class which invokes the derived object expecting certain behavior inherited from base class on mind. Now, that object will fail, and that is what I meant when I said that mistakes in overriding often cause indirect negative effects.
Lucky enough, there are other ways to derive a class, ways that make base classes more resilient to derivation. It may sound harsh when I say that base class must be resilient to derivation. Deriving classes is not a disease, of course. But on the other hand, if we wish to derive from a class and not introduce defects, we have to design base class with derivation on mind. You cannot pick just any class and derive from it and expect everything to be fine after that. More often than not, there will be things which are not fine.
So, the vehicle class. If we wanted to make sure that the engine is started, then we cannot start the engine from a virtual method. There are a couple of ways to fix that. One solution is Template method pattern.
We turn the Drive() method in the base class into a non-virtual, statically invoked function, which starts the engine. But then, we have to introduce extension points. We need functions which derived class can somehow substitute without causing inadvertent effects on the base class. Below is the new implementation of the Vehicle class in which Drive() method cannot be overridden, but two additional methods – BeforeStart and AfterStart – can be overridden in the derived class because they are marked virtual.
class Vehicle
{
public void Drive()
{
this.BeforeStart();
this.TurnEngineOn();
this.AfterStart();
}
protected virtual void BeforeStart() { }
protected virtual void AfterStart() { }
private void TurnEngineOn() =>
Console.WriteLine("Turning engine on.");
}
Extension points formed by BeforeStart and AfterStart methods will have no effect in the base class because their bodies are empty. But they will become important if derived class injects its own specific behavior by overriding one or both of these methods.
In this way, Drive() method has been turned into a template method. It is not defining entire behavior of the end object. But it is defining an order of steps which is immutable in all possible objects of this class or any other class derived from it.
Now that we have an extensible base class, we can derive from it and provide specific behavior in the empty virtual methods. If we wanted to start air conditioner before starting the vehicle, we would override BeforeStart method in the Bus class:
class Bus: Vehicle
{
protected override void BeforeStart()
{
this.TurnAirConditionerOn();
}
private void TurnAirConditionerOn() =>
Console.WriteLine("Turning air conditioner on.");
}
Note that this time the Drive method cannot be overridden, simply because it is not virtual anymore. The only way in which derived class could affect behavior of its base was through methods which was meant to be overridden in the first place. There is no fear that the derived class could somehow jeopardize correctness of its base.
However, it might happen that some programmer who comes after us doesn’t know that base implementation of the BeforeStart method is empty. He or she might be tempted to really invoke the base implementation:
class Bus: Vehicle
{
protected override void BeforeStart()
{
this.TurnAirConditionerOn();
base.BeforeStart(); // Needless call
}
private void TurnAirConditionerOn() =>
Console.WriteLine("Turning air conditioner on.");
}
Programmer may make a mistake about this. But he will not introduce a bug. That is the safety valve you get for free when relying on template methods. It is much harder to break them unintentionally.
In practice I don’t like to see class hierarchies in which both base and derived class can produce objects. I find it too risky to let derived class modify behavior of the base class. On a more basic level, I feel that letting both base and derived class produce objects is counterintuitive because base and derived classes should operate on different level of abstraction. How could they both be used to instantiate objects then?
To correct that anomaly, I normally turn base class abstract. And then I turn both extension points abstract. This will force derived classes tell precisely how they want to behave:
abstract class Vehicle
{
public void Drive()
{
this.BeforeStart();
this.TurnEngineOn();
this.AfterStart();
}
protected virtual void BeforeStart() { }
protected virtual void AfterStart() { }
private void TurnEngineOn() =>
Console.WriteLine("Turning engine on.");
}
There will be no more objects of type Vehicle. All objects will have to come from derived classes. In that respect, I will have to fix the Bus class by telling that it has nothing to do after the vehicle has started:
class Bus: Vehicle
{
protected override void BeforeStart()
{
this.TurnAirConditionerOn();
}
protected override void AfterStart()
{
}
private void TurnAirConditionerOn() =>
Console.WriteLine("Turning air conditioner on.");
}
And if I wanted to have a regular car as well, I would have to construct it from a brand new class representing cars:
class Car: Vehicle
{
protected override void BeforeStart()
{
}
protected override void AfterStart()
{
this.TurnRadioOn();
}
private void TurnRadioOn() =>
Console.WriteLine("Turning the radio on.");
}
Cars have no other customizations but turning the radio on. Still, even if this didn’t exist, the car would have to be represented by a separate class because base Vehicle class cannot be instantiated.
Like it or not, this organization saves us from many dangers. Derived class will never modify behavior of the base class. Instead, it will make its base complete.
In this article we have tackled one of the oldest practical issues in object-oriented programming – fragility of base classes after parts of their implementation are overridden in the derived class.
We have seen that main problem in overriding methods is the relation between base and derived versions of the same method. That is the gray zone in which bugs are hiding, especially if the developer cannot make informed decisions.
We have seen that a better approach exists to modify behavior of a class through derivation. By providing a template method in the base class, we can let the derived class fill in the blanks with its own vision of a complete implementation without ever being able to modify those parts that the base class held under its control.
Combination of abstract methods, abstract base class and template method in the base class keeps the desired flexibility in terms that we can still use derived classes to extend behavior of the base class, while at the same time attaining a level of safety we don’t normally have with regular method overriding.
If you wish to learn more, please watch my latest video courses
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.