Паттерны проектирования: твоя настольная статья
Зачем вам паттерны проектирования? Кратко о назначении каждого. Разберём, как отличить композицию от агрегации и что лучше: наследование или композиция.
Паттерны проектирования делятся на три группы.
1. Порождающие паттерны
1. Абстрактная фабрика помогает создавать семейства объектов с общими связями или зависимостями без указания их конкретных классов. Фабрики и продукты – основа паттерна. Как раз слово «семейства» отличает шаблон от других порождающих паттернов, где только один тип объекта.
2. Строитель отделяет конструирование сложного объекта от его представления. Представьте приготовление чая с сахаром, чая с молоком и обыкновенного чая. Вы одинаково кипятите воду и насыпаете заварку, а в зависимости от типа добавляете сахар, вливаете молоко или ничего не делаете.
- Строитель определяет способ конструирования отдельных частей. Содержит процессы для инициализации и конфигурации продукта (чая).
- Директор берёт эти отдельные функции и задаёт последовательность создания продукта.
- Продукт – конечный объект, полученный при взаимодействии строителя и директора.
3. Фабричный метод определяет интерфейс для конструирования объекта и даёт свободу дочерним классам выбирать тип класса.
Паттерн делегирует создание экземпляров подклассам. Создадим полосу прокрутки в стиле Mac:
А чтобы это работало на любой платформе, напишем код:
Поскольку guiFactory
– экземпляр класса MacFactory
, CreateScrollBar
возвращает новый экземпляр полосы прокрутки в стиле Mac. MacFactory
– сам по себе дочерний класс GUIFactory
– абстрактного суперкласса, определяющего общий интерфейс для виджетов.
Переменную экземпляра guiFactory
инициализируем так:
4. Прототип определяет тип объектов с применением прототипического экземпляра и создаёт новые путём его копирования.
5. Одиночка гарантирует, что у класса будет единственный экземпляр, и предоставляет глобальную точку доступа к нему.
2. Структурные паттерны
1. Адаптер конвертирует интерфейс класса в ожидаемый клиентами интерфейс. Благодаря этому классы работают вместе, несмотря на несовместимость интерфейсов.
2. Мост отделяет абстракцию объекта от его реализации.
3. Компоновщик объединяет объекты в структуры в виде дерева для представления иерархий частей и целого. Поэтому клиенты одинаково работают и с одним объектом, и с композицией объектов.
4. Декоратор добавляет обязанности к объектам динамически. Это гибкая альтернатива дочерним классам для расширения функциональности.
Например, объект TextView
отображает текст в окне. Когда нужна полоса прокрутки, используйте ScrollDecorator
, граница вокруг текстовой области – BorderDecorator
.
5. Фасад – единственный класс, представляющий целую подсистему.
6. Приспособленец – мелкоструктурный экземпляр для облегчения и оптимизации совместного доступа.
7. Заместитель изображает и представляет другой объект.
3. Поведенческие паттерны
1. Посредник определяет упрощённую связь между классами.
2. Хранитель фиксирует и восстанавливает внутреннее состояние объекта.
3. Интерпретатор – способ добавления языковых элементов в программу.
4. Итератор – последовательный доступ к элементам коллекции.
5. Цепочка обязанностей – способ передачи запросов между цепочкой объектов.
6. Команда упаковывает запрос в объект для параметризации отличающихся клиентов, создания очереди, логирования или поддержки операции отмены.
7. Состояние трансформирует поведение объекта, когда он меняет своё состояние.
8. Стратегия определяет семейство алгоритмов, инкапсулирует каждый для взаимозаменяемости. Благодаря чему изменение алгоритма не подвергается влиянию клиентов, которые его используют.
Пример: Утиный класс с инкапсулированным поведением, таким как Flybehavior
для полёта и Quackbehavior
для кряканья.
9. Наблюдатель устанавливает объектную зависимость «один ко многим» следующим образом: когда состояние главного объекта изменяется, зависимые уведомляются и получают автоматическое обновление.
- Субъект знает наблюдателей.
- Наблюдатель задаёт интерфейс обновления для объекта, который ждёт уведомления об изменениях в субъекте.
10. Шаблонный метод задаёт каркас алгоритма операции, делегируя некоторые шаги дочерним классам. Дочерние классы переопределяют конкретные этапы алгоритма и оставляют его структуру неизменённой.
11. Посетитель добавляет новую функциональность классу без изменений.
Композиция против агрегации
Агрегация и композиция – сильные формы ассоциации, которые описывают отношения между целым и его частями. Таким образом, вместо отношения «имеет», как у обыкновенной ассоциации, получаем отношение «это часть» или в обратном направлении – «состоит из».
Примеры такого вида связей:
- Оркестр – Музыкант: оркестр состоит из музыкантов, или музыкант – часть оркестра.
- Брошюра – Продукт: брошюра состоит из продуктов, или продукт – часть брошюры.
- Здание – Комната: здание состоит из комнат, или комната – часть здания.
Композиция ещё сильнее агрегации. Для проверки типа связи используйте правило единственного использования для композиции: часть принадлежит только одному целому.
Для примера №1 и №2 музыкант или продукт принадлежит только одному оркестру или каталогу? Да! Но может ли в примере №3 комната принадлежать двум зданиям? Нет! Сработало правило единственного использования. Таким образом, отношение №3 лучше описывается композицией, чем агрегацией.
Вот другое правило для композиции: если целое уничтожено, уничтожается ли его часть?
Только для случая №3 работает дополнительное правило композиции. Если оркестр распадётся, исчезнут ли музыканты? Нет!
Пример кода: в университете масса факультетов, в каждом из которых группа профессоров. Если университет закроется, факультетов больше не будет, но профессора останутся. Следовательно, университет – композиция факультетов, а у факультетов агрегация профессоров. Кроме того, профессор может работать не только на одном факультете, но а факультет не может быть частью двух университетов.
1. Университет – факультет: композиция: собственность: уничтожаются вместе: сильное отношение «имеет».
Составной объект – владелец компонента и отвечает за создание и уничтожение частей. Компонент – часть одного составного объекта, уничтожение которого ведёт к ликвидации частей.
Композиция гарантирует инкапсуляцию, поскольку части – члены составного объекта:
2. Факультет – профессора: агрегация: нет собственности: отдельное существование: у компонента остаётся слабое отношение «имеет».
В агрегации объект содержит только ссылку или указатель на объект. Объект не отвечает за ссылку или указатель на протяжении всего жизненного цикла:
Полный пример кода и диаграмма:
В UML композиция изображается в виде закрашенного ромба и сплошной линии. Это говорит о кратности 1 или 0...1, то есть ответственность за объект несёт только один объект. Агрегация обозначается в виде пустого ромба и сплошной линии.
Наследование против композиции
Оба способа применяют для многократного использования функциональности.
Наследование
Класс Cat
связывается с классом Animal
наследованием, потому что Cat
происходит от Animal
.
Композиция
Класс Cat
связывается с Animal
композицией, ведь Cat
содержит переменную с указателем на объект Animal
. Классы подобные Cat
иногда называют front-end классами, а Animal
– back-end классами.
С помощью наследования реализуют один класс на базе другого. В альтернативной композиции новая функциональность получается при составлении объекта. В результате получаем сокрытие внутренних деталей объектов, чего нет в наследовании.
Минусы наследования
Хотя наследование классов облегчает модификацию, реализация подкласса становится слишком связанной с родительской. В результате любая правка в реализации родителя затрагивает подкласс.
Посмотрите на пример:
Так как Cat
наследует (переиспользует) Animal
, получим:
Однако, если изменить метод makeSound()
родительского класса, вот так:
Подпрограмма main()
также изменится, даже когда используем не Animal
, а Cat
.
Вот новое решение:
Как насчёт композиции
Композиция – альтернативный способ для Cat
повторно использовать Animal-реализацию makeSound()
. Добавьте в Cat
указатель на экземпляр Animal
и определите собственный метод makeSound()
, который вызывает makeSound()
для Animal
. Вот код:
При использовании композиции подкласс становится front-end классом, а суперкласс – back-end классом. При наследовании подкласс автоматически наследует реализацию любого неприватного метода суперкласса, если не переопределяет его. С композицией, однако, front-end класс явно вызывает соответствующий метод back-end класса из собственной реализации метода. Этот явный вызов иногда называется переадресацией или делегированием вызова метода back-end объекту.
Композиция даёт больше инкапсуляции, чем наследование, потому что изменение back-end класса не нарушает код, полагающийся на front-end класс. При наследовании подкласс знает детали реализации родителя и своеобразно вредит инкапсуляции.
Изменение возвращаемого типа метода makeSound()
в Animal
из предыдущего примера не вынуждает изменять интерфейс Cat
, и, следовательно, код main()
не ломается.
Пример показывает, что цепная реакция от изменения back-end класса останавливается на front-end классе.
Композиция помогает сохранять каждый класс инкапсулированным и ориентированным на одну задачу. Классы и иерархии классов останутся маленькими и не разрастутся до неуправляемости.
Однако в проекте на базе композиции большее количество объектов (при уменьшении классов) и глобальное поведение опирается на их взаимосвязи, а не объявляется в одном классе.
Чаще используйте композицию.
Делегирование
Делает использование композиции для переиспользования равным по мощности наследованию. В делегировании два объекта участвуют в обработке запроса. Принимающий объект поручает действия делегату, что похоже на случай, когда подкласс доверяет запросы родительскому классу.
В коде ниже, вместо наследования класса Window
от Rectangle
, Window
переиспользует поведение Rectangle
, сохраняя экземпляр Rectangle
в переменной rectangle
и делегируя ему поведение, специфичное для Rectangle
. Таким образом, хотя Window
не Rectangle
, он имеет Rectangle
. С наследованием у Window
было бы такое поведение. Но при композиции Window
делает явное перенаправление запросов экземпляру Rectangle
.
И выводит:
Основное преимущество делегирования в лёгкости объединения поведения во время выполнения. Например, превратим Window
в круг рантайм:
Выбирайте делегирование, только когда больше упрощения, чем усложнения. Динамическое и параметризованное программное обеспечение сложнее в понимании, чем статичное, и добавляется проблема неэффективности во время выполнения. Поэтому используйте делегирование с умом, например, следуя стандартным паттернам проектирования, таким как Стратегия.