abstract class Report {
String printMessage;
Report(String printMessage) {
this.printMessage = printMessage;
}
void print() {
switch (printMessage) {
case "printHTML":
printHTML();
break;
case "printXML":
printXML():
break;
}
};
void printHTML() {
}
void printXML() {
}
}
Каждый раз, когда вы добавляете новую разновидность печати, вы должны позаботиться о добавлении нового метода печати и редактировании оператора switch.
Шаблон «Встраиваемый переключатель» (Pluggable Selector) предлагает динамически обращаться к методу с использованием механизма рефлексии:
void print() {
Method runMethod = getClass(). getMethod(printMessage, null);
runMethod.invoke(this, new Class[0]);
}
По-прежнему существует весьма неприятная зависимость между создателями отчетов и именами методов печати, однако, по крайней мере, мы избавились от оператора switch.
Естественно, этим шаблоном не следует злоупотреблять. Самая большая связанная с ним проблема состоит в отслеживании вызываемого кода. Используйте встраиваемый переключатель только в случае, когда вы оказались в стандартной ситуации: каждый из подклассов обладает всего одним методом, и у вас есть желание сделать этот код более чистым.
Фабричный метод (Factory Method)Как лучше всего создавать объекты в случае, если вы хотите обеспечить гибкость при создании объектов? Вместо того чтобы использовать конструктор, создайте объект внутри специального метода.
Безусловно, конструкторы являются выразительным инструментом. Если вы используете конструктор, всем, кто читает код, однозначно становится ясно, что вы создаете объект. Однако конструкторы, в особенности в Java, не обеспечивают достаточной гибкости.
В рассмотренном ранее «денежном» примере при создании объекта мы хотели бы возвращать объект иного класса. У нас есть следующий тест:
public void testMultiplication() {
Dollar five = new Dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
Мы хотели бы добавить в программу новый класс Money, однако мы не можем этого сделать, так как для тестирования нам нужен экземпляр класса Dollar. Чтобы решить проблему, достаточно добавить в программу дополнительный уровень перенаправления – метод, который будет возвращать объект иного класса. В этом случае мы сможем оставить выражения assert без изменений:
public void testMultiplication() {
Dollar five = Money.dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
Money
static Dollar dollar(int amount) {
return new Dollar(amount);
}
Такой метод называется фабричным методом (Factory Method), так как он предназначен для создания объектов.
Недостаток этого шаблона заключается в том, что предназначение фабричного метода не очевидно: вы должны помнить о том, что этот метод создает объекты, вместе с тем это обычный метод, а не конструктор. Фабричный метод следует использовать только тогда, когда вы действительно нуждаетесь в гибкости, которую он обеспечивает. В противном случае для создания объектов вполне можно использовать обычные конструкторы.
Самозванец (Imposter)Как можно добавить в программу новую вариацию некоторой функциональности? Создайте новый объект с точно таким же протоколом, как и существующий объект, но с отличающейся реализацией.
При использовании процедурно-ориентированного подхода для решения подобной задачи в программу требуется добавить как минимум один условный оператор. Как было продемонстрировано ранее, при обсуждении шаблона «Встраиваемый переключатель» (Pluggable Selector), такие условные операторы имеют тенденцию плодиться подобно саранче. Чтобы избавиться от дублирования, требуется полиморфизм.
Представьте, что у вас уже есть необходимая инфраструктура. У вас уже есть объект, который реализует необходимую функциональность. Теперь вы хотите, чтобы ваша система делала нечто отличающееся. Если вы обнаружили очевидное место для добавления оператора if и при этом не возникает дублирования какой-либо существующей логики, действуйте смело и решительно. Однако зачастую для добавления вариации требуется внести изменения в код нескольких методов.
Если вы работаете в стиле TDD, решение об использовании самозванца может возникнуть исходя из разных предпосылок. Иногда вы пишете тест и у вас возникает желание реализовать новый сценарий. Однако ни один из существующих объектов не выражает того, что вы хотите выразить. Представьте, что мы тестируем графический редактор и нам уже удалось реализовать корректное рисование прямоугольников:
testRectangle() {
Drawing d = new Drawing();
d. addFigure(new RectangleFigure(0, 10, 50, 100));
RecordingMedium brush = new RecordingMedium();
d. display(brush);
assertEquals("rectangle 0 10 50 100\n", brush.log());
}
Теперь мы хотим реализовать рисование овалов. В данном случае необходимость применения шаблона «Самозванец» (Imposter) очевидна: заменяем RectangleFigure на OvalFigure.
testOval() {
Drawing d = new Drawing();
d. addFigure(new OvalFigure(0, 10, 50, 100));
RecordingMedium brush = new RecordingMedium();
d. display(brush);
assertEquals("oval 0 10 50 100\n", brush.log());
}
Как правило, чтобы увидеть необходимость использования этого шаблона еще до начала разработки кода, требуется озарение. Именно озарением можно назвать момент, когда Уорд Каннингэм решил, что вектор объектов Money может вести себя так же, как одиночный объект Money. Сначала можно подумать, что они различаются, однако после вы понимаете, что они одинаковы.
Вот два примера использования «Самозванец» (Imposter) в процессе рефакторинга:
• «Нуль-объект» (Null Object) – вы можете рассматривать отсутствие данных в точности так же, как и присутствие данных;
• «Компоновщик» (Composite) – вы можете рассматривать коллекцию объектов как одиночный объект.
Решение об использовании «Самозванец» (Imposter) в процессе рефакторинга принимается для устранения дублирования, впрочем, целью любого рефакторинга является устранение дублирования.
Компоновщик (Composite)Как лучше всего реализовать объект, чье поведение является композицией функций некоторого набора других объектов? Примените шаблон «Самозванец» (Imposter) – заставьте этот объект вести себя подобно тому, как ведут себя отдельные объекты, входящие в набор.
Мой любимый пример основан на двух объектах: Account (счет) и Transaction (транзакция). Этот пример помимо прочего демонстрирует некоторую противоречивость шаблона «Компоновщик» (Composite), но об этом позже. В объекте Transaction хранится изменение величины счета (безусловно, транзакция – это более сложный и интересный объект, однако на данный момент мы ограничимся лишь мизерной долей его возможностей):
Transaction
Transaction(Money value) {
this.value = value;
}
Объект Accout вычисляет баланс счета путем суммирования значений относящихся к нему объектов Transaction: