Экстремальное программирование: Разработка через тестирование — страница 32 из 41

Встраиваемый объект (Pluggable Object)

Как можно выразить несколько разных вариантов поведения кода? Проще всего использовать явный условный оператор:


if(circle) then {

… код, относящийся к circle.

} else {

… код, не относящийся к circle

}


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

Вторая по важности задача TDD – устранение дублирования, поэтому вы должны подавить угрозу распространения явных условных операторов в зародыше. Если вы видите, что одно и то же условие проверяется в двух разных местах вашего кода, значит, настало время выполнить базовое объектно-ориентированное преобразование: «Встраиваемый объект» (PluggableObject).

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


SelectionTool

Figure selected;

public void mouseDown() {

selected = findFigure();

if (selected!= null)

select(selected);

}

public void mouseMove() {

if (selected!= null)

move(selected);

else

moveSelectionRectangle();

}

public void mouseUp() {

if (selected == null)

selectAll();

}


В глаза бросаются три похожих условных оператора (я же говорил, что они плодятся, как мухи). Что делать, чтобы избавиться от них? Создаем встраиваемый объект, SelectionMode, обладающий двумя реализациями: SingleSelection и MultipleSelection.


SelectionTool

SelectionMode mode;

public void mouseDown() {

selected = findFigure();

if (selected!= null)

mode = SingleSelection(selected);

else

mode = MultipleSelection();

}

public void mouseMove() {

mode.mouseMove();

}

public void mouseUp() {

mode.mouseUp();

}


В языках с явными интерфейсами вы обязаны реализовать интерфейс с двумя (или больше) встраиваемыми объектами.

Встраиваемый переключатель (Pluggable Selector)[26]

Как обеспечить различающееся поведение разных экземпляров одного и того же класса? Сохраните имя метода в переменной и динамически обращайтесь к этому методу.

Что делать, если у вас есть десять подклассов одного базового класса и в каждом из них реализован только один метод? Может оказаться, что создание подклассов – это слишком тяжеловесный механизм для реализации столь небольших различий в поведении объектов.


abstract class Report {

abstract void print();

}

class HTMLReport extends Report {

void print() {…

}

}

class XMLReport extends Report {

void print() {…

}

}


Альтернативное решение: создать единственный класс с оператором switch. В зависимости от значения поля происходит обращение к разным методам. Однако в этом случае имя метода упоминается в трех местах:

• при создании экземпляра;

• в операторе switch;

• в самом методе.


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. Сначала можно подумать, что они различаются, однако после вы понимаете, что они одинаковы.