3

To start off with an example: I have a read-only Repository used for getting arbitrary values. This behavior can be implemented multiple ways.

I also want to allow opt-in mutation of the repository's values through a MutableRepository, which implements Repository because it satisfies Liskov substitution principle (any repository that supports writing should support reading). MutableRepository also has multiple implementations.

At the same time, I do not want to couple the implementation of writing to the implementation of reading. Given:

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

Declaring MutableRepository as:

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

forces any implementor of MutableRepository to handle implementation of getValue. Whereas, if I do this:

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

I allow implementations of MutableRepository to only handle implementing setValue. Given three ways of writing to a repository and three ways of reading to a repository that can be mixed and matched:

  • The former way of declaring MutableRepository forces 3 * 3 = 9 different implementations of MutableRepository / Repository since an implementation of setValue is coupled to getValue.

  • The latter way of declaring MutableRepository forces only 3 + 3 = 6 different implementations of MutableRepository / Repository, since an implementation of getValue can be injected into that of setValue.

Composing MutableRepository from Repository while inheriting from Repository at the same time seems the only way to support LSP while decoupling implementation of the reading from writing. But I've always seen composition and inheritance presented as alternatives (and if they are used together, not over the same type), instead of being combined like this.

Is there a different approach I should take here?

1
  • Think about how you'd write a subclass of MutableRepository now. You need two classes!
    – user253751
    Oct 12, 2020 at 17:04

2 Answers 2

2

Using inheritance and composition like that is perfectly fine and is also done in well known pattern such as the decorator (looks like this is what you are doing here)

1

Keep your two interfaces, but create an abstract class for each:

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

It might seem a little odd to have BaseMutableRepository inherit from BaseRepository (which implements Repository) and also implement the MutableRepository interface (which also implements Repository), but that's the nice thing about how interfaces are designed in Java. Since interfaces do not contain implementation then there is no ambiguity that arises from two different super types inheriting from a common super-super type, which is what happens in the Diamond Inheritance Problem.

The main point is to keep the behavior non-ambiguous, which is accomplished using the two abstract classes, BaseRepository and BaseMutableRepository.

You trade more complexity in the class hierarchy for ease of implementation in concrete classes to facilitate unit testing and dependency injection via the interfaces.

1
  • Isn't OP then getting the problem he is trying to avoid with NxM implementations for the different combinations of read- and write strategies? The point was to inject the read-only repository into the mutable repository?
    – sfiss
    Oct 12, 2020 at 15:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.