📜 ⬆️ ⬇️

Composition or inheritance: how to choose?

At the beginning...


... there was no composition, no inheritance, only code.


And the code was slow, repetitive, inseparable, unhappy, redundant and exhausted.


The main tool for code reuse was copy-paste. Procedures and functions were rare, suspicious newfangled things. Calling procedures was an expensive pleasure. Parts of the code, separated from the main logic, were puzzling!


Dark times were.


But here the ray of the PLO appeared above the world ... True, for several decades 1 no one noticed this. Until the graphical interface 2 appeared , which, as it turned out, was very, very short of OOP. When you click on a button in a window, what could be simpler than sending the message “Pressing” to the button (or its representative) 3 and getting the result?


And here the PLO took off. Many 4 books were written, countless 5 articles bred. So today, everyone can in object-oriented programming, right?



Alas, the code (and the Internet) says that not so


The most heated debates and the most misunderstanding seem to cause a choice between composition and inheritance, often expressed by the mantra " prefer composition to inheritance ." That's about it and talk.


When mantras harm


In everyday terms, "prefer composition over inheritance" is generally normal, although I am not a fan of mantras . Despite the fact that they often carry the grain of truth, it is too easy to succumb to the temptation and mindlessly follow the slogan, not realizing what lies behind it. And it always goes sideways.


Jaundiced articles with headlines like "Inheritance is evil" 6 is also not for me, especially if the author tries to substantiate his attacks, first incorrectly applying inheritance, and then concluding that it is all to blame. Well, like "hammers - sucks, because they can not screw the screw."


Let's start with the basics.


Definitions


Further in the article I will understand by OOP "classical" object language, which supports classes with properties, methods and simple (single) inheritance. No interfaces, impurities, aspects, multiple inheritance, delegates, closures, lambdas, anything but the simplest things:



Inheritance is fundamental


Inheritance is the fundamental concept of OOP. In a programming language, there may be objects and messages, but without inheritance it will not be object-oriented (only based on objects, but still polymorphic).


... like composition


Composition is also a fundamental property, and any language. Even if the language does not support composition (which is rare nowadays), people will still think in terms of parts and components. Without composition it would be impossible to solve complex tasks in parts.


(Encapsulation is also a fundamental thing, but now it’s not about it)


So what is all the fuss about?


Well, and composition, and inheritance are fundamental, what's the matter?


But the fact is that one might think that one can always replace the other, or that the first is better or worse than the second. Software development is always the choice of a reasonable balance, a compromise.


Everything is more or less simple with the composition, we constantly encounter it in life: the chair has legs, the wall consists of bricks and cement, and the like. But inheritance, despite its simple definition, can complicate and confuse everything, if you do not think carefully about how to apply it. Inheritance is a very abstract thing, you can talk about it, but you just don’t touch it. We, of course, can imitate inheritance using composition, but this is usually too much fuss. What the composition is for is obvious: assemble the whole from parts. But inheritance is more complicated, because it is about two things at once: the meaning and the mechanics.


Semantic inheritance


As classification of taxa in biology organizes them in a hierarchy, so inheritance reflects a hierarchy of concepts from the subject area. Organizes them from the general to the particular, collects related ideas in the branch of the hierarchical tree. The meaning (semantics) of a class is mostly expressed in its interface - a set of messages that a class is able to understand, but is also determined by the messages with which the class responds. Inherited from the ancestor - be kind not only to understand all the messages that the ancestor could understand, but also to be able to respond as he did, and therefore inheritance links the heir to the ancestor much more than if we took ancestor instance as a component. Please note that even if a class does something quite simple, has almost no logic, its name carries a significant semantic load, the developer draws important conclusions about the subject area from it.


Mechanical inheritance


Speaking of mechanical inheritance, we mean that inheritance takes the data (fields) and behavior (methods) of the base class and allows you to use them again or add them to the heirs. From the point of view of mechanics, if the descendant inherits the implementation (code) of the ancestor, then its interface will inevitably receive.


I’m sure that this dual nature of inheritance 7 in most OO languages ​​is to blame for misunderstanding. Many people think that inheritance is to reuse code, although it is not only for this. If you give excessive value to reuse - expect trouble in architecture. Here are a couple of examples.


How not to inherit. Example 1


 class Stack extends ArrayList { public void push(Object value) { … } public Object pop() { … } } 

It would seem that the class is Stack , all is well. But look carefully at its interface. What should be in a class named Stack? Methods push() and pop() , what else. And we have? We have get() , set() , add() , remove() , clear() and a bunch of junk inherited from ArrayList , which the stack doesn’t need at all.


It would be possible to redefine all undesirable methods, and some (for example, clear() ) could even be adapted to our needs, but isn’t it a lot of work due to one design error? In fact, three: one semantic, one mechanical and one combined:


  1. The statement "Stack is an ArrayList" is false. Stack not a subtype of ArrayList . The task of the stack is to ensure the fulfillment of the LIFO rule (the last one arrived, the first one left), which is easily satisfied by the push / pop interface, but is not observed at all by the ArrayList interface.
  2. Mechanically inheriting from an ArrayList breaks encapsulation. Client code should not be aware that we decided to use an ArrayList to store stack elements.
  3. Finally, implementing the stack through ArrayList we mix two different subject areas: ArrayList is a collection with random access, and the stack is a concept from the world of queues, with strictly limited (not arbitrary) 8 access.

The last point is insignificant at first glance, but an important thing. Let's take a closer look at it.


How not to inherit. Example 2


A common mistake in inheritance is to create a model from the domain, inheriting it from a ready implementation. Here, for example, we need to allocate some of our customers ( Customer class) into a specific subset. Easy! Inherited from ArrayList<Customer> , call it CustomerGroup and rushed.


It was not there. Doing so we again confuse the two subject areas. Try to avoid this:


  1. ArrayList<Customer> is already the heir of the list, a collection-type utility, a finished implementation.
  2. CustomerGroup is a completely different thing - a class from the subject area (domain).
  3. Classes from the domain must use implementations, not inherit them.

The domain layer does not need to know how everything is done inside there. Speaking about what our program does, we operate with concepts from the subject area, and we do not want to be distracted by the nuances of the internal structure. If we only see the code reuse tool in inheritance, we will fall into this trap time after time.


It's not about single inheritance.


Single inheritance is still the most popular OOP model. It inevitably entails the inheritance of the implementation, which leads to a strong engagement between the classes. It may seem that the trouble is that the branch of inheritance, we have only one for both needs: the semantic and mechanical. If used for one, then for another it is already impossible. And if so, maybe multiple inheritance will fix everything?


Not. The inheritance relation should not cross the boundaries between subject areas: instrumental (data structures, algorithms, networks) and applied (business logic). If the CustomerGroup ArrayList<Customer> and at the same time, say, DemographicSegment, then the two subject areas will intertwine with each other, and the "species" of the objects will not be obvious.


It is preferable (at least from my point of view) to do so. We inherit from the minimum instrumental classes available in the language, just enough to implement the "mechanical" part of your logic. Then we combine the resulting parts with composition, but not inheritance. In other words:


Only other tools can be inherited from tools.


This is a very common mistake for newbies. What is not surprising, because it is so easy to take and inherit. Rarely where you will meet discussions, why exactly this is wrong. Once again: business entities should use tools, not be them. Flies (tools) - separately, cutlets (business models) - separately.


So when is inheritance necessary?


Inherited as necessary


Most often - and with the greatest return - inheritance is used to describe objects that are slightly different from each other (in the original, the term “differential programming” is used.) For example, we need a special button with small additions. Normally, we inherit from the existing button class. Because our new class is still a button, and we completely inherit the Button class API, its behavior and implementation. New functionality is only added to the existing one. But if the successor removes some of the functionality, it is a reason to think about whether inheritance is necessary.


Inheritance is most useful for grouping similar entities and concepts, defining class families, and, in general, for organizing terms and concepts that describe a subject domain. Often, when a significant part of the subject logic is already implemented, the originally selected inheritance hierarchies stop working. If everything goes to that, do not be afraid to disassemble and reassemble these hierarchies 9 so that they better fit and work with each other.


Composition or inheritance: what to choose?


In a situation where both seem to be appropriate, take a look at the design in two planes:


  1. Structure and mechanical execution of business objects.
  2. What they mean by meaning and how they interact.

As long as inheritance remains inside one plane, everything is fine. But if the hierarchy goes through two planes at once, this is a bad symptom.


For example, you have one object inside another. The internal object implements a significant part of the external behavior. The external object has a bunch of proxy methods that stupidly forward parameters to the internal object and return the result from it. In this case, look, and not whether to inherit from the internal object, at least partially.


Of course, no instructions will replace the head on the shoulders. When you build an object model, it is generally helpful to think. But if you want specific rules, then please.


Inherit if:


  1. Both classes are from the same subject area.
  2. The heir is a valid subtype of ancestor in terms of LSP .
  3. Ancestor code is necessary or well suited for the heir
  4. Heir basically adds logic

Sometimes all these conditions are fulfilled simultaneously:



If this is not your case, then you will most likely not need to inherit often. But not because it is necessary to “prefer” the composition to inheritance, and not because it is “better”. Choose the one that suits best for your specific task.


I hope these rules will help you understand the difference between the two approaches.


Enjoy your coding!


Afterword


Special thanks to the staff of ThoughtWorks for their valuable input and comments: Pete Hogson , Tim Brown, Scott Robinson, Martin Fowler , Mindy Or, Sean Newham, Sam Gibson and Mahendra Kariya.




one

The first official OO language, SIMULA 67, appeared in 1967.


2

System and application programmers adopted C ++ in the mid-1980s, but before the PLO became generally accepted, another ten years passed.


3

I deliberately simplify, not talking about the pub / sub, delegates, and the like, so as not to inflate the article.


four

At the time of writing, Amazon is offering 24,777 OOP books.


five

A Google search for "object-oriented programming" gives 8 million results.


6

A Google search yields 37,600 results for the query "inheritance is evil."


7

The meaning (interface) and mechanics (execution) can be divided due to the complexity of the language. See an example from the D specification.


eight

Sadly, I note that Java Stack inherits from Vector .


9

Designing for reuse through inheritance is beyond the scope of the article. Just keep in mind that your design must meet the needs of those who use the base class, and those who need an heir.




The translator is grateful to the OOP chat in the Telegram , without which this text could not appear.

')

Source: https://habr.com/ru/post/325478/


All Articles