Книга: Экстремальное программирование. Разработка через тестирование

Компоновщик (Composite)

Компоновщик (Composite)

Как лучше всего реализовать объект, чье поведение является композицией функций некоторого набора других объектов? Примените шаблон «Самозванец» (Imposter) – заставьте этот объект вести себя подобно тому, как ведут себя отдельные объекты, входящие в набор.

Мой любимый пример основан на двух объектах: Account (счет) и Transaction (транзакция). Этот пример помимо прочего демонстрирует некоторую противоречивость шаблона «Компоновщик» (Composite), но об этом позже. В объекте Transaction хранится изменение величины счета (безусловно, транзакция – это более сложный и интересный объект, однако на данный момент мы ограничимся лишь мизерной долей его возможностей):

Transaction

Transaction(Money value) {

this.value = value;

}

Объект Accout вычисляет баланс счета путем суммирования значений относящихся к нему объектов Transaction:

Account

Transaction transactions[];

Money balance() {

Money sum = Money.zero();

for (int i = 0; i < transactions.length; i++)

sum = sum.plus(transactions[i].value);

return sum;

}

Все выглядит достаточно просто:

• в объектах Transaction хранятся значения;

• в объекте Account хранится баланс.

Теперь самое интересное. У клиента есть несколько счетов, и он хочет узнать общий баланс по всем этим счетам. Первая мысль, которая приходит в голову: создать новый класс OverallAccount, который суммирует балансы для некоторого набора объектов Account. Дублирование! Дублирование!

А что, если классы Account и Transaction будут поддерживать один и тот же интерфейс? Давайте назовем его Holding (сбережения), потому что сейчас мне не удается придумать что-либо лучшее:

Holding

interface Holding

Money balance();

Чтобы реализовать метод balance() в классе Transaction, достаточно вернуть хранящееся в этом классе значение:

Transaction

Money balance() {

return value;

}

Теперь в классе Account можно хранить не транзации, а объекты Holding:

Account

Holding holdings[];

Money balance() {

Money sum = Money.zero();

for (int i = 0; i < holdings.length; i++)

sum = sum.plus(holdings[i].balance());

return sum;

}

Проблема, связанная с созданием класса OverallAccount, испарилась в воздухе. Объект OverallAccount – это просто еще один объект Account, в котором хранятся не транзакции, а другие объекты Account.

Теперь о противоречивости. В приведенном примере хорошо чувствуется запах шаблона «Компоновщик» (Composite). В реальном мире транзакция не может содержать в себе баланс. В данном случае программист идет на уловку, которая совершенно не логична с точки зрения всего остального мира. Вместе с тем преимущества подобного дизайна неоспоримы, и ради этих преимуществ можно пожертвовать некоторым концептуальным несоответствием. Если присмотреться, подобные несоответствия встречаются нам на каждом шагу: папки (Folders), в которых содержатся другие папки (Folders), наборы тестов (TestSuites), в которых содержатся другие наборы тестов (TestSuites), рисунки (Drawings), в которых содержатся другие рисунки (Drawings). Любая из этих метафор недостаточно хорошо соответствует взаимосвязи между вещами в реальном мире, однако все они существенно упрощают код.

Я вынужден был длительное время экспериментировать с шаблоном «Компоновщик» (Composite), прежде чем научился понимать, когда его следует использовать, а когда – нет. Наверное, вы уже поняли, что я не могу предоставить вам однозначных рекомендаций относительно решения проблемы, в каких ситуациях коллекция объектов является просто коллекцией объектов, а в каких это – объект-компоновщик. Хорошая новость состоит в том, что, когда вы достаточно хорошо освоите рефакторинг, вы наверняка сможете обнаружить возникновение дублирования, воспользоваться шаблоном «Компоновщик» (Composite) и обнаружить, что код существенно упростился.

Оглавление книги


Генерация: 0.298. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз