Шаблонный метод - шаблоны проектирования в деталях
Шаблонный метод - это популярный поведенческий шаблон проектирования. При его помощи можно задать некоторое поведение в методе абстрактного класса, но при этом оставить детали реализации на классы-наследники. Проще говоря, в контексте языка программирования Java шаблонный метод - это метод абстрактного класса или метод по умолчанию интерфейса, задающий общее поведение, но опирающийся на другие абстрактные методы. Шаблонный метод наряду с шаблоном проектирования "Стратегия" часто применяется при реализации инверсии управления.
Демонстрируя пример реализации шаблона проектирования "Декоратор", я использовал и шаблонный метод в классе CachingFindTaskByIdDecorator:
abstract class CachingFindTaskByIdDecorator implements FindTaskByIdSpi {
private FindTaskByIdSpi delegate;
abstract Optional<TaskData> retrieveFromCache(UUID id);
abstract void storeInCache(TaskData task);
CachingFindTaskByIdDecorator(FindTaskByIdSpi delegate) {
this.delegate = delegate;
}
public Optional<TaskData> findTaskById(UUID id) {
return this.retrieveFromCache(id)
.or(() -> this.delegate.findTaskById(id)
.map(task -> {
this.storeInCache(task);
return task;
}));
}
}Метод findTaskById в данном случае реализует шаблон проектирования "Шаблонный метод", он задаёт некоторое поведение, но оставляет детали реализации, поиск и сохранение задачи в кэше, на классы-наследники.
Тестирование шаблонного метода
Как правило, тестирование шаблонного метода происходит при тестировании классов-наследников. Однако возможна ситуация, когда классов-наследников у абстрактного класса, содержащего шаблонный метод, нет наследников в рамках проекта. Происходит это в случаях, когда вы разрабатываете какой-то фреймворк или библиотеку и хотите, чтобы реализацию этого абстрактного класса предоставлял пользователь вашей библиотеки. В этом случае всё же желательно написать тест для абстрактного класса, содержащего шаблонный метод. Сделать это можно двумя способами: при помощи тестовой реализации абстрактного класса или при помощи фреймворков для создания объектов-имитаций, например Mockito.
В первом случае вам необходимо создать в директории тестовых классов максимально простой класс, реализующий тестируемый абстрактный класс. Для CachingFindTaskByIdDecorator тестовая реализация может выглядеть следующим образом:
class TestingCachingFindTaskByIdDecorator implements CachingFindTaskByIdDecorator {
TestingCachingFindTaskByIdDecorator(FindTaskByIdSpi delegate) {
super(delegate);
}
TaskData cachedTask;
@Override
Optional<TaskData> retrieveFromCache(UUID id) {
return Optional.ofNullable(this.cachedTask);
}
@Override
void storeInCache(TaskData task) {
this.cachedTask = task;
}
}Теперь необходимо написать тест для TestingCachingFindTaskByIdDecorator:
@ExtendWith(MockitoExtension.class)
class TestingCachingFindTaskByIdDecoratorTest {
@Mock
FindTaskByIdSpi delegate;
TestingCachingFindTaskByIdDecorator decorator;
@BeforeEach
void setUp() {
this.decorator = spy(new TestingCachingFindTaskByIdDecorator(this.delegate));
}
@Test
void findTaskById_TaskIsCached_ReturnsNotEmptyOptional() {
// given
var taskId = UUID.randomUUID();
var task = new TaskData(taskId);
this.decorator.cachedTask = task;
// when
var optional = this.decorator.findTaskById(taskId);
// then
assertEquals(Optional.of(task), optional);
verify(this.decorator, never()).storeInCache(any());
}
@Test
void findTaskById_TaskIsNotCached_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.decorator).storeInCache(task);
}
@Test
void findTaskById_TaskDoesNotExist_ReturnsEmptyOptional() {
// given
var taskId = UUID.randomUUID();
// when
var optional = this.decorator.findTaskById(taskId);
// then
assertEquals(Optional.empty(), optional);
verify(this.decorator, never()).storeInCache(any());
}
}Я не сторонник создания классов специально для тестов вроде TestingCachingFindTaskByIdDecorator, поэтому предлагаю рассмотреть второй вариант. В нём необходимо будет создать объект-имитацию CachingFindTaskByIdDecorator, но использовать реальный метод findTaskById. Кроме этого при создании объекта-имитации потребуется использовать правильный конструктор для передачи декорируемого объекта.
Тест будет выглядеть следующим образом:
@ExtendWith(MockitoExtension.class)
class CachingFindTaskByIdDecoratorTest {
@Mock
FindTaskByIdSpi delegate;
CachingFindTaskByIdDecorator decorator;
@BeforeEach
void setUp() {
this.decorator = mock(CachingFindTaskByIdDecorator.class, withSettings()
.useConstructor(this.delegate));
doCallRealMethod().when(this.decorator).findTaskById(any());
}
@Test
void findTaskById_TaskIsCached_ReturnsNotEmptyOptional() {
// given
var taskId = UUID.randomUUID();
var task = new TaskData(taskId);
doReturn(Optional.of(task)).when(this.decorator).retrieveFromCache(taskId);
// when
var optional = this.decorator.findTaskById(taskId);
// then
assertEquals(Optional.of(task), optional);
verify(this.decorator, never()).storeInCache(any());
}
@Test
void findTaskById_TaskIsNotCached_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.decorator).storeInCache(task);
}
@Test
void findTaskById_TaskDoesNotExist_ReturnsEmptyOptional() {
// given
var taskId = UUID.randomUUID();
// when
var optional = this.decorator.findTaskById(taskId);
// then
assertEquals(Optional.empty(), optional);
verify(this.decorator, never()).storeInCache(any());
}
}Как видно из получившегося кода, можно обойтись без тестовых реализаций абстрактных классов.
Применяйте шаблонный метод для реализации общего поведения в абстрактных классах для придания большей гибкости вашему коду.
