Чистая архитектура и недействительность кеша

У меня есть приложение, которое пытается следовать чистой архитектуре, и мне нужно сделать некоторую недействительность кеша, но я не знаю, на каком уровне это должно быть сделано.

Для этого примера предположим, что у меня есть OrderInteractor с двумя вариантами использования: getOrderHistory() и sendOrder(Order).

В первом варианте использования используется OrderHistoryRepository, а во втором - OrderSenderRepository. Эти репозитории представляют собой интерфейсы с несколькими реализациями (MockOrderHistoryRepository и InternetOrderHistoryRepository для первой). OrderInteractor взаимодействуют с этими репозиториями только через интерфейсы, чтобы скрыть реальную реализацию.

Версия Mock очень фиктивна, но версия Internet хранилища истории хранит некоторые данные в кеше для лучшей работы.

Теперь я хочу реализовать следующее: когда заказ отправлен успешно, я хочу сделать недействительным кеш истории, но я не знаю, где именно я должен выполнить фактическое аннулирование кеша.

Мое первое предположение - добавить invalidateCache() к OrderHistoryRepository и использовать этот метод в конце метода sendOrder() внутри интерактора. В InternetOrderHistoryRepository мне просто нужно будет реализовать аннулирование кеша, и все будет хорошо. Но я буду вынужден фактически реализовать метод внутри MockOrderHistoryRepository, и это покажет извне тот факт, что некоторое управление кешем выполняется репозиторием. Я думаю, что OrderInteractor не должен знать об этом управлении кешем, потому что это детали реализации Internet версии OrderHistoryRepository.

Моим вторым предположением было бы выполнить аннулирование кеша внутри InternetOrderSenderRepository, когда он знает, что заказ был успешно отправлен, но он заставит этот репозиторий узнать InternetOrderHistoryRepository, чтобы получить ключ кеша, используемый этим репо для управления кешем. И я не хочу, чтобы мой OrderSenderRepository зависел от OrderHistoryRepository.

Наконец, мое третье предположение - иметь какой-то CacheInvalidator (независимо от имени) интерфейс с Dummy реализацией, используемой, когда репозиторий имитируется, и реализацией Real, когда Interactor использует Internet репозитории. Этот CacheInvalidator будет внедрен в Interactor, а выбранная реализация будет предоставлена ​​Factory, который строит репозиторий, и CacheInvalidator. Это означает, что у меня будет MockedOrderHistoryRepositoryFactory - строящий MockedOrderHistoryRepository и DummyCacheInvalidator - и InternetOrderHistoryRepositoryFactory - который строит InternetOrderHistoryRepository и RealCacheInvalidator. Но опять же, я не знаю, следует ли использовать этот CacheInvalidator Interactor в конце sendOrder() или непосредственно InternetOrderSenderRepository (хотя я думаю, что последнее лучше, потому что снова взаимодействующий, вероятно, не должен знать, что есть какой-то кеш управление под капотом).

Какой способ построения архитектуры вы бы предпочли?

Большое тебе спасибо. Пьер


person pdegand59    schedule 31.08.2016    source источник
comment
Я думаю, что ваше второе предположение действительно подходит лучше всего, потому что кеширование - это деталь вашего InternetOrder*Repository. Подумайте об этом: InternetOrderHistoryRepository также использует кеш, но также скрывает его.   -  person John McFo    schedule 07.04.2017


Ответы (1)


Ваше второе предположение верно, потому что кеширование - это деталь механизма сохранения. Например. если репозиторий будет файловым, кеширование репозитория может не быть проблемой (например, локальный ssd).

Интерактор (вариант использования) вообще не должен знать о кешировании. Это упростит тестирование, потому что вам не нужен настоящий кеш или макет для тестирования.

Моим вторым предположением было бы выполнить аннулирование кеша внутри InternetOrderSenderRepository, когда он знает, что заказ был успешно отправлен, но он заставит этот репозиторий узнать InternetOrderHistoryRepository, чтобы получить ключ кеша, используемый этим репо для управление кешем.

Кажется, что ваш ключ кеша состоит из нескольких свойств порядка, и поэтому вам нужно где-то инкапсулировать логику создания ключа кеша для повторного использования.

В этом случае у вас есть следующие возможности:

Одна реализация для обоих интерфейсов

Вы можете создать класс, реализующий как InternetOrderSenderRepository, так и InternetOrderHistoryRepository интерфейс. В этом случае вы можете извлечь логику генерации ключа кеша в частный метод и повторно использовать его.

Используйте служебный класс для создания ключа кеша

Просто извлеките логику создания ключа кеша в служебный класс и используйте ее в обоих репозиториях.

Создайте класс ключа кеша

Ключ кеша - это просто произвольный объект, потому что кеш должен только проверять, существует ли ключ, а это означает использование метода equals, который есть у каждого объекта. Но для большей безопасности типов большинство кешей используют общий тип для ключа, чтобы вы могли его определить.

Таким образом, вы можете поместить логику и проверку ключа кеша в собственный класс. Это имеет то преимущество, что вы можете легко проверить эту логику.

public class OrderCacheKey {

    private Integer orderId;
    private int version;

    public OrderCacheKey(Integer orderId, int version) {
        this.orderId = Objects.requireNonNull(orderId);
        if (version < 0) {
            throw new IllegalArgumentException("version must be a positive integer");
        }
        this.version = version;
    }

    public OrderCacheKey(Order order) {
        this(order.getId(), order.getVersion());
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        OrderCacheKey other = (OrderCacheKey) obj;

        if (!Objects.equals(orderId, other.orderId))
            return false;

        return Objects.equals(version, other.version);
    }

    public int hashCode() {
        int result = 1;
        result = 31 * result + Objects.hashCode(orderId);
        result = 31 * result + Objects.hashCode(version);
        return result;
    }

}

Вы можете использовать этот класс в качестве ключевого типа вашего кеша: Cache<OrderCacheKey, Order>. Затем вы можете использовать класс OrderCacheKey в обеих реализациях репозитория.

Добавьте интерфейс кеширования заказов, чтобы скрыть сведения о кешировании

Вы можете применить принцип разделения интерфейса и скрыть все детали кеширования за простым интерфейсом. Это упростит ваши модульные тесты, потому что вам придется меньше имитировать.

public interface OrderCache {

    public void add(Order order);
    
    public Order get(Integer orderId, int version);

    public void remove(Order order);
    
    public void removeByKey(Integer orderId, int version);
}

Затем вы можете использовать OrderCache в обеих реализациях репозитория, а также можете комбинировать разделение интерфейса с указанным выше классом ключа кеша.

Как подать заявку

  • Вы можете использовать аспектно-ориентированное программирование и один из описанных выше вариантов для реализации кэширования.
  • Вы можете создать оболочку (или делегата) для каждого репозитория, который применяет кеширование и при необходимости делегирует реальным репозиториям. Это очень похоже на аспектно-ориентированный способ. Вы просто реализуете аспект вручную.
person René Link    schedule 07.10.2019