Writing Safe Inheritance
Background
I found myself writing many classes, abstractions, and a lot of inheritance at Microsoft. Something that I found myself struggling was writing well structured inheritance. Argueably I’d choose Composition over Inheritance to avoid the flaws of Inheritance. But, when I do find myself using Inheriance I will think about what I will discuss in this blog. See you must be very careful when choosing to use Inheritance, I learned this the hard way by making poorly design decisions. Inheritance became a nightmare! But, as I became more experienced I noticed patterns and determine scenarios in which I will use Inheritance and how to use it.
See, at schools or textbooks, you will simply learn about Inheritance and its mechanism but not how to use it properly.
Let me first point out the complexities I’ve faced:
- Subclasses were coupled to parent classes - I will find myself needing to override functions I didn’t need. If I went ahead and break the uneccessary props/methods from these base classes then this will cause a expensive refactor since many classes depending on this will start breaking. There were dependency issues — modifying the base class can accidentally cause problems for the subclasses, so you’re stuck with your base class, and don’t even get compiler errors here. One way to avoid this is have every subclass override every method, in which case I’m just using interfaces basically…
- This type of complexity can lead to ambiguities and confusion about which superclass’s methods and properties are being used, making the code harder to read and maintain.
- Misuing
virtual
andoverride
keywords - in certain cases I found myself using thevirtual
keyword tooverride
the method in the child class and not knowing wheen to call the basevirtual
method. There will also be cases where I will override thevirtual
method when it is doing side effects. - Be consistent with your inheritance across your codebase! Say if you’re dealing with a part of a codebase
Extensions
and it uses, say,ExtensionBase
base class that its child classes uses across the codebase, and you start using another base class the hierachy will start being confusing. If you are going to do this, have a good reason for this, do you truly need to do this?
Alternatives
So what solutions can we try?
- My goto will be to use Composition. If you’d like to see a code walkthrough of, learn in the Avoid this Inheriance Problem section.
- Leverage Interfaces to achieve this:
1interface IAnimal2{3void Eat();4}56class Animal : IAnimal7{8public void Eat()9{10Console.WriteLine("Eating...");11}12}1314class Dog : IAnimal15{16public void Eat()17{18Console.WriteLine("Dog is eating...");19}20}
- Using Abstract Classes and Interfaces Together. This approach provides a balance between reusing code (through abstract classes) and ensuring polymorphic behavior (through interfaces):
1abstract class Content2{3public abstract void Display();4}56interface IStorable7{8void Save();9}1011class Article : Content, IStorable12{13public override void Display()14{15// Display the article16}1718public void Save()19{20// Save the article to a database21}22}
- Understand when to use
virtual
andabstract
:
I follow these principels:
- If the base method has side effects, then keep non-virtual.
- If the base method only returns a result, then make it virtual and override it freely.
- If you need the base result, call the base method, and use its result freely.
See for example of a bad design choice:
- As you may see,
StartEngine()
may have side effects which is a big NO to override! - When will you call the
base.StartEngine()
?
1public class Vehicle2{3public string Make { get; set; }45public string Model { get; set; }67public string EngineStatus { get; private set; } = "Off";89public Vehicle(string make, string model)10=> (Make, Model) = (make, model);1112// this code will cause side effects!13public virtual void StartEngine()14{15// some code...1617EngineStatus = "idle";1819// some code...20}21}2223public class Car : Vehicle24{25public string ScreenContent { get; private set; } = "Off";2627public Car(string make, string model) : base(make, model)28{29}3031public override void StartEngine()32{33ScreenContent = "...";34base.StartEngine(); // when to call this?35}36}
To fix this, we can leverage abstract
methods by doing the following:
1public abstract class Vehicle2{3public string Make { get; set; }45public string Model { get; set; }67public string EngineStatus { get; private set; } = "Off";89public Vehicle(string make, string model)10=> (Make, Model) = (make, model);1112public void StartEngine()13{14if (EngineStatus is not "Off") return;1516BeforeEngineStart();17EngineStatus = "Idle";18AfterEngineStart();19}2021// Freely to override necessary methods22protected virtual void BeforeEngineStart() { }23protected virtual void AfterEngineStart() { }24}2526public class Car : Vehicle27{28public string ScreenContent { get; private set; } = "Off";2930public Car(string make, string model) : base(make, model)31{32}3334protected override void AfterEngineStart()35{36ScreenContent = "...";37base.StartEngine(); // you may call this before or after...38}39}
I hope this helps!
x, gio