Compor e herdar do mesmo tipo

Oct 12 2020

Para começar com um exemplo: Eu tenho um somente leitura Repositoryusado para obter valores arbitrários. Esse comportamento pode ser implementado de várias maneiras.

Também quero permitir a mutação opcional dos valores do repositório por meio de um MutableRepository, que implementa Repositoryporque satisfaz o princípio de substituição de Liskov (qualquer repositório que ofereça suporte à escrita deve oferecer suporte à leitura). MutableRepositorytambém tem várias implementações.

Ao mesmo tempo, não desejo acoplar a implementação da escrita à implementação da leitura. Dado:

interface Repository<T> {
    T getValue(String valueID);
}

Declarando MutableRepositorycomo:

interface MutableRepository<T> extends Repository<T> {
    void setValue(String valueID, T value);
}

força qualquer implementador de MutableRepositorya lidar com a implementação de getValue. Considerando que, se eu fizer isso:

abstract class MutableRepository<T> implements Repository<T> {
    private final Repository<T> baseRepository;

    MutableRepository(Repository<T> baseRepository) {
        this.baseRepository = baseRepository;
    }

    @Override
    public T getValue(String valueID) {
        return baseRepository.getValue(valueID);
    }

    abstract void setValue(String valueID, T value);
}

Eu permito implementações de MutableRepositorypara tratar apenas da implementação setValue. Dadas três maneiras de gravar em um repositório e três maneiras de ler em um repositório que podem ser misturadas e combinadas:

  • A primeira forma de declarar MutableRepositoryforça 3 * 3 = 9 implementações diferentes de MutableRepository/, Repositoryuma vez que uma implementação de setValueé acoplada a getValue.

  • A última forma de declarar MutableRepositoryforça apenas 3 + 3 = 6 implementações diferentes de MutableRepository/ Repository, uma vez que uma implementação de getValuepode ser injetada na de setValue.

Compor MutableRepositoryde Repositoryenquanto herda de Repositoryao mesmo tempo parece a única maneira de oferecer suporte ao LSP enquanto desassocia a implementação da leitura da escrita. Mas sempre vi composição e herança apresentadas como alternativas (e se forem usadas juntas, não sobre o mesmo tipo), em vez de serem combinadas dessa forma.

Existe uma abordagem diferente que devo adotar aqui?

Respostas

2 sfiss Oct 12 2020 at 14:36

Usar herança e composição dessa forma está perfeitamente bem e também é feito em um padrão bem conhecido, como o decorador (parece que é isso que você está fazendo aqui)

1 GregBurghardt Oct 12 2020 at 20:30

Mantenha suas duas interfaces, mas crie uma classe abstrata para cada uma:

interface Repository<T> {
    T getValue(String id);
}

interface MutableRepository<T> implements Repository<T> {
    void setValue(String id, T value);
}

abstract class BaseRepository<T> implements Repository<T> {
    public T getValue(String id) {
        // common code for getting the value
    }
}

abstract class BaseMutableRepository<T> extends BaseRepository<T> implements MutableRepository<T> {
    public void setValue(String id, T value) {
        // common code for setting the value
    }
}

Pode parecer um pouco estranho ter BaseMutableRepository herdado de BaseRepository (que implementa Repository) e também implementar a interface MutableRepository (que também implementa Repository), mas essa é a coisa boa sobre como as interfaces são projetadas em Java. Como as interfaces não contêm implementação, não há ambigüidade que surja de dois supertipos diferentes herdados de um supertipo comum, que é o que acontece no Problema de Herança de Diamante .

O ponto principal é manter o comportamento não ambíguo, o que é realizado usando as duas classes abstratas, BaseRepository e BaseMutableRepository.

Você negocia mais complexidade na hierarquia de classes para facilitar a implementação em classes concretas para facilitar o teste de unidade e injeção de dependência por meio das interfaces.