The Secret Life of Objects: Inheritance
Want to learn more about the secret life of objects? Check out this post on the types of inheritance methods you should be avoiding.
Join the DZone community and get the full member experience.Join For Free
This post is the second part of a series of posts concerning the basic concepts of object-oriented programming. The first post was focused on Information Hiding. This time, I will focus on the concept of inheritance. Whereas most people think that it is easy to understand, the inheritance is a piece of knowledge that is very difficult to master correctly. It subtends a lot of other object-oriented programming principles that are not always directly related to it. Moreover, it is directly related to one of the most potent forms of dependency between objects.
Out of there, many articles and posts rant against object-oriented programming and its principles, just because they don’t understand them. One of these posts is Goodbye, Object Oriented Programming, which treats inheritance and the others object-oriented principles with a very biased and naive perspective.
So, let’s get this party started, and let’s demystify the inheritance in object-oriented programming once for all.
Providing the definition of inheritance would be a good place start, as we being to analyze the concept:
Inheritance is a language feature that allows new objects to be defined from existing ones.
Given that we have a class for each object,
an object’s class defines how the object is implemented, i.e. the internal state and the implementation of its operations. [..] In contrast, an object’s type only refers to its interface, i.e. the set of requests to which it can respond.
Because of this:
It is important to understand the difference between class inheritance and interface inheritance (or subtyping). Class inheritance defines an object’s implementation in terms of another object’s implementation. In short, it’s a mechanism for code sharing. In contrast, interface (or subtyping) describes when an object can be used in place of another.
The Big Misunderstanding
By now, we understood that there are two types of inheritance — class inheritance and interface inheritance (or subtyping). When you used the former, you’re performing code reuse that you know you will need a class and a piece of code. You are extending it to reuse that code, redefining all the stuff that you don’t need.
Clearly, the class
AlgorithmThatReadFromKafkaAndWriteOnMongo inherits from
AlgorithmThatReadFromCsvAndWriteOnMongo only to reuse the code that writes on MongoDB. The drawback is that we have to pass
null to the constructor of the parent class. However, something is not right here.
The Banana, Monkey, Jungle Problem
The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana, but what you got was a gorilla holding the banana and the entire jungle.
This quote is by Joe Armstrong, the creator of Erlang. For this principle, every time you try to reuse some class, you need to add dependencies to its parent class, and to the parent class of the parent class, and so on.
Using the previous example, if you want to reuse the class
AlgorithmThatReadFromKafkaAndWriteOnMongoAndLogs, you also need to import classes
This code demonstrated above is the problem with class inheritance and code reuse.
As discussed in the post about dependency, class inheritance is the worst form of dependency between two classes. We already related the concept of dependency between classes and the probability of modifying one against one change in another.
It is normal that, if two classes are linked through this type of relation, they must be used together. This fact is that the problem of inheritance is tight coupling.
First of all, do not use class inheritance. Do not use inheritance to reuse some code. Use inheritance so share behavior among components.
Afterward, do not put more than one responsibility inside a class. I don’ t dwell on responsibility. I already wrote about this topic in the Single-Responsibility Principle done right. However, if you let your components implement more than one use case, you let two different clients of a component depend from different slices of it. Then, the problem is not inheritance; it is in your whole design. A component that implements only one responsibility is very likely to inherit from a hierarchy that contains only one type, which is most of the times an abstract type.
Concerning our previous example, there is a problem of code reuse and a violation of the Single-Responsibility Principle. Each class is doing too much, and it is doing in the wrong way.
Inheritance and Encapsulation
If you think about it, there is a big problem with class inheritance — it seems to break encapsulation. A subclass (a class that inherits from another) knows and can virtually access the internal state of the base class. We are breaking encapsulation. So, we are violating the first principle of object-oriented programming, are not we?
Well, not properly. If a class you inherit from exposes some
protected state, it is like that class is exposing two kinds of interfaces.
The public interface lists what the general client may see, whereas the protected interface lists what inheritors may see.
The problem is that maintaining two different interfaces of the same type is very hard. Adding more types of clients to a class means to add dependencies. The more dependencies, the higher the coupling. A high coupling means a higher probability of changes cascades among types.
So, don’t use class inheritance. Stop!
If you can’t use inheritance, why is this considered one of the essential principles of object-oriented programming? Technically, inheritance is still possible under certain circumstances.
Subtyping or Reusing Behavior
The first case in which you are allowed to use inheritance is subtyping, also known as behavior inheritance or interface inheritance.
Class inheritance defines an object’s implementation in terms of another object’s implementation. In short, it’s a mechanism for code and representation sharing. In contrast, interface inheritance (or subtyping) describes when an object can be used in place of another.
The above sentence is very informative. It screams to the world that you must not override methods of superclasses if you want to reuse behavior.
Following this principle, the only types from which we can inherit are interfaces and abstract classes, avoiding the override of any concrete method of the latter.
When inheritance is used carefully (some will say properly), all classes derived from an abstract class will share its interface. [..] Subclasses merely adds or overrides operations and does not hide the operations of the parent class.
Why is this principle so important? Because of polymorphism depends on it. In this way, clients remain unaware of the specific type of objects they use, reducing implementation dependencies drastically.
Program to an interface, not an implementation.
Favor Object Composition Over Class Inheritance
The only way we have to extend the behavior of a class is to use object composition. This can be done by obtaining new functionalities by assembling objects to get more complex functionalities. Objects are composed using their well-defined interfaces only.
This style of reuse is called the black-box reuse. No internal details of objects are visible from the outside, producing the lowest possible dependency degree.
Object composition has another effect on system design. Favoring object composition over class inheritance helps you keep each class encapsulated and focused on one task and on a single responsibility.
Our initial example can be rewritten using two new highly specialized types: a
Reader and a
The Liskov Substitution Principle
It seems that inheritance should not be used in any case. You must not inherit from a concrete class, only from abstract types. Is it correct? Well, there is a specific case in which the inheritance from concrete classes is allowed.
The Liskov Substitution Principle (LSP) tells us exactly which is the case.
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
A class can inherit from another concrete class if — and only if — it does not override the pre-postcondition of the base class. In a derivated class, preconditions must not be stronger than in the base class. In a derivate class, postconditions must be stronger than in the base class.
When redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.
The above is called design-by-contract. Respecting this principle avoids the redefinition of the extrinsic public behavior of a base class — that is the behavior the clients of a class depend upon.
Returning to our previous example, take the class
AlgorithmThatReadFromCsvAndWriteOnMongoand its subclass
AlgorithmThatReadFromKafkaAndWriteOnMongo. The LSP tells us that, whenever we need a reference to the first, we can use a reference to the second instead. However, in our example, we can’t. The first read from a CSV; the second from Kafka.
It’s easy to write a test that uses an object of type
AlgorithmThatReadFromKafkaAndWriteOnMongowhere an object of type
AlgorithmThatReadFromCsvAndWriteOnMongo is requested. It’s easy to see it fail.
Why? The reason is that we have violated base class invariants. We tried to reuse code and not to reuse behavior.
The same, old story.
To sum up, let's try to avoid the reuse of code and avoid class inheritance, if possible. Try to reuse behavior and try to use subtyping. I prefer not to extend a concrete class. Alternatively, if you have to, verify that you fulfill the Liskov Substitution Principle.
If you follow these simple rules, the dependency degree between your classes will stay low, and your architecture will be simpler to maintain and evolve. Easy, right?
- Four Basic Concpets in Object-Oriented Languages, Chapter 10: Concepts in Object-Oriented Languages. Concepts in Programming Languages, John C. Mitchell, 2003, Cambridge University Press
- How Design Patterns Solve Design Problems, Chapter 1: Introduction. Design Patterns, Elements of Reusable Software, E. Gamma, R. Helm, R. Johnson, J. Vlissides, 1995, Addison-Wesley
- The Liskov Substitution Principle (LSP). Agile Principles, Patterns, and Practices in C#, Robert C. Martin, Micah Martin, 2006, Prentice Hall
- What is an example of the Liskov Substitution Principle?
Published at DZone with permission of Riccardo Cardin, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.