Is it wrong to use class inheritance to group common behaviors and attributes?

Asked

Viewed 4,053 times

71

What they teach us about Inheritance

The overwhelming majority* of materials dealing with Class Inheritance exemplify it as a mere mechanism for grouping common attributes or actions. At most, as a means of representing a hierarchy analogous to something we observe in real life. As a sample**, see this booklet from Unicamp and this other one from Caelum.

The same applies to the classes I attended on the subject. I still remember when I learned that "object orientation makes it possible to represent what we see in real life". Inheritance is commonly defined as a "is one" relationship. Classic examples include Cachorro and Gato extend Animal illustrate this.

The problem is that, when we learn this way, our concern is to classify the "real-life objects" to group in hierarchies and then look for common attributes and actions to put in the superclass.

I can say that I spent a lot of time using inheritance as a way to group similar things together and avoid code repetition, not considering important points like coupling and cohesion.

Inheritance in practice

However, it seems that in practice this simply does not work as well as in theory, as exemplified in the question "Prefer composition to inheritance". To excellent response accepted says:

Only use inheritance if the subtype can perfectly replace the base type.

This statement, which I consider correct, leads us to the logical consequence that the example of the animals does not adequately represent the way we should face the inheritance. Gato is a Animal, but each Animal displays a very distinct behavior from others. Gato is not exactly a specialization of Animal, namely, a Animal that has something else, he’s a unique kind of Animal which can hardly be replaced by another without impact.

As explained in the aforementioned answer, using inheritance simply to display common behavior or reuse code is generally not an appropriate solution. For this there are interfaces and delegation.

Completion

Anyway, my question is: Are there situations where it would be appropriate to simply use inheritance to group objects with common behaviors and attributes? Or to create hierarchies of objects that represent "real life"? (if possible, give an example)

Bonus question: is it worth starting using other examples for inheritance and not letting people discover for themselves that "in practice it is not quite like that"?


Updating: consider "appropriate" or "right" the use of inheritance that benefits the long-term system, ie avoid problems of maintenance and product evolution. For example, using inheritance for a hierarchy can cause a major problem if the children in that hierarchy have different characteristics from the parents and the polymorphic code starts to "break" or get multiple ifs to deal with specific cases.


* By "overwhelming majority" I mean almost every book, workbook and class I know.

** Note that I am not saying that they are totally wrong, but that they convey an aspect in a wrong way.

Note: I understand that many may consider this issue based on opinions. If you find that it is not suitable for the format of the site, please comment that I will adjust or delete as appropriate.

  • 2

    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"

  • 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 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.

  • 1

    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.

  • 3

    @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 and mixing (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.

  • 1

    @Very interesting anthonyaccioly. Certainly your comment added quite a lot. Really the traits and mixins 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.

  • Has a long dissension, Prefer composition to inheritance

  • 1

    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 a Cachorro, but that in everywhere there is a Animal it is possible to replace it with a Gato. There is no guarantee of any relationship between classes derived from the same third class.

  • @Henriquebarcelos My argument is not that it should be possible to use a Cachorro in place of a Gato. 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 like voz, patas, peso, cor...

  • 1

    @utluiz, yes, I agree, I just made it clear that the LSP does not concern that which you have set as an example =]

  • 2

    @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 has comer, dormir, fazerNecessidades and etc, but Gato implements etc how to "catch mice" and Cachorro implements etc like "bury bones", you can still have a collection of Animal mixing dogs and cats and calling their methods polymorphically. The main function of etc would then be its side effects.

  • 3

    This question is in meta discussion.

Show 7 more comments

2 answers

47


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.

But and the example of Animal?

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.

  • 7

    "I prefer simplicity to purity." I said it all. Simple is easier to understand and extend, change and remodel, use and abuse. However, simplicity is different from being simplistic. I notice that beginner programmers cannot write simple code. It seems that only after study, of reinventing the wheel several times, of breaking the face so many is that finally the plug falls and begin to appear beautiful and simple solutions. Actually that’s what I meant in my comment on "mental purity". Well, anyway, it was a great response.

  • 2

    @utluiz thanks :). I updated (the already "prolixa") response with a little more content on decisions of design. I also understood your position on "mental purity"! I only stressed the dogmatic characteristic due to my own evolution as a programmer and knowing that this enthusiastic Stack Overflow audience with stratospheric IQ can fall into the same traps. I think our area needs a continuous stream of "Uncle Bob" s challenging, re-capitulating, reinterpreting, refining and reformulating best practices.

  • 1

    Excellent answer! I would just like to give a clarification (since my answer in the other question is being so cited): my example of AbrigoAnimais does not use generics on purpose, just to be a [bad] example of inheritance and not a "non-example". Just as List<Object> x = new ArrayList<String>() is invalid (even if Object x = new String() is valid) when parameterizing AbrigoAnimais you solved the problem in question but also created another: how to create a generalized method to deal with AbrigoAnimais? Now, just using wildcards...

  • 1

    Thank you @mgisonbr. Sorry for appropriating your (excellent) reply. You did a great job covering classic examples of problems with variance and covariance (did not want to enter this merit not to leave the answer even bigger). And yes, I agree with you, in languages that support type variance adicionarAnimal would be a typical case of covariance in countervariant position. In Scala we would have to relax the type to conform with Liskov (killing the restriction).

  • 1

    In Eiffel we could keep the most restricted parameter in the subtype at the expense of trusting that no one will use one AbrigoAnimais to put a cat in a kennel. In Common Lisp we could additionally dynamically update the type of the object canil for AbrigoAnimais in the presence of a cat. That is, in different languages we deal with different technical constraints and we end up adopting different standards of solution as well illustrated in your response.

21

Answering your main question:

there are situations where it would be appropriate to simply use inheritance to group objects with common behaviors and attributes?

  • Rather, a comment: "having common behavior and attributes" means nothing unless you implement a common interface. If two objects/classes have 99% of the fields in common, but are never processed by the same subroutine, it makes no sense to group them in any way. The rest of the answer assumes that not only these objects have common characteristics but that this common part performs the same interface/fulfills the same contract.

To reply from Anthony Accioly It already answers very well, but I would like to stress one important point: not always the right tool for a given task is readily available, and we have to manage with what we have. If your language does not support traits or mixings (nor dynamic types, nor Duck Typing, nor algebraic data types...), and inheritance is the most direct way to reuse a common code, so I would say it is rather appropriate to do so.

Quoting my answer to the related question, one of the big problems of reusing code via composition under a statically typed language is that there is no easy way to delegate the realization of an interface to another object: or you already have an object that implements that interface, or you have to create one that does - creating each method and "redirecting" it to the appropriate object.

Python is dynamically typed, but let us assume for a moment that he is not. There’s a class (I can’t remember which, and I can’t find it in a search) meant to help create comparators. Basically, it gives a standard implementation to comparators =, <, <=, >, >= and != so that the programmer only needs to implement two of them - and the library does the rest (ex.: a == b sse not(a < b or a > b), a > b sse not(a == b or a < b) and a < b sse not(a == b or a > b)). How could this be done without using inheritance?

  1. Forcing the programmer to define all other methods, only redirecting them to the utility object (too much code to write, right?); or:
  2. Creating an interface for "implements bigger and smaller", another for "implements bigger and equal", another for "implements smaller-equal and different", etc...

Each comparator will be totally different from each other (or it would not be necessary to create it), all they have is a common behavior (deduce the missing methods from the existing ones). It’s perfectly possible not use heritage and implement the interface directly, but lose all convenience. It is an unusual case, but it is still a case...

Or to create hierarchies of objects that represent "real life"?

The biggest mistake I see when someone tries to model "real life" is to think that just because two things are different they should be modeled differently. For example, if the classes Cachorro and Gato have no behaviour other than class Animal - only distinct parameters - then they should not even exist! Each dog and each cat should be an object of the class Animal with a parameter tipo = "cachorro" (which also facilitates the inclusion/removal/modification of other animals without changing the code).

Nobody models real life. What we model are the aspects of real life who are relevant for our systems.

(There are situations where - for reasons of memory economy - it is good to move everything that is common in a "real-life class" to a distinct class or object that, by coincidence, ends up correlating to real-life taxonomy; but this is already the subject of an entire post...)

  • 2

    +1 Highlight: What we model are the real-life aspects that are relevant to our systems.

  • @utluiz If we go further, we do not even model in a class the aspects of the real object that are relevant to our system; we model a class with only some aspects of the real object, among all aspects that are relevant in the system. These some aspects are selected together because they solve an area of the problem, and other aspects of the real object will be modeled in another class that will solve another area of the problem. Thus, a product in a store would have a class representing it in stock and another class representing it in e-commerce (DDD-Bounded Context).

Browser other questions tagged

You are not signed in. Login or sign up in order to post.