Предыдущая Оглавление Следующая

Что такое паттерн проектирования

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

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

В общем случае паттерн состоит из четырех основных элементов:

  1. Имя. Сославшись на него, мы можем описать проблему проектирования, ее решения и их последствия.
  2. Задача. Описание того, когда следует применять паттерн. Необходимо сформулировать задачу и ее контекст. Может описываться конкретная проблема проектирования, например способ представления алгоритмов в виде объектов.
  3. Решение. Описание элементов дизайна, отношений между ними, функций каждого элемента.
  4. Результаты - это следствия применения паттерна и разного рода компромиссы.

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

При описании паттернов проектирования будем придерживаться единого принципа. Описание каждого паттерна разбито на разделы, описанные ниже.

Название и классификация паттерна. Название паттерна должно четко отражать его назначение.

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

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

Структура. Графическое представление классов в паттерне.

Участники. Классы или объекты, задействованные в данном паттерне проектирования и их функции.

Отношения. Взаимодействие участников для выполнения своих функций.

Результаты. Насколько паттерн удовлетворяет поставленным требованиям? Результаты применения, компромиссы, на которые приходится идти.

Реализация. Сложности и подводные камни при реализации паттерна. Советы и рекомендуемые приемы. Есть ли зависимость от языка программирования.

Родственные паттерны. Связь с другими паттернами проектирования. Важные различия. Совместное использование.

Решение задач проектирования с помощью паттернов

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

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

Посылка запроса - единственный способ заставить объект выполнить операцию. А выполнение операции - единственный способ изменить внутреннее состояние объекта.

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

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

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

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

Тип - это имя, используемое для обозначения конкретного интерфейса. У одного объекта может быть много типов. Напротив, сильно отличающиеся объекты могут разделять общий тип. Часть интерфейса может характеризоваться одним типом, а чась - другим. Интерфейсы могут содержать другие интерфейсы в качестве подмножеств. Один тип является подтипом другого, если интерфейс первого содержит интерфейс второго. В этом случае второй тип является супертипом для первого. Говорят также, что подтип наследует интерфейс своего супертипа.

Когда объекту посылается запрос,то операция, которую он будет выполнять, зависит как от запроса, так и от объекта-адресата. Разные объекты могут выполнять разные операции. Ассоциация запроса с объектом и одной из его операций во время выполнения называется динамическим связыванием.

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

Важно понимать различие между классом объекта и его типом. Класс объекта определяет, как объект реализован, т.е. внутреннее строение. Напротив, тип относится только к интерфейсу объекта. У объекта может быть много типов, и объекты разных классов могут иметь один и тот же тип.

Важно также понимать различие между наследованием класса и наследованием интерфейса. В случае наследования класса реализация объекта определяется в терминах реализации другого объекта. Напротив, наследование интерфейса описывает, когда один объект можно использовать вместо другого. Стандартный способ реализации наследования интерфейса в C++ - это открытое наследование классу, в котором есть исключительно виртуальные функции. Наследование класса или реализации можно осуществить с помощью закрытого наследования.

Механизмы повторного использования

Наследование и композиция

Наследование класса позволяет определить реализацию одного класса в терминах другого. Повторное использование за счет порождения подкласса называют еще прозрачным ящиком (white-box reuse). В этом случае внутреннее устройство родительских классов видимо производным классам.

Композиция объектов - это альтернатива наследованию классов. В этом случае новую, более сложную функциональностьмы получаем путем объединения или композиции объектов. Для композиции требуется, чтобы объединяемые объекты имели четко определенные интерфейсы. Такой способ повторного использования называют черным ящиком (black-box reuse).

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

Но у наследования есть и минусы. Во-первых, нельзя изменить унаследованную реализацию во время выполнения. Во-вторых, родительский класс нередко хотя бы частично определяет физическое представление своих подклассов. Реализации родительского и производного классов сильно связаны.

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

Делегирование

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

Например, вместо того чтобы делать класс Window (окно) подклассом класса Rectangle (прямоугольник) - ведь окно является прямоугольником, - мы можем воспользоваться внутри класса Window поведением класса Rectangle, поместив в класс Window переменную экземпляра типа Rectangle и делегируя ей операции, специфичные для прямоугольников. Теперь класс Window может явно перенаправлять запросы своему члену Rectangle, а не наследовать его операции.

На диаграмме ниже изображен класс Window, который делегирует операцию Area() над своей внутренней областью переменной экземпляра Rectangle.

Главное достоинство делегирования в том, что оно упрощает композицию поведений во время выполнения. При этом способ комбинирования поведений можно изменять. Внутреннюю область окна разрешается сделать круговой во время выполнения, просто подставив вместо экземпляра класса Rectangle экземпляр класса Circle; предполагается, конечно, что оба эти класса имеют одинаковый тип.

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

Наследование и параметризованные типы

Еще один метод повторного использования имеющейся функциональности - применение параметризованных типов, известных также как обобщенные типы или шаблоны (templates). Данная техника позволяет определить тип, не задавая типы, которые он использует. Неспецифицированные типы передаются в виде параметров в точке использования. Параметризованные типы дают еще один способ комбинировать поведение в объектно-ориентированных системах.

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

Агрегирование и осведомленность

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

Осведомленность (acquaintance) подразумевает, что объекту известно о другом объекте. Иногда осведомленность называют ассоциацией или отношением "использует". Осведомленные объекты могут запрашивать друг у друга операции, но они не несут никакой ответственности друг за друга. Осведомленность - это более слабое отношение, чем агрегирование; оно предполагает гораздо менее тесную связь между объектами.

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

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

Предыдущая Оглавление Следующая