Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
30
Добавлен:
12.03.2016
Размер:
2.81 Mб
Скачать

Глава 9. Шаблон Bridge

127

ки. Здесь по прежнему сохраняются проблемы слабой связности классов и их избы точной связанности между собой.

ÐÈÑ. 9.8. Диаграмма последовательностей для нового варианта иерар хии классов

Замечание. Эта версия иерархии классов по прежнему не вызывает желания со провождать программный код системы! Должно существовать лучшее решение.

Ищите альтернативы на начальной стадии проектирования

Хотя предложенный здесь альтернативный вариант иерархии наследования классов немного лучше первоначального, следует отметить, что сама по себе практика поиска альтернативы первоначальному варианту проекта — это хороший стиль разработки. Слишком часто проектировщики останавливаются на том варианте, который первый пришел им в голову, и в дальнейшем не предпринимают каких"либо попыток его улучшить. Я не призываю вас к чрезмерно глубокому и исчерпывающему изучению всех возможных альтернатив (это еще один способ ввергнуть проект в состояние "паралича за счет анализа"). Однако сделать шаг назад и подумать, как можно было бы преодолеть затруднения, свойственные исходному варианту проекта, — это верный подход. Фактически, именно подобный шаг назад и отказ от дальнейшей работы над заведомо слабым проектным решением привели меня к пониманию всей мощи подхода с использованием шаблонов проектирования, описанию которого, собственно, и посвящена эта книга.

128 Часть III. Шаблоны проектирования

Замечания об использовании шаблонов проектирования

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

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

Я пришел к выводу, что при изучении шаблонов полезнее сосредоточиться на кон тексте их применения — т.е. на тех проблемах, которые пытаются решить с их помо щью. Это позволяет получить ответы на вопросы когда и почему. Такой подход пере кликается с философией шаблонов Александера: "Каждый шаблон описывает про блему, которая возникает в данной среде снова и снова, а затем предлагает принцип ее решения таким способом"3.

Попробуем применить данный подход к нашему случаю и сформулируем задачу, для решения которой предназначен шаблон Bridge.

Шаблон Bridge полезен тогда, когда имеется некоторая абстракция и существует не сколько различных вариантов ее реализации. Шаблон позволяет абстракции и реали зации изменяться независимо друг от друга.

Указанные характеристики как нельзя лучше соответствуют нашей ситуации. Поэтому можно сделать вывод, что шаблон Bridge следует использовать, даже еще не зная толком, как он реализуется на практике. То, что абстракцию можно будет менять независимо от ее реализации, означает, что новые элементы абстракции можно будет добавлять, не внося каких либо изменений на уровне реализации и наоборот.

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

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

3 Alexander C., Ishikawa S., Silverstein M. A Pattern Language: Towns/Buildings/Construction,

New York, NY: Oxford University Press, 1977, с. X.

Глава 9. Шаблон Bridge

129

Описание шаблона проектирования Bridge и его вывод

Теперь, когда достигнуто ясное понимание стоящей перед нами проблемы, при шло время общими усилиями вывести шаблон Bridge. Самостоятельный вывод этого шаблона поможет нам осознать его сложность и, одновременно, мощь.

Используем на практике некоторые из основных положений качественного объ ектно ориентированного проектирования — они помогут нам найти решение, очень близкое к шаблону Bridge. С этой целью обратимся к работе Джима Коплина по ана лизу общности и изменчивости4.

Шаблоны проектирования это проектные решения, применяемые многократно

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

Мы уже выяснили, что шаблоном, который необходимо вывести в данном случае, является шаблон Bridge, — выше обсуждалось его определение в книге "банды четырех" и рассматривалась возможность его применения к некоторой реальной задаче. Однако следует напомнить, что в действительности этот шаблон нами еще не выведен. По определению всякий шаблон характеризуется повторяемостью — чтобы считаться шаблоном, некоторое решение должно быть применено, по крайней мере, в трех независимых случаях. Под словом "вывести" здесь подразумевается, что в процессе проектирования самостоятельно будет найдено решение, соответствующее идее шаблона, как если бы до этого момента мы не имели о нем никакого представления. Именно этот подход позволит нам выявить ключевые принципы и полезные стратегии использования данного шаблона.

Работа Дж. Коплина по анализу общности и изменчивости включает рекоменда ции по поиску того, что изменяется и что является общим в заданной проблемной об ласти. Этими рекомендациями мы и воспользуемся. Наша задача состоит в том, чтобы определить, где возможны изменения (анализ общности), а затем установить, как это изменение происходит (анализ изменчивости).

По Коплину, "анализ общности заключается в поиске общих элементов, что помо жет понять, чем члены семейства похожи друг на друга."5 Таким образом, это процесс поиска общих черт во всех элементах, составляющих некоторое семейство (и, следо вательно, их различий).

Анализ изменчивости позволяет установить, каким образом члены семейства из меняются. Изменчивость имеет смысл только в пределах данной общности.

Анализ общности предусматривает выявление структур, которые вряд ли будут изме няться с течением времени, тогда как анализ изменчивости заключается в обнаруже нии структур, которые, вероятно, изменятся. Анализ изменчивости имеет смысл толь ко в терминах контекста, определенного предварительным анализом общностиВ архитектурном смысле анализ общности дает архитектуре ее долговечность, а анализ изменчивости способствует достижению удобства в использовании.

4 Coplein J. Multi Paradigm Design for C++. Reading, MA: Addison Wesley, 1998.

5 Coplein J. Multi Paradigm Design for C++. Reading, MA: Addison Wesley, 1998, с. 63.

130 Часть III. Шаблоны проектирования

Другими словами, если изменчивость — это особые случаи в рамках заданной пред метной области, то общность устанавливает в ней концепции, объединяющие эти осо бые случаи между собой. Общие концепции будут представлены в системе абстрактны ми классами. Вариации, обнаруженные при анализе изменчивости, реализуются по средством создания конкретных классов, производных от этих абстрактных классов.

Вобъектно ориентированном проектировании стала уже почти аксиомой практи ка, когда разработчик, анализируя описание проблемной области, выделяет в нем су ществительные и создает объекты, представляющие их. Затем он отыскивает глаголы, связанные с этими существительным (т.е. их действия), и реализует их, добавляя к объектам необходимые методы. Подобный процесс проявления повышенного вни мания к существительным и глаголам в большинстве случаев приводит к созданию слишком громоздкой иерархии классов. Я считаю, что анализ общности и изменчиво сти как первичный инструмент выделения объектов, в действительности, предпочти тельнее поиска существительных и глаголов в описании предметной области. (Полагаю, что книга Джима Коплина убедительно подтверждает эту точку зрения.)

Впрактике проектирования для работы с изменяющимися элементами применя ются две основные стратегии.

Найти то, что изменяется, и инкапсулировать это.

Преимущественно использовать композицию вместо наследования.

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

Внимательный взгляд на инкапсуляцию

Чаще всего начинающих разработчиков объектно"ориентированных систем учат, что инкапсуляция заключается в сокрытии данных. К сожалению, это очень ограниченное определение. Несомненно, что инкапсуляция действительно позволяет скрывать данные, но она может использоваться и для многих других целей. Еще раз обратимся к рис. 7.2, на котором показано, как инкапсуляция применяется на нескольких уровнях. Безусловно, с ее помощью скрываются данные для каждой конкретной фигуры. Однако обратите внимание на то, что объект Client также ничего не знает о конкретных типах фигур. Следовательно, объект Client не имеет сведений о том, что объекты класса Shape, с которыми он взаимодействует, в действительности являются объектами разных конкретных классов — Rectangle и Circle. Таким образом, тип конкретного класса, с которым объект Client взаимодействует, скрыт от него (инкапсулирован). Это тот вид инкапсуляции, который подразумевается во фразе "найдите то, что изменяется, и инкапсулируйте это". Здесь как раз было обнаружено то, что изменяется, и оно было скрыто за "стеной" абстрактного класса (см. главу 8, Расширение горизонтов).

Рассмотрим данный процесс на примере задачи с вычерчиванием прямоугольника. Сначала идентифицируем то, что изменяется. В нашем случае это различные типы фигур (представленных абстрактным классом Shape) и различные версии графиче ских программ. Следовательно, общими концепциями являются понятия типа фигуры

Глава 9. Шаблон Bridge

131

и графической программы, как показано на рис. 9.9. (Обратите внимание на то, что имена классов выделены курсивом, поскольку это абстрактные классы.)

ÐÈÑ. 9.9. Сущности, которые изменяются в нашем примере

В данном случае предполагается, что абстрактный класс Shape инкапсулирует концепцию типов фигур, с которыми необходимо работать. Каждый тип фигуры дол жен знать, как себя нарисовать. В свою очередь, абстрактный класс Drawing (рисование) отвечает за вычерчивание линий и окружностей. На рисунке указанные выше обязательства представлены посредством определения соответствующих мето дов в каждом из классов.

Теперь необходимо представить на схеме те конкретные вариации, с которыми мы будем иметь дело. Для класса Shape это прямоугольники (класс Rectangle) и окружно сти (класс Circle). Для класса Drawing это графические программы DP1 (класс V1Drawing) и DP2 (класс V2Drawing). Все это схематически показано на рис 9.10.

ÐÈÑ. 9.10. Представление конкретных вариаций

Сейчас наша диаграмма имеет очень упрощенный вид. Определенно известно, что класс V1Drawing будет использовать программу DP1, а класс V2Drawing — программу DP2, но на схеме еще не указано, как это будет сделано. Пока мы просто описали кон цепции, присутствующие в проблемной области (фигуры и графические программы), и указали их возможные вариации.

Имея перед собой два набора классов, очевидно, следует задаться вопросом, как они будут взаимодействовать друг с другом. На этот раз попытаемся обойтись без то го, чтобы добавлять в систему еще один новый набор классов, построенный на углуб лении иерархии наследования, поскольку последствия этого нам уже известны (см. рис. 9.3 и 9.7). На этот раз мы подойдем с другой стороны и попробуем опреде лить, как эти классы могут использовать друг друга (в полном соответствии с приве денным выше утверждением о предпочтительности композиции над наследованием). Главный вопрос здесь состоит в том, какой же из абстрактных классов будет исполь зовать другой?

132 Часть III. Шаблоны проектирования

Рассмотрим две возможности: либо класс Shape использует классы графических программ, либо класс Drawing использует классы фигур.

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

Это требование также нарушает инкапсуляцию. Объекты класса Drawing должны были бы знать определенную информацию об объектах класса Shape, чтобы иметь возможность отобразить их (а именно — тип конкретной фигуры). В результате объ екты класса Drawing фактически оказываются ответственными не только за свое соб ственное поведение.

Вернемся к первому варианту. Что если объекты класса Shape для отображения себя будут использовать объекты класса Drawing? Объектам класса Shape не нужно знать, какой именно тип объекта класса Drawing будет использоваться, поэтому клас су Shape можно разрешить ссылаться на класс Drawing. Дополнительно класс Shape в этом случае можно сделать ответственным за управление рисованием.

Последний вариант выглядит предпочтительнее. Графически это решение пред ставлено на рис. 9.11.

ÐÈÑ. 9.11. Связывание абстрактных классов между собой

В данном случае класс Shape использует класс Drawing для проявления собствен ного поведения. Мы оставляем без внимания особенности реализации классов V1Drawing, использующего программу DP1, и V2Drawing, использующего программу DP2. На рис. 9.12 эта задача решена посредством добавления в класс Shape защищен ных методов drawLine() и drawCircle(), которые вызывают методы drawLine() и drawCircle() объекта Drawing, соответственно.

 

 

 

 

 

 

Глава 9. Шаблон Bridge

133

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ÐÈÑ. 9.12. Расширение проекта

Одно правило, одно место

При проектировании реализации очень важно следовать стратегии, согласно которой каждое правило реализуется только в одном месте. Другими словами, если имеется некоторое правило, определяющее способ выполнения каких"либо действий, оно должно быть реализовано только в одном месте. Такой подход обычно приводит к получению программного кода с большим количеством коротких методов. При этом за счет минимальных дополнительных усилий устраняется дублирование и удается избежать множества потенциальных проблем. Дублирование вредно не только из"за выполнения дополнительной работы по многократному вводу одинаковых фрагментов кода, но и в большей степени из"за вероятности того, что при каких"либо изменениях в будущем модификация будет произведена не везде.

В методе draw() класса Rectangle можно было бы непосредственно вызвать метод DrawLine() того объекта класса Drawing, к которому обращается объект класса Shape. Однако, следуя указанной выше стратегии, можно улучшить программный код, создав в классе Shape метод drawLine(), который и будет вызывать метод DrawLine() класса

Drawing.

Я не считаю себя консерватором (по крайней мере, в большинстве случаев), но в этом вопросе я полагаю совершенно необходимым всегда соблюдать установленные правила. В примере, который будет обсуждаться ниже, класс Shape содержит метод drawLine(), описывающий правило вычерчивания линии объектом Drawing. Аналогичный метод drawCircle() предназначен для отображения окружностей. Следуя рекомендуемой здесь стратегии, мы готовим фундамент для появления других производных классов фигур, при вычерчивании которых могут потребоваться линии и окружности.

Где впервые была предложена стратегия реализации каждого правила в одном месте? Хотя многие упоминают о ней в своих публикациях, она утвердилась в фольклоре разработчиков объектно"ориентированных проектов уже давно и всегда представлялось как оптимальная практика проектирования. Совсем недавно Кент Бекк (Kent Beck) назвал эту стратегию правилом "однажды и только однажды"6.

Он определяет это правило так.

Система (и код, и тесты) должна иметь доступ ко всему, к чему, по вашему мнению, она должна иметь доступ.

Система не должна содержать никакого дублирующегося кода.

Эти две составляющие вместе и представляют собой правило "однажды и только однажды".

6 Beck K. Extreme Programming Explained: Embrace Change, Reading, MA: Addison Wesley,

2000, с. 108, 109.

134 Часть III. Шаблоны проектирования

На рис. 9.13 показано, как абстракция Shape может быть отделена от реализации

Drawing.

С точки зрения методов, новый вариант системы весьма похож на реализацию, построенную на наследовании (см. рис. 9.3). Главное отличие состоит в том, что здесь методы расположены в различных объектах.

Как уже было сказано в начале этой главы, мое замешательство при первом зна комстве с определением шаблона Bridge было вызвано неправильным пониманием термина "реализация". Поначалу я полагал, что этот термин относится к тому, как конкретная абстракция реализуется в программном коде.

Шаблон Bridge позволяет взглянуть на реализацию как на нечто, находящееся вне наших объектов, нечто используемое этими объектами. В результате мы получаем на много большую свободу за счет сокрытия вариации в реализации от вызывающей час ти программы. Разрабатывая объекты по этому принципу, я также обнаружил воз можность размещения вариаций различного типа в раздельных иерархиях классов. Иерархия, изображенная на рис. 9.13 слева, включает вариации в абстракциях. Иерархия справа включает вариации в том, как будут реализованы эти абстракции. Такой подход отвечает новой парадигме создания объектов (использование анализа общности и изменчивости), упомянутой выше.

ÐÈÑ. 9.13. Диаграмма классов, иллюстрирующая отделение абстракции от реализации

Очень легко визуализировать сказанное, если вспомнить, что в любой момент времени в системе существует только три взаимодействующих объекта — несмотря на то, что в ней реализовано около десятка различных классов (рис. 9.14).

 

 

 

 

 

 

 

Глава 9. Шаблон Bridge

135

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ÐÈÑ. 9.14. В любой момент времени в программе существует только три объекта

Достаточно полный фрагмент программного кода для нашего примера представ лен в листинге 9.3 для языка Java и в листингах 9.4–9.6, помещенных в приложение к этой главе, для языка C++.

Листинг 9.3. Фрагмент кода на языке Java

class Client {

public static void main (String argv[]) {

Shape r1, r2; Drawing dp;

dp = new V1Drawing();

r1 = new Rectangle(dp, 1, 1, 2, 2);

dp = new V2Drawing();

r2 = new Circle(dp, 2, 2, 3);

r1.draw();

r2.draw();

}

}

abstract class Shape { abstract public draw() ; private Drawing _dp;

Shape (Drawing dp) { _dp= dp;

}

public void drawLine ( double x1, double y1, double x2, double y2) {

_dp.drawLine(x1, y1, x2, y2);

}

public void drawCircle (

136 Часть III. Шаблоны проектирования

double x, double y, double r) { _dp.drawCircle(x, y, r);

}

}

abstract class Drawing { abstract public void drawLine (

double x1, double y1, double x2, double y2);

abstract public void drawCircle ( double x, double y, double r);

}

class V1Drawing extends Drawing { public void drawLine (

double x1, double y1, double x2, double y2) {

DP1.draw_a_line(x1, y1, x2, y2);

}

public void drawCircle (

double x, double y, double r) { DP1.draw_a_circle(x, y, r);

}

}

class V2Drawing extends Drawing { public void drawLine (

double x1, double y1, double x2, double y2) {

//У программы DP2 порядок аргументов отличается

//и они должны быть перестроены

DP2.drawline(x1, x2, y1, y2);

}

public void drawCircle (

double x, double y, double r) { DP2.drawcircle(x, y, r);

}

}

class Rectangle extends Shape { public Rectangle (

Drawing dp,

double x1, double y1, double x2, double y2) {

super(dp) ;

_x1 = x1; _x2 = x2; _y1 = y1; _y2 = y2;

}

public void draw () { drawLine(_x1, _y1, _x2, _y1); drawLine(_x2, _y1, _x2, _y2); drawLine(_x2, _y2, _x1, _y2); drawLine(_x1, _y2, _x1, _y1);

}

}

class Circle extends Shape { public Circle (

Drawing dp,

double x, double y, double r) { super(dp) ;

_x = x; _y = y; _r = r ;

}

Соседние файлы в папке Материалы