Composer et hériter du même type

Oct 12 2020

Pour commencer avec un exemple: j'ai une lecture seule Repositoryutilisée pour obtenir des valeurs arbitraires. Ce comportement peut être implémenté de plusieurs manières.

Je souhaite également autoriser la mutation opt-in des valeurs du référentiel via a MutableRepository, qui implémente Repositorycar il satisfait au principe de substitution de Liskov (tout référentiel prenant en charge l'écriture devrait prendre en charge la lecture). MutableRepositorya également plusieurs implémentations.

En même temps, je ne veux pas coupler l'implémentation de l'écriture à l'implémentation de la lecture. Donné:

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

Déclarant MutableRepositorycomme:

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

force tout réalisateur de MutableRepositoryà gérer l'implémentation de getValue. Alors que si je fais ça:

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);
}

J'autorise les implémentations de MutableRepositoryà gérer uniquement l'implémentation setValue. Compte tenu de trois façons d'écrire dans un référentiel et de trois façons de lire dans un référentiel qui peuvent être mélangées et mises en correspondance:

  • L'ancienne façon de déclarer MutableRepositoryforce 3 * 3 = 9 implémentations différentes de MutableRepository/ Repositorydepuis qu'une implémentation de setValueest couplée à getValue.

  • Cette dernière façon de déclarer MutableRepositoryforce seulement 3 + 3 = 6 implémentations différentes de MutableRepository/ Repository, puisqu'une implémentation de getValuepeut être injectée dans celle de setValue.

Composer à MutableRepositorypartir de Repositorytout en héritant de Repositoryen même temps semble être le seul moyen de supporter LSP tout en découplant l'implémentation de la lecture de l'écriture. Mais j'ai toujours vu la composition et l'héritage présentés comme des alternatives (et s'ils sont utilisés ensemble, pas sur le même type), au lieu d'être combinés comme ça.

Y a-t-il une approche différente que je devrais adopter ici?

Réponses

2 sfiss Oct 12 2020 at 14:36

Utiliser l'héritage et la composition comme ça est parfaitement bien et est également fait dans un modèle bien connu tel que le décorateur (on dirait que c'est ce que vous faites ici)

1 GregBurghardt Oct 12 2020 at 20:30

Gardez vos deux interfaces, mais créez une classe abstraite pour chacune:

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
    }
}

Il peut sembler un peu étrange que BaseMutableRepository hérite de BaseRepository (qui implémente Repository) et implémente également l'interface MutableRepository (qui implémente également Repository), mais c'est la bonne chose à propos de la façon dont les interfaces sont conçues en Java. Étant donné que les interfaces ne contiennent pas d'implémentation, il n'y a pas d'ambiguïté qui découle de deux super types différents héritant d'un super-super type commun, ce qui se passe dans le problème d'héritage de diamant .

Le point principal est de garder le comportement non ambigu, ce qui est accompli en utilisant les deux classes abstraites, BaseRepository et BaseMutableRepository.

Vous échangez plus de complexité dans la hiérarchie des classes pour faciliter la mise en œuvre dans des classes concrètes afin de faciliter les tests unitaires et l'injection de dépendances via les interfaces.