Шаблон проектирования «Декоратор»
Во второй статье цикла, посвящённого шаблонам проектирования, я хотел бы поговорить про шаблон проектирования "декоратор", который я частенько путаю с шаблоном "адаптер". О последнем, кстати, я подробно рассказывал в предыдущей статье. Впрочем, оба шаблона проектирования делят одно альтернативное название - "обёртка" (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());
}
}Выводы
Как уже было сказано выше, шаблон проектирования "декоратор" позволяет добавить новые свойства и методы существующему объекту без внесения изменения в код его класса и без необходимости использования наследования. Применение этого шаблона проектирования добавляет гибкости коду и позволяет соблюдать принцип открытости/закрытости и принцип разделения интерфейсов.
Несмотря на то, что шаблон проектирования "декоратор" идеологически близок шаблону проектирования "адаптер", он имеет существенное отличие от последнего - класс-декоратор реализует интерфейс декорируемого класса, в то время как класс-адаптер реализует интерфейс, с которым адаптируемый объект изначально не совместим.
