Well, let’s start from the academic definitions of the development pattern SOLID, and in particular of The Liskov Substitution Principle illustrated by mgibsonbr in reply which gave rise to that question.
Why use composition and not inheritance?
If I had to summarize in one line it would be "To separate behavior from hierarchy". Often we just want to group behavior and characteristics. The problem is that the same behavior can be observed in different hierarchies: Example Passáros
and Aviões
fly, in fact what we want to reuse between them is the behavior (Voador
), as well as common attributes relevant to this behavior. I always think of behaviors as interfaces (examples from Java: Runnable
, Closeable
, etc.).
Common behavior does not necessarily define an inheritance relationship, it is often better to abstract behavior in its own unity. For this some languages have mechanisms like Traits and Mixin. Java 8 has introduced Default Methods to facilitate re-use.
So why use inheritance?
Because often you really want to establish a class hierarchy, your relationship is naturally stronger, and the attributes and behaviors are specific to that chain. The various objects of a chain share "existential identity" (IS-A
), not only common behavior.
Note that in the example in question the problem is not with the Animal
and yes with the AbrigoAnimais
, in particular with the operation void adicionarAnimal()
which is an operation to modify the status of the shelter. Assuming the relationship between AbrigoAnimais
and AbrigoCachorros
be strong, we can solve the problem of overload
composition-free:
abstract class AbrigoAnimais<T extends Animal> {
abstract T obterAnimal();
void adicionarAnimal(T a) { }
}
class AbrigoCachorros extends AbrigoAnimais<Cachorro> {
Cachorro obterAnimal() { return new Cachorro(); }
@Override
void adicionarAnimal(Cachorro c) { } // É override
}
AbrigoAnimais<Cachorro> canil = new AbrigoCachorros();
canil.adicionarAnimal(new Cachorro());
// inválido
canil.adicionarAnimal(new Gato());
But the example of the Rectangle?
Again the problem is with a method to modify dimensions (atribuirLados
), in this case the post-conditions of the method are conflicting with a subclass invariant. Eliminating mutability eliminates this problem (Source: Wikipedia).
Guidelines
A question guide to direct use of interfaces / composition vs heritage.
In the universe of your problem:
- Does a certain class always "is" an instance of the superclass or can it only behave as such? In the first case favour inheritance, in the second composition.
- In the current hierarchy there are classes that naturally belong to other hierarchical chains with the same behavior and attribute group (see that exists is stronger than "may exist")? If yes favor composition.
- Is the behavior in question interchangeable? That is, you need certain instances of a class to have behavior
X
and other behaviour Y
? Do you need to dynamically change the behavior of an instance? If yes favor composition.
- Does the functioning of your class as a whole depend on attributes and methods of the relative? Or does this behavior only demonstrate a "facet" of your class? In the first case favor inheritance, in the second composition.
Completion: There are several occasions when the use of inheritance is perfectly acceptable, it is possible to group behaviors and attributes in ways that do not hurt the Liskov principle, or to refactor these objects to eliminate problems without introducing composition. When certain behavior and attribute set is unique to a string, there is no harm in keeping it in that string.
SOLID / Liskov and Overengering
Attention: Opinionated Text
There is an implicit question in the @utluiz question: "Hurting the Liskov principle is acceptable?".
I was once much more "purist", always had a cake recipe with Patterns design (always found occasions to apply patterns such as Visitor), created several Wrappers unnecessary to obey the Demeter’s law, carried my model of generic, Lower Bounds, upper Bounds, etc., because everything had to be correctly typed, generic and extensible. Then I experienced a phase of functional purity, immutability, monads, existential types and so on. Over time I realized that the languages introduced new constructors that redefined the use and applicability of most of the Patterns design that I preached; I also learned that it is perfectly acceptable (and desirable) mutability here and there, and today I have no reservations about a cast
explicit and or a typeof
well encapsulated if it saves me several lines of "stunts" with the type system.
Obeying the Liskov principle (as well as the other SOLID principles) is certainly beneficial. In particular the Liskov principle eliminates a whole set of possible defects in which the API is used or extended in ways that were not thought out by the developer. ensure that subtypes do not have stronger pre-conditions than those of the inherited type, ensure that subtypes do not have weaker post-conditions than those of their supertypes, preserve invariants and the history of modifications, these are all desirable features in a robust system. Since it is reasonably simple to break these rules we can reach extremes, one of them is the line of thought "always use composition to avoid having to refactor the code in the future".
That said, in practice we need to weigh the complexity factor of the API. It’s important to think about who’s going to maintain and who’s going to consume the code. Before refactoring a simple code into another more complex code to satisfy a certain guideline it is always good to think:
- What is best for those who will maintain this system?
- What are the chances the maintainer has issues with the code as it is vs after the Refactoring?
We often make the system much more complicated than it needs to be by assuming that a particular API "might" be misused or extended in the wrong way... Or we still do it to satisfy corncases that just don’t happen in practice.
Today I prefer simplicity to purity. Sometimes a comment is a much better solution than Refactoring :D.
There are situations where inheritance fits better than composition and vice versa. My choice is always by the most natural model to represent a certain situation. If inheritance hurts Liskov’s principle but is still the most natural abstraction technique, producing "sensible" behavior (e.g., For the class Quadrado
the method atribuirLados
could throw an exception when the height is different from the width, this would be "sensible" behavior that hurts the Liskov principle) I see no reason to introduce more complexity in the API. On the other hand if I find myself "fighting" with modeling on several occasions (e.g, need for mutable states and problems of the type Circle-Ellipse in programming languages without adequate mechanisms to represent the model) it is time to rethink the modeling and stop fighting with the code.
Of course, a programmer does not learn to differentiate between "rule exceptions" that cause problems alone. The best way to do this, in addition to writing a lot of code, is to study complex Apis and realize in which situations the misuse of inheritance caused problems and was refactored over time. Toolkits Apis, Frameworks, collections, unit testing libraries, etc are all excellent candidates for study (class, interfaces and methods deprecated are your friends). Over time it becomes clear what can be used in each situation and the trade-offs of each model.
Using inheritance indiscriminately is a thing of the 1990s :) Cocoa (from OSX and iOS) is considered one of the best frameworks and uses very little inheritance, the hierarchy is quite "boring"
– epx
When using interface I am not reusing code but a model that should be followed, if not using inheritance to reuse code, what would be the other goal for an inheritance? (it is not a question to criticize but to complement, I think it lacked this explanation in your text, what you think about what would be ideal)
– Filipe Moraes
@Filipe It may not have been clear, but I argue that the correct thing is to use inheritance to extend a type and then replace it in the existing code. Anyway, I’m agreeing with the answer I quoted and disagreeing with the way OO is taught. Anyway, I edited the question to clarify this. At least I hope I clarified.
– utluiz
I find the question very pertinent, I myself have already created examples of Dog and Cat inheriting from Animal here at SOPT (am I guilty then?), but I went completely on impulse because as quoted in the question here ALL examples do this! (I prefer to say all than overwhelming majority, because 80% for me is already "All"). I don’t know where you can continue this matter, but I think it is essential that it occurs. Maybe later I will re-read and try to give a helping hand.
– Math
@utluiz, to be quite honest, I created a more pragmatic view of the Liskov substitution principle. Modern languages like Scala introduced middle-term notions like
traits
andmixing
(composition with an inheritance face). But honestly? We are entering a typical level of academicism of Haskell forums (with all due respect to staff). There comes a time when we have to sacrifice purity for practicality. This kind of decision is visceral, not technical. Overengering is a much more serious problem in practice than misrepresentation of academic principles.– Anthony Accioly
@Very interesting anthonyaccioly. Certainly your comment added quite a lot. Really the
traits
andmixins
are a great practical solution for code reuse without the high coupling that heritage brings. I also agree that overengineering It’s a problem, but my goal here is not just to spend a lot of time on a detail, but to achieve a certain purity of thought to actually be able to make decisions more safely and quickly. One of the reasons, in my opinion, why people spend too much time deciding something is because they are filled with doubts.– utluiz
Has a long dissension, Prefer composition to inheritance
– rray
Contributing my 2 cents, only emphasizing the following line of the author: [...]he is a unique type of animal that can hardly be replaced by another without impact.. Here you got a little mixed up in what the LSP says. The idea is not that you can replace a
Gato
by aCachorro
, but that in everywhere there is aAnimal
it is possible to replace it with aGato
. There is no guarantee of any relationship between classes derived from the same third class.– Henrique Barcelos
@Henriquebarcelos My argument is not that it should be possible to use a
Cachorro
in place of aGato
. The problem is that inheritance is not suitable for this example because, although both are animals, they have four legs, two eyes and so on, they rarely have behaviors in common. What good is a cat that can’t catch rats or a dog that can’t bury bones? My argument is that using inheritance is not appropriate because each has unique characteristics. If I wanted a generic animal, I wouldn’t need inheritance, just attributes likevoz
,patas
,peso
,cor
...– utluiz
@utluiz, yes, I agree, I just made it clear that the LSP does not concern that which you have set as an example =]
– Henrique Barcelos
@utluiz is better now, yes. And a comment on the "controversy" of
Animal
: I see no problem in a subclass being radically different from the superclass, provided that - with regard to common and aggregate use - it is compatible with the base type. If every animal hascomer
,dormir
,fazerNecessidades
andetc
, butGato
implementsetc
how to "catch mice" andCachorro
implementsetc
like "bury bones", you can still have a collection ofAnimal
mixing dogs and cats and calling their methods polymorphically. The main function ofetc
would then be its side effects.– mgibsonbr
This question is in meta discussion.
– bfavaretto