Herança e composição (relacionamento Is a vs Has-a) em Java

Herança e composição (relacionamento Is a vs Has-a) em Java

1. Visão geral

Inheritancee composição - junto com abstração, encapsulamento e polimorfismo - são os pilares deobject-oriented programming (OOP).

Neste tutorial, vamos cobrir os conceitos básicos de herança e composição, e vamos nos concentrar fortemente em detectar as diferenças entre os dois tipos de relacionamento.

2. Noções básicas de herança

A herança é um mecanismo poderoso, embora usado em demasia e mal usado.

Simplesmente coloque, com herança, uma classe base (também conhecida como tipo base) define o estado e o comportamento comum a um determinado tipo e permite que as subclasses (também conhecidas como subtipos) fornecem versões especializadas desse estado e comportamento.

Para ter uma ideia clara de como trabalhar com herança, vamos criar um exemplo ingênuo: uma classe basePerson que define os campos e métodos comuns para uma pessoa, enquanto as subclassesWaitresseActress fornecem implementações adicionais de métodos de baixa granularidade.

Aqui está a classePerson:

public class Person {
    private final String name;

    // other fields, standard constructors, getters
}

E estas são as subclasses:

public class Waitress extends Person {

    public String serveStarter(String starter) {
        return "Serving a " + starter;
    }

    // additional methods/constructors
}
public class Actress extends Person {

    public String readScript(String movie) {
        return "Reading the script of " + movie;
    }

    // additional methods/constructors
}

Além disso, vamos criar um teste de unidade para verificar se as instâncias das classesWaitresseActress também são instâncias dePerson, mostrando assim que a condição "é-a" é atendida no nível de tipo:

@Test
public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() {
    assertThat(new Waitress("Mary", "[email protected]", 22))
      .isInstanceOf(Person.class);
}

@Test
public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() {
    assertThat(new Actress("Susan", "[email protected]", 30))
      .isInstanceOf(Person.class);
}

It’s important to stress here the semantic facet of inheritance. Além de reutilizar a implementação dePerson class,we’ve created a well-defined “is-a” relationship entre o tipo de basePersone os subtiposWaitresseActress. Garçonetes e atrizes são, efetivamente, pessoas.

Isso pode nos levar a perguntar:in which use cases is inheritance the right approach to take?

Se os subtipos cumprem a condição "é um" e fornecem principalmente funcionalidade aditiva mais abaixo na hierarquia de classes, então a herança é o caminho a percorrer.

Obviamente, a substituição de método é permitida desde que os métodos substituídos preservem a substituibilidade de tipo / subtipo base promovida porLiskov Substitution Principle.

Além disso, devemos ter em mente quethe subtypes inherit the base type’s API, o que em alguns casos, pode ser exagero ou simplesmente indesejável.

Caso contrário, devemos usar a composição.

3. Herança em padrões de projeto

Embora o consenso seja de que devemos favorecer a composição sobre a herança sempre que possível, existem alguns casos de uso típicos em que a herança tem seu lugar.

3.1. O padrão de supertipo de camada

Nesse caso, nósuse inheritance to move common code to a base class (the supertype), on a per-layer basis.

Esta é uma implementação básica desse padrão na camada de domínio:

public class Entity {

    protected long id;

    // setters
}
public class User extends Entity {

    // additional fields and methods
}

Podemos aplicar a mesma abordagem às outras camadas do sistema, como as camadas de serviço e persistência.

3.2. O padrão de método de modelo

No padrão de método de template, podemosuse a base class to define the invariant parts of an algorithm, and then implement the variant parts in the subclasses:

public abstract class ComputerBuilder {

    public final Computer buildComputer() {
        addProcessor();
        addMemory();
    }

    public abstract void addProcessor();

    public abstract void addMemory();
}
public class StandardComputerBuilder extends ComputerBuilder {

    @Override
    public void addProcessor() {
        // method implementation
    }

    @Override
    public void addMemory() {
        // method implementation
    }
}

4. Noções básicas de composição

A composição é outro mecanismo fornecido pelo OOP para reutilizar a implementação.

Resumindo,composition allows us to model objects that are made up of other objects, definindo assim uma relação “tem-a” entre eles.

Além disso,the composition is the strongest form of association, o que significa quethe object(s) that compose or are contained by one object are destroyed too when that object is destroyed.

Para entender melhor como a composição funciona, vamos supor que precisamos trabalhar com objetos que representam computadores.

Um computador é composto de diferentes partes, incluindo o microprocessador, a memória, uma placa de som e assim por diante, para que possamos modelar o computador e cada uma de suas partes como classes individuais.

Veja como uma implementação simples da classeComputer pode parecer:

public class Computer {

    private Processor processor;
    private Memory memory;
    private SoundCard soundCard;

    // standard getters/setters/constructors

    public Optional getSoundCard() {
        return Optional.ofNullable(soundCard);
    }
}

As seguintes classes modelam um microprocessador, a memória e uma placa de som (as interfaces são omitidas por uma questão de brevidade):

public class StandardProcessor implements Processor {

    private String model;

    // standard getters/setters
}
public class StandardMemory implements Memory {

    private String brand;
    private String size;

    // standard constructors, getters, toString
}
public class StandardSoundCard implements SoundCard {

    private String brand;

    // standard constructors, getters, toString
}

É fácil entender as motivações por trás de empurrar a composição ao invés da herança. In every scenario where it’s possible to establish a semantically correct “has-a” relationship between a given class and others, the composition is the right choice to make.

No exemplo acima,Computer atende à condição “has-a” com as classes que modelam suas partes.

Também é importante notar que, neste caso,the containing Computer object has ownership of the contained objects if and only if the objects can’t be reused within another Computer object. Se eles pudessem, estaríamos usando agregação, em vez de composição, onde a propriedade não está implícita.

5. Composição sem Abstração

Como alternativa, poderíamos ter definido a relação de composição codificando as dependências da classeComputer, em vez de declará-las no construtor:

public class Computer {

    private StandardProcessor processor
      = new StandardProcessor("Intel I3");
    private StandardMemory memory
      = new StandardMemory("Kingston", "1TB");

    // additional fields / methods
}

Claro, este seria um projeto rígido e fortemente acoplado, já que estaríamos tornandoComputer fortemente dependente de implementações específicas deProcessoreMemory.

Não estaríamos aproveitando o nível de abstração fornecido pelas interfaces edependency injection.

Com o design inicial baseado em interfaces, obtemos um design pouco acoplado, que também é mais fácil de testar.

6. Conclusão

Neste artigo, aprendemos os fundamentos da herança e composição em Java e exploramos em profundidade as diferenças entre os dois tipos de relacionamentos ("é-a" vs. "tem um").

Como sempre, todos os exemplos de código mostrados neste tutorial estão disponíveisover on GitHub.