Is it right to prefer composition to inheritance?

Asked

Viewed 2,309 times

66

I’ve read several articles and books of Designer Patterns the expression Prefira composição em vez de herança. I believe it is a rather controversial subject due to the views discussed.

In my view, composition and inheritance are used to solve different problems, right?

class Carro {  
    private Motor motor;      
}

The above example is very coherent as a car must have an engine. However if we replace the inheritance by composition in some cases the structure gets a little strange, for example.

class Carro {
    private Veiculo veiculo;
}

It is strange to think that Vehicle will come a Car attribute. In this context, Vehicle serves only for code reuse because it does not follow a hierarchy and does not add polymorphism.

Excuse my ignorance but the inherited classes keep the coupling that is necessary between them (the inheritance itself), certain?

What are the advantages of using composition instead of inheritance? Is it possible to specify the right time to use them? Following the opinion of the title, the best combination would be composition + interface?

3 answers

64


TL;DR - Only use inheritance if the subtype can perfectly replace the base type. In addition, reduce the responsibilities of its classes. Use composition to join several isolated responsibilities into one object (i.e. add functionality).

The rest of the answer is based on practical examples. For a more conceptual (and shorter) answer, including theoretical background, see the answer of utluiz. And the response of the carlosrafaelgn presents a rather interesting counterpoint.

When not to use inheritance?

The decision between using or not inheriting should be based on Principle of Liskov’s Substitution, which says: "where a base class object can be used a subclass object must also be able to be used". Maintaining this principle becomes complicated in complex inheritance relationships, as in the example below:

class Animal { }
class Cachorro extends Animal { }
class Gato extends Animal { }

class AbrigoAnimais {
    Animal obterAnimal() { }
    void adicionarAnimal(Animal a) { }
}
class AbrigoCachorros extends AbrigoAnimais {
    Cachorro obterAnimal() { } // OK
    void adicionarAnimal(Cachorro c) { } // É overload, não overrride
}

AbrigoAnimais canil = new AbrigoCachorros();
canil.adicionarAnimal(new Gato()); // Deveria ser válido, pelo princípio da substituição

The same is observed in cases where the subclass is more restricted than the superclass:

class Retangulo {
    void atribuirLados(int altura, int largura) { }
}
class Quadrado extends Retangulo { }

new Quadrado().atribuirLados(10, 20); // Deveria ser válido, pelo princípio da substituição

In addition - not related to this principle, but also relevant - there is the problem of what to do in the presence of multiple inheritance:

interface Voador { }

class Veiculo { }

class Aviao extends Veiculo implements Voador { }
class Passaro extends Animal implements Voador { }

// Como reaproveitar a funcionalidade de Voador?

Or in cases where the interface is conflicting:

interface Foo {
    String metodo();
}
interface Bar {
    int metodo();
}

class Baz implements Foo, Bar {
    // Não é possivel, pois o que "metodo" vai retornar?
}

If your model doesn’t have any of these problems, then go ahead and use inheritance! But most of the time (in my experience) at least one of these problems is present. In addition, the programmer’s motivation is often simply to "reuse code" - he doesn’t have a clear taxonomy where the inheritance relationship is justified, he only uses it because he thinks he has to use it. It is in these cases that the composition becomes a preferable alternative.

How to solve using composition?

In the first case, this can be done by placing in the base type only the methods that meet the substitution principle:

interface AbrigoAnimais {
    Animal obterAnimal();
}
class ImplAbrigoAnimais<T extends Animal> implements AbrigoAnimais {
    T obterAnimal() { } // OK, tipo de retorno covariante
    void adicionarAnimal(T animal) { } // Não está presente na interface
}
class AbrigoCachorros implements AbrigoAnimais {
    ImplAbrigoAnimais<Cachorro> abrigo = new ImplAbrigoAnimais<Cachorro>();

    Cachorro obterAnimal() { return abrigo.obterAnimal() }
    void adicionarAnimal(Cachorro cachorro) { abrigo.adicionarAnimal(cachorro); }
}

AbrigoAnimais canil = new AbrigoCachorros();
canil.obterAnimal(); // OK

Same thing in the second (note that it is possible to support both reading and writing):

interface Retangular {
    void atribuirLargura(int largura);
    void atribuirAltura(int altura);
}
class Retangulo implements Retangular {
    void atribuirLargura(int largura) { }
    void atribuirAltura(int altura) { }
    void atribuirLados(int largura, int altura) { } // Não está na interface
}
class Quadrado implements Retangular {
    Retangulo quadrado = new Retangulo();
    void atribuirLargura(int largura) { 
        atribuirLados(largura);
    }
    void atribuirAltura(int altura) { 
        atribuirLados(altura);
    }
    void atribuirLados(int valor) {  // Não está na interface
        quadrado.atribuirLargura(valor);
        quadrado.atribuirAltura(valor);
    }
}

Separation of Responsibilities

Finally, I will demonstrate how the separation of responsibilities can greatly help to model a complex set of entities:

interface ObjetoPosicionado { }
interface Animal extends ObjetoPosicionado { }
interface Veiculo extends ObjetoPosicionado { }

class ImplObjetoPosicionado implements ObjetoPosicionado {
    int x;
    int y;
    int z;
}
class ImplAnimal implements Animal {
    ObjetoPosicionado impl;
    int fome;
}
class ImplVeiculo implements Veiculo {
    ObjetoPosicionado impl;
    List<Pessoa> passageiros;
}

Here each class takes care of a specific aspect: one controls where the object is in space, another controls the specific attributes of the animal making use of of the previous object (as a Animal is a ObjetoPosicionado), and another controls the specific vehicle attributes (idem). With single responsibilities, one can reuse them in a particular class by taking only what is needed, and not wasting space:

class Cavalo implements Animal, Veiculo {
    ImplAnimal ia;
    ImplVeiculo iv;

    public Cavalo() {
        ObjetoPosicionado op = new ObjetoPosicionado(); // Uma única cópia...
        ia = new ImplAnimal(op);  // ...é utilizada aqui...
        iv = new ImplVeiculo(op); // ...e aqui.
    }

    // Realiza a interface de ObjetoPosicionado
    void deslocar(int x, int y, int z) {
        ia.deslocar(x,y,z); // Ou iv - dá no mesmo, pois ambos têm a mesma "impl"
    }

    // Realiza a interface de Animal
    void comer() {
        ia.comer();
    }

    // Realiza a interface de Veiculo
    void adicionarPassageiro(Pessoa p) {
        iv.adicionarPassageiro(p);
    }
}

But it’s not much code to write?

Yes! If language syntax doesn’t help, you’d have to do a lot of gymnastics to create models of this type, walk that effort "pay" later (a clean API and efficient code). And what would be the solution? Leave the "purity" aside and end the inheritance at once:

class Cavalo {
    Animal comoAnimal() { return ia; }
    Veiculo comoVeiculo() { return iv; }
}

Cavalo c = new Cavalo();

//Animal a = c;
Animal a = c.comoAnimal();

That is: back to the "strangeness" of the original question, but gain all the benefits of the use of composition except the overhead of the extra code to make this possible. At the end of the day, it’s still heritage that you’re doing - but without using its syntax/semantics, but rather that of composition.

Is the ideal?

No. The ideal would be for the computer to take good care of the "confusion" that is the way the human brain categorizes things ("Birds fly. Ostriches are birds, only they don’t fly. Planes also fly, but they don’t flap their wings. Rockets fly, and they don’t even have wings! Ducks are birds, and they also swim. Boats don’t swim, but that’s about it... Planes, rockets and boats are vehicles. Missiles look like rockets, but they don’t carry passengers.").

In the absence of this, all that remains is to create models with desirable characteristics, such as type safety. It aims to ensure that "similar" entities can be treated as one type - by substituting according to the principle quoted at the beginning of the answer. If you don’t have this security, what good is having an object that may or may not fulfil the class contract?

In the end, what interests the programmer is that a given object has certain desirable functionality. That is, conceptually um carro é um veículo, but pragmatically um objeto carro possui a funcionalidade de um objeto veículo.

  • 3

    Note: my answer was very long, but I can not explain it using fewer words... If anyone has a more succinct answer - and perhaps even more complete, because what I wrote was only a result of my personal experience - they will be very welcome!

  • very explanatory answer, now it makes more sense with the real examples of misuse of inheritance, in relation to "strangeness" of using composition in place of inheritance I think it is worth for all the benefits I get and with the advantage of using "Generics" as in your example gets better yet, very good explanation! Tomorrow I’ll give you another 50pts for the answer.

35

I know I can enter the sphere of opinion, but still, very curious the placement

prefer composition rather than inheritance

During my master’s classes, I heard the exact opposite. Including, among all the types of heritage that exist, those who respect the principle of substitution were "green" and those who do not respect, were "red".

In a nutshell, the principle of substitution was passed on to me as follows:

  • Be the classes A and B, B subclass of A

  • It is possible to replace an instance iA class A by one instance iB class B in any situation, with no observable effect

From then on, the concept of subclass and subtype was defined:

  • A subclass is a structure built using inheritance, satisfying or not the substitution principle

  • A subtype is a class that satisfies the substitution principle

Therefore, not all subclasses are subtypes, and (in some very specific languages) it is possible to have a subtype other than a subclass.

With this defined, it is possible to separate which forms of inheritance may or may not respect the substitution principle.

Some that may respect the principle:

  • Specialization
  • Specification
  • Extension
  • Combination (or Multiple Inheritance)

While some others do not always respect (and should be avoided if possible):

  • Construction: A superclass is only used for its behavior, and has no real relationship with the superclass. For example, creating a class Pilha, which is a subclass of Vetor, just because some of the necessary methods are already there ready in class Vetor.
  • Generalization: A subclass extends the behavior of the superclass by creating a more general type of object, generalizing or extending the superclass by providing more functionality.
  • Limitation: The subclass limits some of the superclass’s behavior by overriding undesirable methods (for example, causing a superclass functional method to generate an exception such as InvalidOperationException).
  • Variation: Two or more classes appear to be related, but it is unclear who should be superclass and who should be subclass. It usually occurs when constructing classes from existing classes. For example, between classes Mouse, TouchPad and Joystick, it becomes difficult, or even impossible, to determine who should superclass whom in theory. But in practice, the developer simply decides that Mouse is a particular case of Joystick, and ready!

With that said, it is possible to effectively go to the idea of composition.

Clearly, in the example of "Inheritance by Construction", where Pilha is a subclass of Vetor, use of the sentence Stack is-a Vector sounds very bad. In that case, the composition should have been used, since the sentence Stack has-a Vector sounds much better (whereas the listening person knows what a stack is and what a vector is).

On the other hand, when using composition in cases where inheritance could be used, the developer loses several possibilities. For example, if a method m1() expects a class A, and you created a class B (which, although logically be-a class A and if you behave as such, it was created by composition and not by inheritance), you cannot pass an instance iB for m1(), and will need to create another method m2() to work with class instances B. That is, you missed an opportunity to reuse code, and ended up "duplicating" code.

Code reuse and behavior reuse bring several benefits and security:

  • Leverages the creation of new software components from existing software components
  • Productivity (less time spent developing new components)
  • Consistency (a certain behavior is equal/consistent throughout the system)
  • Reliability (reusing an already tested and validated code provides extra security for the developer)

Both composition and inheritance reuse code and/or behavior, and both have their specific use scenarios. After everything I’ve been through throughout my master’s degree and my professional life, I wouldn’t say that one is better than the other, because each one’s use scenarios are different.

A "thumb rule" for this case: if the sentence A is-a B "sounds good", so you should utilize inheritance, to take advantage of everything it offers. Otherwise, both if A is-a B not "sounds good", but wants to reuse the behavior of B in A, as if A has-a B "sounds good, "so you should use composition.

  • 8

    I see nothing of opinion here. I see a different view with facts and arguments. But I understand your concern, there are people who have a lot of opinion (using the same criteria that they adopt) about what is opinion :)

  • 4

    A kind of "meta opinion" :)

26

Less prolific response ;)

Many misuse inheritance to repurpose or organize code.

Inheritance

Extend a class only when it is necessary to replace the original class with the subclass in order to extend the original functionalities.

This involves the Principle of Liskov’s Substitution and the Open/Closed Principle of the concept of design SOLID.

Basically, the Principle of Substitution says that a subclass must replace the superclass without changing the existing code.

The Closed/Open Principle says that a class must be closed to change and open to extension, which means that it must allow new functionality without changing the existing code.

Composition and delegation

Use composition and delegation to properly distribute responsibilities between classes. A class can reference other classes and then delegate the execution of methods that are not its responsibility to them.

This has to do with Principle of Single Liability.

Polymorphism

To allow polymorphism, use interfaces and apply the Principle of Segregation of Interfaces. This means that no class should be forced to rely on methods it does not use.

The interfaces should not simply contain the methods that we find intuitive at a given time, because this concept is different in each context (as well explained in Marcelo’s answer). Rather, an interface should contain only the interesting methods for those who use it. If it is used in two different situations, divide it into two.

Browser other questions tagged

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