-
Notifications
You must be signed in to change notification settings - Fork 1
О паттернах, которые я использовал в 3 лабораторной работе
“Мы должны модифицировать нашу программу, не изменяя уже написанный код.”
Тассов К. Л.
"Вы должны глубоко продумывать и прорабатывать архитектуру. Вы должны рассматривать систему в развитии, предполагая, что она будет постоянно модифицироваться. И любая модификация [в каком месте системы она бы не возникла] не должна приводить к изменению уже написанного кода."
Тассов К. Л.
Именно поэтому мы создаём кучу классов.
"В процессе разработки нужно постоянно задавать себе вопрос: "А если я здесь что-то изменю, что будет?" Сейчас это будет идти тяжело, а потом будет на автоматизме. Вы все эти моменты будете чувствовать уже ... другими местами [ахахах, тонко]. ... Через некоторое время вы будете это воспринимать как структурное программирование."
Тассов К. Л.
Именно поэтому мы должны накрутить на нашу лабу различные паттерны, которые позволят нам этого добиться.
Продумать и реализовать такую архитектуру, чтобы её можно было легко модифицировать, не изменяя (вообще никак) уже написанный код. Мы должны продумывать структуру так, чтобы мы могли в любое место программы впихнуть новый класс и начать его использовать, вообще не меняя то, что мы уже написали. Классно же, да?
Стоит отметить, что очень важно представлять, как меняется клиентский код, когда мы начинаем использовать какой-либо паттерн.
Итак, перейдём к паттернам, которые я использовал в 3 лабе по ООП:
Необходимо избавиться от операторов new
, раскиданных по всей программе, обернув их, и унифицировать создание объектов конкретного класса (сделать создание только в одном месте).
- Когда заранее неизвестно, объекты каких типов необходимо создавать
- Когда система должна быть независимой от процесса создания новых объектов и расширяемой: в нее можно легко вводить новые классы, объекты которых система должна создавать.
- Когда создание новых объектов необходимо делегировать из базового класса классам наследникам
Для того, чтобы система оставалась независимой от различных типов объектов, паттерн Factory Method использует механизм полиморфизма - классы всех конечных типов наследуют от одного абстрактного базового класса, предназначенного для полиморфного использования. В этом базовом классе определяется единый интерфейс, через который пользователь будет оперировать объектами конечных типов.
Для обеспечения относительно простого добавления в систему новых типов паттерн Factory Method локализует создание объектов конкретных типов в специальном классе-фабрике. Методы этого класса, посредством которых создаются объекты конкретных классов, называются фабричными.
- Абстрактный класс
Product
определяет интерфейс класса, объекты которого надо создавать. - Конкретные классы
ConcreteProductA
иConcreteProductB
представляют реализацию классаProduct
. Таких классов может быть множество - Абстрактный класс
Creator
определяет абстрактный фабричный методFactoryMethod()
, который возвращает объектProduct
. - Конкретные классы
ConcreteCreatorA
иConcreteCreatorB
- наследники классаCreator
, определяющие свою реализацию методаFactoryMethod()
. Причем методFactoryMethod()
каждого отдельного класса-создателя возвращает определенный конкретный тип продукта. Для каждого конкретного класса продукта определяется свой конкретный класс создателя.
Таким образом, класс Creator
делегирует создание объекта Product
своим наследникам. А классы ConcreteCreatorA
и ConcreteCreatorB
могут самостоятельно выбирать какой конкретный тип продукта им создавать.
- Код очищается от
new
, используя механизм полиморфизма по полной. - Создаём новые классы, не изменяя уже написанный код.
- Этот паттерн работает во всех языках программирования
- Позволяет разнести в коде принятие решения о создании объекта и само создание ==> эти задачи можно делегировать разным классам
- Решение о том, какой объект создать, принимается во время выполнения программы (тогда же можем менять решение в зависимости от каких-либо данных)
Паттерн “Фабричный метод” довольно сложно объяснить в метафорах, но всё же попробую.
Ключевой сложностью объяснения данного паттерна является то, что это «метод», поэтому метафора метода будет использовано как действие, то есть, например, слово «Хочу!». Соответственно, паттерн описывает то, как должно выполнятся это «Хочу!».
Допустим ваша фабрика производит пакеты с разными соками. Теоретически мы можем на каждый вид сока делать свою производственную линию, но это не эффективно. Удобнее сделать одну линию по производству пакетов-основ, а разделение ввести только на этапе заливки сока, который мы можем определять просто по названию сока. Однако откуда взять название?
Для этого мы создаем основной отдел по производству пакетов-основ и предупреждаем все подотделы, что они должны производить нужный пакет с соком про простому «Хочу!» (т.е. каждый подотдел должен реализовать паттерн «фабричный метод»). Поэтому каждый подотдел заведует только своим типом сока и реагирует на слово «Хочу!».
Таким образом если нам потребуется пакет апельсинового сока, то мы просто скажем отделу по производству апельсинового сока «Хочу!», а он в свою очередь скажет основному отделу по созданию пакетов сока, «Сделай-ка свой обычный пакет и вот сок, который туда нужно залить».
Возникает необходимость создания похожих (но не родственных) объектов, связанных одной библиотекой или приложением. И при этом мы хотим иметь возможность подменять библиотеки. А Creator’ы из паттерна Factory Method отвечают только за объекты одной иерархии.
Паттерн "Абстрактная фабрика" (Abstract Factory) предоставляет интерфейс для создания семейств взаимосвязанных объектов с определенными интерфейсами без указания конкретных типов данных объектов.
- Когда система не должна зависеть от способа создания и компоновки новых объектов
- Когда создаваемые объекты должны использоваться вместе и являются взаимосвязанными
- Абстрактные классы
AbstractProductA
иAbstractProductB
определяют интерфейс для классов, объекты которых будут создаваться в программе. - Конкретные классы
ProductA1
/ProductA2
иProductB1
/ProductB2
представляют конкретную реализацию абстрактных классов - Абстрактный класс фабрики
AbstractFactory
определяет методы для создания объектов. Причем методы возвращают абстрактные продукты, а не их конкретные реализации. - Конкретные классы фабрик
ConcreteFactory1
иConcreteFactory2
реализуют абстрактные методы базового класса и непосредственно определяют какие конкретные продукты использовать - Класс клиента
Client
использует класс фабрики для создания объектов. При этом он использует исключительно абстрактный класс фабрикиAbstractFactory
и абстрактные классы продуктовAbstractProductA
иAbstractProductB
и никак не зависит от их конкретных реализаций
- Интерфейс
AbstractFactory
сложно сделать универсальным (под разные иерархии).
Суть паттерна практически полностью описывается его названием. Когда вам требуется получать какие-то объекты, например, пакеты сока, вам совершенно не нужно знать, как их делают на фабрике. Вы просто говорите «сделайте мне пакет апельсинового сока», а «фабрика» возвращает вам требуемый пакет. Как? Всё это решает сама фабрика, например, «копирует» уже существующий эталон. Основное предназначение «фабрики» в том, чтобы можно было при необходимости изменять процесс «появления» пакета сока, а самому потребителю ничего об этом не нужно было сообщать, чтобы он запрашивал его, как и прежде.
Если наш объект сложный, то, возможно, его нужно будет создавать по частям (как в 1 лабе мы последовательно собирали наш объект “Фигура” из объектов “Точки” и “Связи”, поэтапно считывая их из текстового файла). Тогда можно в один отдельный класс свести все этапы создания какого-то объекта.
А кто будет выполнять все эти этапы? В каком порядке?
А кто будет готовить данные для этих этапов?
А кто будет контролировать процесс создания объектов?
Директор.
-
Product
: представляет объект, который должен быть создан. В данном случае все части объекта заключены в спискеparts
. -
Builder
: определяет интерфейс для создания различных частей объектаProduct
-
ConcreteBuilder
: конкретная реализацияBuilder
'a. Создает объектProduct
и определяет интерфейс для доступа к нему -
Director
: распорядитель - создает объект, используя объектыBuilder
Мы унифицируем процесс создания объектов за счёт директора, которого мы (клиент) просим создать объект и вернуть его. А как это будет происходить, волнует уже директора.
"Разделяй и властвуй. И думай всегда о главном."
Тассов К. Л.
Данный паттерн очень тесно переплетается с паттерном «фабричный метод». Основное различие заключается в том, что «строитель» внутри себя, как правило, содержит все сложные операции по созданию объекта (пакета сока). Вы говорите «хочу сока», а строитель запускает уже целую цепочку различных операций (создание пакета, печать на нем изображений, заправка в него сока, учет того сколько пакетов было создано и т.п.). Если вам потребуется другой сок, например ананасовый, вы точно также говорите только то, что вам нужно, а «строитель» уже позаботится обо всем остальном (какие-то процессы повторит, какие-то сделает заново и т.п.). В свою очередь процессы в «строителе» можно легко менять (например, изменить рисунок на упаковке), однако потребителю сока этого знать не требуется, он также будет легко получать требуемый ему пакет сока по тому же запросу.
Необходимо сделать возможным взаимодействие классов, интерфейсы которых несовместимы.
Часто в новом программном проекте не удается повторно использовать уже существующий код. Например, имеющиеся классы могут обладать нужной функциональностью, но иметь при этом несовместимые интерфейсы. В таких случаях следует использовать паттерн Adapter (адаптер).
Паттерн Adapter, представляющий собой программную обертку над существующими классами, преобразует их интерфейсы к виду, пригодному для последующего использования.
Рассмотрим простой пример, когда следует применять паттерн Adapter. Пусть мы разрабатываем систему климат-контроля, предназначенной для автоматического поддержания температуры окружающего пространства в заданных пределах. Важным компонентом такой системы является температурный датчик, с помощью которого измеряют температуру окружающей среды для последующего анализа. Для этого датчика уже имеется готовое программное обеспечение от сторонних разработчиков, представляющее собой некоторый класс с соответствующим интерфейсом. Однако использовать этот класс непосредственно не удастся, так как показания датчика снимаются в градусах Фаренгейта. Нужен адаптер, преобразующий температуру в шкалу Цельсия.
Контейнеры queue
, priority_queue
и stack
библиотеки стандартных шаблонов STL реализованы на базе последовательных контейнеров list
, deque
и vector
, адаптируя их интерфейсы к нужному виду. Именно поэтому эти контейнеры называют контейнерами-адаптерами.
Пусть класс, интерфейс которого нужно адаптировать к нужному виду, имеет имя Adaptee
. Для решения задачи преобразования его интерфейса паттерн Adapter
вводит следующую иерархию классов:
- Виртуальный базовый класс
Target
. Здесь объявляется пользовательский интерфейс подходящего вида. Только этот интерфейс доступен для пользователя. - Производный класс
Adapter
, реализующий интерфейсTarget
. В этом классе также имеется указатель или ссылка на экземплярAdaptee
. ПаттернAdapter
использует этот указатель для перенаправления клиентских вызовов вAdaptee
. Так как интерфейсыAdaptee
иTarget
несовместимы между собой, то эти вызовы обычно требуют преобразования.
-
Target
: представляет объекты, которые используются клиентом -
Client
: использует объектыTarget
для реализации своих задач -
Adaptee
: представляет адаптируемый класс, который мы хотели бы использовать у клиента вместо объектовTarget
-
Adapter
: собственно адаптер, который позволяет работать с объектамиAdaptee
как с объектамиTarget
. То есть клиент ничего не знает обAdaptee
, он знает и использует только объектыTarget
. И благодаря адаптеру мы можем на клиенте использовать объектыAdaptee
какTarget
- Адаптер позволяет разделить роли (интерфейсы) одного объекта
- Решается проблема подмены интерфейса (например, можно подменить библиотеку, с которой мы взаимодействуем через адаптер)
- Можно использовать адаптер для расширения интерфейса класса без использования расширения при наследовании (использовать только в крайних случаях, т. к. надо приводить тип от базового класса к производному, используя
dynamic_cast
) - Можно использовать старый код (из других программ), адаптируя его нашим новым интерфейсам
- Интерфейсы, которые предоставляют адаптеры, могут пересекаться ==> может возникнуть дублирование кода
Данный паттерн полностью соответствует своему названию. Чтобы заставить работать «советскую» вилку через евро-розетку требуется переходник. Именно это и делает «адаптер», служит промежуточным объектом между двумя другими, которые не могут работать напрямую друг с другом.
Клиенты хотят получить упрощенный интерфейс к общей функциональности сложной подсистемы.
- Паттерн Facade предоставляет унифицированный интерфейс вместо набора интерфейсов некоторой подсистемы. Facade определяет интерфейс более высокого уровня, упрощающий использование подсистемы.
- Паттерн Facade "обертывает" сложную подсистему более простым интерфейсом.
Паттерн Facade инкапсулирует сложную подсистему в единственный интерфейсный объект. Это позволяет сократить время изучения подсистемы, а также способствует уменьшению степени связанности между подсистемой и потенциально большим количеством клиентов. С другой стороны, если фасад является единственной точкой доступа к подсистеме, то он будет ограничивать возможности, которые могут понадобиться "продвинутым" пользователям.
Объект Facade, реализующий функции посредника, должен оставаться довольно простым и не быть всезнающим "оракулом".
Клиенты общаются с подсистемой через Facade. При получении запроса от клиента объект Facade переадресует его нужному компоненту подсистемы. Для клиентов компоненты подсистемы остаются "тайной, покрытой мраком".
Другая диаграмма:
- Классы
SubsystemA
,SubsystemB
,SubsystemC
и т.д. являются компонентами сложной подсистемы, с которыми должен взаимодействовать клиент -
Client
взаимодействует с компонентами подсистемы -
Facade
- непосредственно фасад, который предоставляет интерфейс клиенту для работы с компонентами
Паттерн «фасад» используется для того, чтобы делать сложные вещи простыми. Возьмем для примера автомобиль. Представьте, если бы управление автомобилем происходило немного по-другому: нажать одну кнопку чтобы подать питание с аккумулятора, другую чтобы подать питание на инжектор, третью чтобы включить генератор, четвертую чтобы зажечь ламочку на панели и так далее. Всё это было бы очень сложно. Для этого такие сложные наборы действий заменяются более простыми и комплексные как «повернуть ключ зажигания». В данном случае поворот ключа зажигания и будет тем самым «фасадом» для всего обилия внутренних действий автомобиля.
А ещё тут, по-моему, очень напрашивается аналогия с красивым фасадом дома...
Унификация взаимодействия пользователя с группой схожих (но не одинаковых) объектов (разных классов). Эту проблему решает обычное наследование, но оно нам не поможет в том случае, если наши объекты имеют сложную структуру.
- Необходимо объединять группы схожих объектов и управлять ими.
- Объекты могут быть как примитивными (элементарными), так и составными (сложными). Составной объект может включать в себя коллекции других объектов, образуя сложные древовидные структуры. Пример: директория файловой системы состоит из элементов, каждый из которых также может быть директорией.
- Код клиента работает с примитивными и составными объектами единообразно.
Как управлять группами объектов, которые, в свою очередь, могут содержать собственные объекты?
Паттерн Composite предлагает следующее решение. Он вводит абстрактный базовый класс Component
с поведением (метод operation()
), общим для всех примитивных и составных объектов. Подклассы Primitive
и Composite
являются производными от класса Component
. Составной объект Composite
хранит компоненты-потомки абстрактного типа Component
, каждый из которых может быть также Composite
.
-
Component
: определяет интерфейс для всех компонентов в древовидной структуре -
Composite
: представляет компонент, который может содержать другие компоненты и реализует механизм для их добавления и удаления -
Primitive
: представляет отдельный компонент, который не может содержать другие компоненты -
Client
: клиент, который использует компоненты
Суть паттерна заключается в минимизации различий в управлении как группами объектов, так и индивидуальными объектами. Для примера можно рассмотреть управление солдатами в строю. Существует строевой устав, который определяет, как управлять строем и согласно этому уставу абсолютно не важно кому отдается приказ (например «шагом марш») одному солдату или целому взводу. Соответственно в устав (если его в чистом виде считать паттерном «компоновщик») нельзя включить команду, которую может исполнить только один солдат, но не может исполнить группа, или наоборот.
Унификация обработки событий (запросов) в какой-либо системе.
- Система управляется событиями. При появлении такого события (запроса) необходимо выполнить определенную последовательность действий.
- Необходимо параметризировать объекты выполняемым действием, ставить запросы в очередь или поддерживать операции отмены (undo) и повтора (redo) действий.
- Нужен объектно-ориентированный аналог функции обратного вызова в процедурном программировании.
Пример событийно-управляемой системы – приложение с пользовательским интерфейсом. При выборе некоторого пункта меню пользователем вырабатывается запрос на выполнение определенного действия (например, открытия файла).
Паттерн Command преобразовывает запрос на выполнение действия в отдельный объект-команду. Такая инкапсуляция позволяет передавать эти действия другим функциям и объектам в качестве параметра, приказывая им выполнить запрошенную операцию. Команда – это объект, поэтому над ней допустимы любые операции, что и над объектом.
Интерфейс командного объекта определяется абстрактным базовым классом Command
и в самом простом случае имеет единственный метод execute()
. Производные классы определяют получателя запроса (указатель на объект-получатель) и необходимую для выполнения операцию (метод этого объекта). Метод execute()
подклассов Command
просто вызывает нужную операцию получателя.
В паттерне Command
может быть до трех участников:
- Клиент, создающий экземпляр командного объекта.
- Инициатор запроса, использующий командный объект.
- Получатель запроса.
Сначала клиент создает объект ConcreteCommand
, конфигурируя его получателем запроса. Этот объект также доступен инициатору. Инициатор использует его при отправке запроса, вызывая метод execute()
. Этот алгоритм напоминает работу функции обратного вызова в процедурном программировании – функция регистрируется, чтобы быть вызванной позднее.
Паттерн Command
отделяет объект, инициирующий операцию, от объекта, который знает, как ее выполнить. Единственное, что должен знать инициатор, это как отправить команду. Это придает системе гибкость: позволяет осуществлять динамическую замену команд, использовать сложные составные команды, осуществлять отмену операций.
-
Invoker
: инициатор команды - вызывает команду для выполнения определенного запроса -
Receiver
: получатель команды. Определяет действия, которые должны выполняться в результате запроса. -
Command
: интерфейс, представляющий команду. Обычно определяет методExecute()
для выполнения действия, а также нередко включает методUndo()
, реализация которого должна заключаться в отмене действия команды -
ConcreteCommand
: конкретная реализация команды, реализует методExecute()
, в котором вызывается определенный метод, определенный в классеReceiver
Паттерн «команда» очень похож в реальной жизни на кнопки выключателей света в наших квартирах и домах. Каждый выключатель по своей сути делает одно простое действие — разъединяет или соединяет два провода, однако что стоит за этими проводами выключателю не известно. Что подключат, то и произойдет. Точно также действует и паттерн «команда». Он лишь определяет общие правила для объектов (устройств), в виде соединения двух проводов для выполнения команды, а что именно будет выполнено уже определяет само устройство (объект).
Таким образом мы можем включать одним типом выключателей как свет в комнате, так и пылесос.
Различные и несвязанные операции должны выполняться над узловыми объектами некоторой совокупной структуры. Вы хотите избежать "загрязнения" классов этих узлов такими операциями (то есть избежать добавления соответствующих методов в эти классы). И вы не хотите запрашивать тип каждого узла и осуществлять приведение указателя к правильному типу, прежде чем выполнить нужную операцию. Тассов будет бить за приведение типа!
- Когда имеется много объектов разнородных классов с разными интерфейсами, и требуется выполнить ряд операций над каждым из этих объектов
- Когда классам необходимо добавить одинаковый набор операций без изменения этих классов
- Когда часто добавляются новые операции к классам, при этом общая структура классов стабильна и практически не изменяется
-
Visitor
: интерфейс посетителя, который определяет методVisit()
для каждого объектаElement
-
ConcreteVisitor1
/ConcreteVisitor2
: конкретные классы посетителей, реализуют интерфейс, определенный вVisitor
. - Element: определяет метод
Accept()
, в котором в качестве параметра принимается объектVisitor
-
ElementA
/ElementB
: конкретные элементы, которые реализуют методAccept()
-
ObjectStructure
: некоторая структура, которая хранит объектыElement
и предоставляет к ним доступ. Это могут быть и простые списки, и сложные составные структуры в виде деревьев
Сущность работы паттерна состоит в том, что вначале создает объект посетителя, который обходит или посещает все элементы в структуре ObjectStructure
, у которой вызывается метод Accept()
:
(да-да, я своровал примеры на C#, это неважно!!!!!)
public void Accept(Visitor visitor)
{
foreach (Element element in elements)
element.Accept(visitor);
}
При посещении каждого элемента посещаемый элемент вызывает у посетителя метод, соответствующий данному элементу:
public override void Accept(Visitor visitor)
{
visitor.VisitElementA(this);
}
В этот метод элемент передает ссылку на себя, чтобы посетитель мог получить доступ к состоянию элемента. А в самом посетителе уже могут вызываться методы элемента или производиться различные действия над элементом:
public override void VisitElementA(ElementA elementA)
{
elementA.OperationA();
}
Данная техника еще называется двойной диспетчеризацией (double dispatch), когда выполнение операции зависит от имени запроса и двух типов получателей (объект Visitor
и объект Element
).
- Совокупная структура объектов Elements может определяться с помощью паттерна Composite (но не обязательно должна!)
- Для обхода Composite может использоваться Iterator.
- Паттерн Visitor демонстрирует классический прием восстановления информации о потерянных типах, не прибегая к понижающему приведению типов (dynamic cast).
Данный паттерн можно сравнить с прохождением обследования в больнице. Однако «посетителем» в терминах паттернов здесь будут сами врачи. Чтобы было понятнее: у нас есть больной, которого требуется обследовать и полечить, но так как за разные обследования отвечают разные врачи, то мы просто присылаем к больному врачей в качестве «посетителей». Правило взаимодействия для больного очень простое «пригласите врача (посетителя) чтобы он сделал свою работу», а врач («посетитель») приходит, обследует и делает всё необходимое. Таким образом, следуя простым правилам, можно использовать врачей для разных больных по одним и тем же алгоритмам. Как уже было сказано, паттерном «посетитель» в данном случае является врач, который может одинаково обслуживать разные объекты (больных) если его позовут.
-
Factory method (фабричный метод): для единообразной генерации менеджеров и предотвращения повторного создания сущностей (“Creator - шикарная альтернатива синглтону”). Классы: иерархия
...ManagerCreator
(маскируют синглтоны), иерархия...ModeratorCreator
. -
Abstract factory (абстрактная фабрика): фабрика рисования (обёртка над библиотекой). Классы:
Drawerfactory
,QtFactory
,BaseDrawer
,QtDrawer
. -
Builder (строитель): для генерации каркасной модели: сначала строим точки, потом рёбра. Классы: Директоры (классы иерархии
BaseDirector
) руководят строителями (классами иерархииBaseBuilder
).
В Порождающих паттернах должны быть сущности (Solution
в случае фабричного метода или абстрактной фабрики или Director в случае строителя), которые принимают решения, какой Creator
создать или как работать с Builder
.
-
Adapter (адаптер): для расширения интерфейса (легального, Тассов не побьёт) модели. Конкретнее: мы адаптируем модель (и композит) к
DrawManager
иTransformManager
, чтобы они могли с ней работать. Классы: иерархииCompositeAdapter
иModelAdapter
. -
Facade (фасад) - для упрощения более сложного интерфейса. Класс
Facade
объединяет классыSceneManager
,TransformManager
,LoadManager
иDrawManager
. -
Composite (компоновщик) - для объединения нескольких объектов в группы. Классы:
Composite
,VisibleObject
,InvisibleObject
и их производные. -
Command (команда) - для единообразного взаимодействия с системой. Классы: иерархия
BaseCommand
. -
Visitor (посетитель) - для добавления нового функционала объектам, а также для отрисовки и трансформации. Классы:
Visitor
,DrawVisitor
“посещает” структуруComposite
(т.е. всё, что завёрнуто в паттернComposite
).
Тассов сказал, что если мы добавим в композит новый объект (например, источник света вдобавок к модели и камере), то визитёра придётся менять, добавляя метод visit()
для источника света. А мы не должны менять уже написанный код! Было предложено 2 выхода:
- Сделать визитора с переменным числом параметров (аналогичный есть в
STL
, но мы должны сделать его своими руками) - Наплодить кучу адаптеров. Именно наплодить, потому что вместо 3 классов в случае визитора (
Visitor
,DrawVisitor
иTransformVisitor
) мы делаем 6 (см. диаграмму).