Шаблон проектирования «Декоратор»

Статья на сайте.

Во второй статье цикла, посвящённого шаблонам проектирования, я хотел бы поговорить про шаблон проектирования "декоратор", который я частенько путаю с шаблоном "адаптер". О последнем, кстати, я подробно рассказывал в предыдущей статье. Впрочем, оба шаблона проектирования делят одно альтернативное название - "обёртка" (Wrapper).

Шаблон проектирования декоратор будет полезен в тех случаях, когда необходимо добавить некоторому объекту дополнительное поведение, однако использование наследования по тем или иным причинам невозможно или не оправдано. При этом класс-декоратор реализует интерфейс декорируемого объекта, либо расширяет его, следовательно, экземпляр класса-декоратора может быть использован вместо декорируемого объекта без внесения изменений в исходный код, что важно для соблюдения принципа открытости/закрытости.

Умная розетка - типичный «декоратор»
Умная розетка - типичный «декоратор»

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

Реализация декоратора

Для реализации шаблона проектирования "декоратор" потребуется как минимум два класса: класс или интерфейс декорируемого объекта и класс-декоратор, который будет содержать этот самый объект и расширять его поведение. Класс-декоратор может быть расширен другими классами.

В статье о шаблоне проектирования "адаптер" я описывал интерфейс FindTaskByIdSpi, при помощи которого приложение обращается к базе данных для поиска там задачи по её идентификатору.

@FunctionalInterface
interface FindTaskByIdSpi {

    Optional<TaskData> findTaskById(UUID id);
}

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

Класс-декоратор можно описать следующим образом:

abstract class CachingFindTaskByIdDecorator implements FindTaskByIdSpi {

    private FindTaskByIdSpi delegate;

    abstract Optional<TaskData> retrieveFromCache(UUID id);

    abstract void storeInCache(TaskData task);

    public Optional<TaskData> findTaskById(UUID id) {
        return this.retrieveFromCache(id)
            .or(() -> this.delegate.findTaskById(id)
                    .map(task -> {
                        this.storeInCache(task);
                        return task;
                    }));
    }
}

Как уже было сказано, класс-декоратор CachingFindTaskByIdDecorator содержит декорируемый объект и реализует его интерфейс, а в качестве декорируемого объекта может выступать любая реализация интерфейса FindTaskByIdSpi, даже другой класс-декоратор.

Кстати, метод findTaskById в классе-декораторе реализует шаблон проектирования "шаблонный метод".

CachingFindTaskByIdDecorator можно назвать абстрактным декоратором, теперь можно реализовать конкретный декоратор, использующий API кэширования из Spring Framework.

class SpringCachingFindTaskByIdDecorator extends CachingFindTaskByIdDecorator {

    private Cache cache;

    Optional<TaskData> retrieveFromCache(UUID id) {
        return Optional.ofNullable(this.cache.get(id, TaskData.class));
    }

    void storeInCache(TaskData task) {
        this.cache.put(task.id(), task);
    }
}

Теперь класс SpringCachingFindTaskByIdDecorator может быть использован для добавления кэширования при работе с любыми реализациями FindTaskByIdSpi, как это показано ниже:

@Configuration
class ApplicationBeans {

    @Bean
    FindTaskByIdSpi findTaskByIdSpi(DataSource dataSource, Cache cache) {
        var delegate = new FindTaskByIdMappingSqlQuery(dataSource);
        return new SpringCachingFindTaskByIdDecorator(delegate, cache);
    }
}

Общая схема классов выглядит следующим образом:

Шаблон проектирования «Декоратор»
Шаблон проектирования «Декоратор»

Тестирование

Логика тестирования классов-декораторов полностью соответствует логике тестирования классов, реализующих шаблон проектирования "адаптер объекта": нужно протестировать собственную логику класса-декоратора и корректность его обращения к объекту-делегату.

Класс SpringCachingFindTaskByIdDecorator можно протестировать следующим образом:

@ExtendWith(MockitoExtension.class)
class SpringCachingFindTaskByIdDecoratorTest {

    @Mock
    FindTaskByIdSpi delegate;

    @Mock
    Cache cache;

    @InjectMocks
    SpringCachingFindTaskByIdDecorator decorator;

    @Test
    void findTaskById_TaskExistsButIsNotCached_ReturnsNotEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();
        var task = new TaskData(taskId);

        doReturn(Optional.of(task)).when(this.delegate)
                .findTaskById(taskId);

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.of(task), optional);

        verify(this.cache).put(taskId, task);
    }

    @Test
    void findTaskById_TaskIsCached_ReturnsNotEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();
        var task = new TaskData(taskId);

        doReturn(task).when(this.cache).get(taskId, TaskData.class);

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.of(task), optional);

        verify(this.cache, never()).put(any(), any());
    }

    @Test
    void findTaskById_TaskDoesNotExist_ReturnsEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.empty(), optional);

        verify(this.cache, never()).put(any(), any());
    }
}

Выводы

Как уже было сказано выше, шаблон проектирования "декоратор" позволяет добавить новые свойства и методы существующему объекту без внесения изменения в код его класса и без необходимости использования наследования. Применение этого шаблона проектирования добавляет гибкости коду и позволяет соблюдать принцип открытости/закрытости и принцип разделения интерфейсов.

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

30 views·2 shares