Inheritance is something that comes up quite often during my programming experience. Whenever I have classes that share some logic or properties, I think of inheritance. However, sometimes, using inheritance ends up making a design more brittle. Through my experience and reading, I have learned a few things about inheritance.
Several languages such as Java and C# do not support multiple inheritance. In such languages, a class can not extend more than one abstract class. This is because multiple inheritance can cause ambiguities. For example, suppose I work for a company that specializes in building robotic birds. I am responsible for writing the software to control the robots. In my application, I model different types of bird. As seen in the code snippet below, a robotic bird can swim or fly, depending on its type.
public abstract class Bird { public abstract void Move(); } public class FlyingBird : Bird { public override void Move() { Console.WriteLine("I'm flying"); } } public class SwimmingBird: Bird { public override void Move() { Console.WriteLine("I'm swimming"); } }
The software works great, and the company has been making a lot of money selling the robots. The company got demands from customers to build robotic birds that can both swim and fly. My boss needs me to update the software to support the new feature. Since I already have the codes for swimming and flying from the FlyingBird and SwimmingBird classes, if I can just create another class that extends from both of those classes, life would be easier. However, because C# does not allow multiple inheritance, the code snippets below is not valid.
public class SuperBird : FlyingBird, SwimmingBird // This code will not compile. { public override void Move() { Console.WriteLine("I'm swimming and flying"); } }
In the above snippets, because both FlyingBird and SwimmingBird already extend from Bird and override the Move
() method, it’s ambiguous which Move()
method the SuperBird is overriding. This is why several languages including C# and Java do not allow multiple inheritance.
If I diagram the classes, the shape looks like a diamond. In fact, a term for this type of problem is the “diamond problem”.
In the book “Refactoring Workbook”, the author covers some of the code smells related to inheritance, one of which is the “Combinatorial Explosion”. This smell happens when you have an inheritance hierarchy of which the classes are heavily specialized such that you whenever you need to add a new behavior, you have to create a new class. In the robotic bird example, suppose the company wants to build robotic birds that employ specific swimming types such as wing-propulsion, foot-propulsion, plunge-diving and flying styles that include gliding and flapping. Using inheritance, I may have created classes such as: WingPropulsionSwimmingBird, FootPropulsionSwimmingBird, PlugeDivingSwimmingBird, GlidingFlyingBird, FlappingFlyingBird etc … Even more, I may have created classes to represent robotic birds that can do a combination of these styles. As you see, the way I use inheritance to model the different types of bird is troublesome. Soon I will have ended up with an explosion of subclasses.
When using inheritance, one of the tradeoffs is loosen encapsulation. When I create a subclass that inherits from a parent class, I basically couple the class and its parent. In an inheritance hierarchy, the subclasses often have access to its parent’s data fields that are normally hidden from outside classes.
The coupling between classes in an inheritance hierarchy can have a cascading effect. For example, supposes I have a class A that extends from class B, and a client C that depends on A. When I make a change in B, I have to recompile B and in turn C, even though C does not reference B directly.
In statically typed languages, inheritance is the strongest, and most rigid, of all the source code relationships; consequently, it should be used with great care. In dynamically typed languages, inheritance is less of a problem, but it is still a dependency—and caution is always the wisest choice.
Martin, Robert. “Chapter 11 DIP: The Dependency Inversion Principles.” Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Robert C. Martin Series), 1st ed., e-book, Pearson, 2017, p. 84.
Whenever two classes appear to duplicated codes, I have the tendency to relate them via inheritance, and have the commonalities in the base class so I can reuse code. Sometimes, the similarities are just coincidence.
In an angular project I have been a part of, we build a component and layout to allow a user to submit a type of electronic correspondence. The user goes through a few steps for submitting a correspondence. As the project evolves, a special type of correspondence comes up. The process for submitting the new correspondence type has a few steps that are similar and other steps that are different to those for submitting the existing correspondence. However, as the project evolves, the business rules for handling the new correspondence type vs the existing correspondence type have deviated further from each other. As such, for handling the existing correspondence type, we have opted to create a new component and layout that does not depend on the existing component via inheritance. For the common functionalities, we have extracted them out to a service that both components can use.
Another sign that inheritance is not a good use is when I have more than one thing to vary. In the robotic bird example, inheritance is probably okay if I only need to vary the subclasses by action type (flying vs swimming). However, if I have to also have to vary the subclasses by another attribute such as color, then using inheritance is not appropriate.
If I have more than one reason to vary something, I can only use inheritance for a single axis of variation. So, if I want to vary behavior of people by their age category and by their income level, I can either have subclasses for young and senior, or for welloff and poor—I can’t have both.
Fowler, Martin. “Chapter 12 Dealing With Inheritance.” Refactoring: Improve the Design of Existing Code, 2nd ed., e-book, Addison-Wesley Professional, 2018, p. 424.
A good guidance to determine whether inheritance can work is using the Liskov Substitution Principle. If my codes adhere to the Liskov Substitution Principle, I should be able to substitute a class with any of its subclass without altering the behaviors of the program.
A classic example of a violation of the Liskov Substitution Principle is the rectangle/square problem. Both a rectangle and square has a width and a height. For both types of shape, the calculation of perimeter and area are the same. For instance, to get the perimeter, you add up the length of all sides. For area, you multiple the width and the height.
A naive representation of square/rectangle may be using inheritance.
public class Rectangle { public virtual int Width { get; set; } public virtual int Height { get; set; } } public class Square: Rectangle { public override int Width { get => base.Width; set { base.Width = value; base.Width = value; } } public override int Height { get => base.Height; set { base.Height = value; base.Width = value; } } }
This setup violates the Liskov Substitution principle because you can’t really substitute a rectangle with a square and vice versa. That’s because a square has the characteristics that both the width and the height are equal. Every time you set the width, the height should change to the same value.
public void main(string[] args) { private void DoSomething(Rectangle rectangle) { rectangle.Width = 20; rectangle.Height = 30; Console.WriteLine("Width: " + rectangle.Width); // Expects 20; Console.WriteLine("Height: " + rectangle.Height); // Expects 30; } DoSomething(new Square()); }
In the above snippet, the client expects the width to be 20, and the height to be 30 as the shape is a rectangle. However, because the caller passes in a square, the width and the height are the same. In other words, the behavior of the program has changed, breaking the client. In this case, it’s not appropriate to use inheritance as you can’t really substitute a rectangle with a square.
In the book “Refactoring: Improving the Design of Existing Code”, the author suggests he uses inheritance frequently, especially for expressing behaviors that vary from category to category. When inheritance turns bad, the author refactors and uses an alternative such as delegate or composition.
There is a popular principle: “Favor object composition over class inheritance” (where composition is effectively the same as delegation). Many people take this to mean “inheritance considered harmful” and claim that we should never use inheritance. I use inheritance frequently, partly because I always know I can use Replace Subclass with Delegate should I need to change it later. Inheritance is a valuable mechanism that does the job most of the time without problems.
Fowler, Martin. “Chapter 12 Dealing With Inheritance.” Refactoring: Improve the Design of Existing Code, 2nd ed., e-book, Addison-Wesley Professional, 2018, p. 425.
Inheritance is appropriate when implementing the Template Method design pattern. For example, you may have a base class in which you define an algorithm that perform a series of steps, and subclasses the override some of the steps. In this way, the parent class contains the template, and the subclasses customize the template by overriding some of the steps defined in the base class.
In the book “The Pragmatic Programmer”, the author suggests other alternatives that have the same benefits as inheritance but without the downsides: interfaces and protocol, delegation, mixins and traits.
Interfaces allow you to relate multiple types together without coupling them. Languages like C# and Java allow a class to extend multiple interfaces, but not multiple classes. This is because an interface by default does not contain an implementation, so there is no ambiguity when the class extends from multiple interfaces.
Interfaces and protocols give us polymorphism without inheritance
Thomas, David, and Andrew Hunt. “Chapter 5. Bend, or Break.” The Pragmatic Programmer: Your Journey To Mastery, 20th Anniversary Edition (2nd Edition), 2nd ed., Addison-Wesley Professional, 2019, p. 162.
Delegation and composition allows you to reuse codes with less coupling than inheritance. For instance, you can define common methods in class A. Other classes can reuse these methods by referencing class A without extending it. This way, you can strengthen access control by not exposing the private fields and methods of class A to other classes. You can further reduce the coupling by following the Dependency Inversion Principle. Instead of directly referencing class A, you can define an interface which exposes the common methods and have class A implements the interface. Other classes can reference the interface instead of depending directly on class A. At run time, you can use dependency injection to wire class A as a concrete implementation of the interface. Several software design patterns also make use of delegation and/or composition including Decorator, Adapter, and Composite.
With mixins, you can create a class that has a mix of behaviors from different classes without directly using inheritance or delegation. C# supports mixin using interfaces with default interface methods. For example, you can create an interface and provides default implementations of one or more methods. A class can extend the interface and either uses the default implementations or override the behaviors. The class can have mixed functionalities by extending multiple interfaces with different capabilities. To learn more, check out the document.
In object-oriented programming languages, a mixin (or mix-in) is a class that contains methods for use by other classes without having to be the parent class of those other classes. How those other classes gain access to the mixin’s methods depends on the language. Mixins are sometimes described as being “included” rather than “inherited”.
“Mixin.” Wikipedia, 27 Nov. 2021, en.wikipedia.org/wiki/Mixin.
What text books tell you about inheritance in OOP is wrong
Tutorial: Mix functionality in when creating classes using interfaces with default interface methods
Dependency Inversion Principle
Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Robert C. Martin Series)
The Pragmatic Programmer: Your Journey To Mastery, 20th Anniversary Edition (2nd Edition)
Analyzing a rental property through automation
Web scraping in C# using HtmlAgilityPack
Notes on the three programming paradigms
Three essential libraries for unit testing a .NET project
The Liskov Substitution Principle
Write generic codes with Delegate, Func, Action, and Anonymous Functions in C#
Async/Await beginner mistake: Using async void in non event handler.