5.B Паттерны в программировании (50 / 50)
Фортран: Дорогие друзья, сегодня я хотел бы затронуть тему паттернов (шаблонов) проектирования программного обеспечения. Для этого стоит вернуться в те далекие времена, когда Профессор Фортран был обычной программой бухгалтерского учета и не бороздил по просторам Интернета. Дело было в 1995 году, когда так называемая «банда четырех» в составе Р. Хелма, Э. Гамма, Д. Влиссидеса и Р. Джонсона написали книгу «Приемы объектно-ориентированного проектирования. Паттерны проектирования». В этой книге они показали эволюцию шаблонов (далее я специально буду чередовать термины шаблон и паттерн, которые являются синонимами, чтобы у читателя отложилось в памяти, что это одно и тоже) проектирования на протяжении всей истории развития отрасли программного обеспечения на тот момент. Эта книга утверждает, что здравомыслие является наиболее важным аспектом создания любого паттерна. Таким образом, выработка новых паттернов возможна только при условии, что программисты при проектировании крупных программных систем будут документировать не только свои успехи в этом направлении, но и свои неудачи. Программисты применяют шаблоны проектирования, чтобы, используя накопленный коллективный опыт, создавать свои программы на основе самых удачных решений.
В книге «банды четырех» описаны 23 паттерна, каждый из которых представляет решение одной из типичных задач разработки крупных программных систем. Все эти паттерны поделены на 3 большие группы: поведенческие, порождающие и структурные.
Поведенческие | Порождающие | Структурные | |
---|---|---|---|
Command | Factory Method | Proxy | |
Interpreter | Singleton | Adapter | |
Memento | Abstract Factory | Decorator | |
Mediator | Prototype | Facade | |
Chain-of- Responsibility | Builder | Composite | |
Iterator | Bridge | ||
Observer | Flyweight | ||
Template Method | |||
State | |||
Visitor | |||
Strategy |
Небольшой доклад по каждой из этих категорий я попросил подготовить моих учеников Икса, Гусеницу и Воробья. Друзья, изложите, пожалуйста, ваши наблюдения по этому поводу.
Икс: Я взял на себя смелость рассказать о поведенческих паттернах или, как их еще называют, паттернах поведения. Подобные паттерны назначают объектам задачи и представляют рабочие стратегии для организации взаимодействия между объектами. Например, шаблон проектирования наблюдатель (Observer
), он же – слушатель (Listener
), является типичным примером поведенческого паттерна. Покажем его применение на примере взаимодействия компонентов графического пользовательского интерфейса (GUI) со своими наблюдателями, которые реагируют на нажатия кнопок и другие действия пользователей. Наблюдатели регистрируются в качестве обработчиков событий графических компонентов, для того чтобы отслеживать изменения в их состоянии. Когда мы взаимодействуем с компонентом, он уведомляет своих наблюдателей (слушателей), что его состояние изменилось (например, о том, что мы нажали кнопку).
Воробей: Очень интересно, Икс, но я хочу рассказать о порождающих паттернах, которые описывают способы создания экземпляров классов. Подобные паттерны выполняют задачи по созданию, т.е. порождению, новых объектов. Например, паттерн Factory Method
принимает решение, экземпляр какого класса создавать, только в момент выполнения, а паттерн Singleton
не позволяет создавать более 1 экземпляра класса. Представим, что мы пишем программу для создания 3D изображений, в которой можно будет рисовать различные виды 3-хмерных объектов, такие как цилиндры, сферы, тетраэдры и кубы. На стадии компилирования программа не знает заранее, какие именно фигуры пользователь захочет рисовать в этой программе. Только во время нашего рисования, программа сможет узнать, экземпляр какого класса нужно создать. Если мы решили нарисовать куб, то программа должна понять, что нужно создать экземпляр класса Cube
и добавить его в изображение на экране. Только когда пользователь выбирает, какой именно геометрический объект он хочет нарисовать, программа может определить конкретный класс, экземпляр которого нужно создать.
Гусеница: Ну, а мне, профессор, достались структурные паттерны, которые описывают подходы к организации классов в программной системе. Программисты регулярно сталкиваются с 2 проблемами плохой организации классов:
- Первая связана с тем, что классам поручается очень много задач (в соответствии с антипаттерном
God Class
, т.е. класс-бог или всезнающий класс). Подобные классы нарушают принцип инкапсуляции, поскольку каждый такой класс получает доступ к той информации, которая могла быть вынесена в отдельный класс. - Вторая проблема состоит в возможном пересечении задач, возложенных на разные классы. Это отнимает время у программистов, т.к. им приходится создавать код, который уже написан в другом классе и мог бы быть вынесен в отдельный класс для общего использования. В программировании такой подход сравнивается с изобретением велосипеда.
Структурные паттерны помогают программистам избегать таких проблем.
Фортран: Спасибо, друзья. Шаблоны проектирования описываются в программном коде с помощью набора классов. Чтобы использование шаблонов было эффективным, программисты должны иметь представление о наиболее популярных из них. Мы обсудим основные паттерны, а также их роль и значение при построении качественного программного обеспечения. Теперь я бы хотел провести небольшой обзор тех паттернов проектирования, с которыми мы еще встретимся по ходу создания наших примеров кода, и с которыми, вероятно, не столкнемся и поэтому давайте хотя бы поверхностно их рассмотрим. В таблице ниже я привел те паттерны, с которыми я предполагаю, что нам придется поработать согласно той программе обучения, которая есть в моем электронном мозгу ;)
Поведенческие | Порождающие | Структурные | |
---|---|---|---|
Command | Factory Method | Adapter | |
Observer | Singleton | Decorator | |
Template Method | |||
State |
Мы будем использовать только те шаблоны проектирования, которые будут подходить для решения конкретных задач, и мы будем обсуждать их по мере написания примеров кода. Ниже мы рассмотрим другие популярные паттерны, описанные в книге «банды четырех», и которые могут оказаться полезными.
Поговорим о паттерне Strategy
. Ставший уже классическим пакет java.awt
для создания графических интерфейсов пользователя (GUI) предлагает несколько менеджеров компоновки визуальных компонентов. Среди них есть такие классы, как BorderLayout
, FlowLayout
и GridLayout
. Все они реализуют интерфейс LayoutManager
. Каждый менеджер компоновки размещает визуальные компоненты в графическом окне пользователя, однако каждая из упомянутых реализаций интерфейса LayoutManager
использует для этого свой алгоритм. Объект BorderLayout
помещает компоненты в 5-ти различных областях (по 4-ем краям и в центре окна), FlowLayout
размещает компоненты слева направо, a GridLayout
размещает компоненты по строкам и столбцам. Интерфейс LayoutManager
играет роль стратегии в паттерне проектирования Strategy
.
Паттерн Strategy
позволяет программистам создавать набор алгоритмов, который называется стратегией. Каждый из алгоритмов в таком наборе имеет одно и тоже предназначение (например, размещение компонентов GUI, как в примере выше), но различные реализации. Например, интерфейс LayoutManager
– это стратегия, т.е. набор алгоритмов, которые занимаются размещением компонентов в графическом окне. Каждая конкретная реализация LayoutManager
реализует метод addLayoutComponent
для предоставления определенного алгоритма размещения компонентов.
Со стратегиями мы, разумеется, столкнемся при создании GUI и будем использовать вышеупомянутые классы, но самостоятельно создавать свои стратегии нам вряд ли придется. Хотя время покажет.
Ну, а теперь вы, друзья, поделитесь, какие паттерны проектирования знаете?
Икс: Я могу рассказать о шаблоне Iterator
. Программисты постоянно используют различные структуры данных, например, связанные списки, массивы и хэш-таблицы (иногда называемые картами), для организации данных в своей программе. Шаблон Iterator
позволяет осуществлять доступ к отдельным объектам в структуре данных, не имея представления о реализации этой структуры. Инструкции по обработке структуры данных и доступа к ее элементам содержатся в отдельном объекте, который называется iterator
(итератор). Каждая структура данных имеет свою реализацию итератора, способного обрабатывать эту структуру. Каждый итератор реализует стандартный интерфейс, поэтому другие объекты могут его использовать, даже не зная особенностей его реализации. Мы сейчас говорим об интерфейсе Iterator
из пакета java.util
, который использует паттерн Iterator
. Рассмотрим систему, которая содержит структуры данных List
(список) и Set
(множество). Алгоритмы извлечения данных для каждой из этих структур различаются. Применяя шаблон Iterator
, класс каждой структуры данных содержит ссылку на объект класса Iterator
, который в свою очередь хранит информацию относительно обработки конкретной структуры данных. Для объектов, представляющих структуры данных, мы вызываем метод iterator
для получения ссылки на соответствующий объект класса Iterator
. Работая с этим объектом-итератором, мы вызываем метод next
интерфейса Iterator
для получения следующего элемента в структуре данных. При этом нам нет необходимости знать детали реализации этой структуры данных.
Воробей: Я готов поделиться своими знаниями о паттерне Bridge
. Для этого предположим, что мы создаем класс кнопки Button
для двух операционных систем (ОС) Mac OS X и Windows. Этот класс хранит разного рода данные, такие как ссылка на слушателя события нажатия на кнопку и строку с текстом надписи на кнопке. Далее мы создаем дочерние для этого класса классы MacButton
и WindowsButton
, чтобы расширить его функционал при работе на конкретных платформах. Класс MacButton
включает всю необходимую информацию об отображении кнопки в Mac OS X, а класс WindowsButton
хранит ту же информацию, но для отображения в Windows.
При этом возникают 2 проблемы:
- Первая проблема заключается в том, что, если мы будем создавать новые дочерние классы для класса кнопки
Button
, то мы должны будем создавать и соответствующие дочерние классы для каждой из ОС. Например, если мы создадим классImageButton
для кнопки с изображением картинки и при этом унаследуем его от классаButton
, то нам придется создать и дочерние классыMacImageButton
иWindowsImageButton
.
По сути, для каждой из ОС, которые мы захотим поддерживать, нам придется создать по отдельному дочернему классу для классов кнопокButton
иImageButton
. Это увеличивает общее время программирования и усложняет поддержку кода. - Вторая проблема состоит в том, что при желании доработать нашу программу под еще одну ОС мы будем должны создать новые дочерние классы для класса кнопки
Button
специально для этой ОС. Например,LinuxButton
.
Паттерн Bridge
решает обе эти проблемы за счет отделения абстракции (в примере выше, родительский класс кнопки Button
) от реализации (в примере выше, дочерние классы MacButton
и WindowsButton
). Например, классы пакета java.awt
применяют паттерн Bridge
, чтобы дать возможность программистам создавать дочерние классы графических компонентов без необходимости создания соответствующих дочерних классов, специфичных для каждой ОС. В библиотеке java.awt
класс кнопки Button
хранит ссылку на класс ButtonPeer
, являющийся родительским для таких его реализаций, как MacButtonPeer
и Win32ButtonPeer
. Когда мы создаем объект класса Button
, он определяет экземпляр какой из реализаций класса ButtonPeer
создавать. После создания ссылка на этот объект сохраняется в объекте Button
. Ссылка ButtonPeer
в объекте Button
является мостом (bridge
) в соответствии с терминологией шаблона проектирования Bridge
. Когда мы решаем вызвать какой-нибудь метод объекта Button
, объект Button
, в свою очередь, вызывает соответствующий метод в объекте ButtonPeer
. Если программист создает дочерний от класса Button
класс ImageButton
, ему не придется создавать соответствующие классы MacImageButton
и WindowsImageButton
. В силу того, что класс ImageButton
является дочерним классом класса кнопки Button
, то, когда мы вызываем любой метод класса ImageButton
(например, setImage
), этот вызов транслируется в родительский класс Button
и далее в вызов соответствующего метода класса ButtonPeer
(в нашем случае, drawImage
).
Программисты часто применяют паттерн Bridge
, чтобы сделать свои программы более независимыми от операционной системы. Благодаря этому, мы можем создавать дочерние классы для класса кнопки Button
, не задумываясь о конкретной ОС.
Гусеница: А я вот хочу рассказать про паттерн проектирования Memento
. Представим программу для рисования. На случай, если мы неправильно что-то нарисовали, программа имеет функцию отмены, которая позволит нам устранить ошибку. Программа также может восстановить изначальное состояние области рисования, которое существовало на момент запуска программы и до того, как мы начали в ней рисовать. Кроме того, такая программа может иметь журнал, который может хранить список состояний, чтобы мы могли восстановить любое состояние нашего рисунка. Паттерн проектирования Memento
служит этой цели и позволяет объекту сохранить свои состояния для последующего их восстановления.
Применение шаблона Memento
подразумевает наличие 3 видов объектов:
- Объект-хозяин (
Originator
). Он обладает состоянием, т.е. характерным набором атрибутов, на момент, когда программа выполняется. В нашем примере выше объект-хозяин – это область для рисования. Она может менять свое состояние. В начальном состоянии эта область не содержит каких-либо нарисованных на ней фигур. - Объект-хранитель (
Memento object
). Он сохраняет копию всех атрибутов, определяющих состояние объекта-хозяина. Хранители хранятся в журнале. - Журнал, который является объектом механизма отката состояния (
Caretaker
). Журнал представляет собой список из объектов-хранителей, связанных с разными состояниями объекта-хозяина.
Теперь представим, что мы рисуем круг в области для рисования. Объект-хозяин теперь изменяет свое состояние. При этом прежнее его состояние помещается в объект-хранитель, а тот, в свою очередь, в журнал. Этот объект-хранитель является элементом списка в объекте-журнале. Данный список из журнала может быть отображен на экране, поэтому мы можем выбрать, какое из сохраненных состояний мы хотим восстановить. Представим теперь, что мы хотим удалить нарисованный нами круг. При выборе первого состояния из списка, объект-хозяин примет первый объект-хранитель из журнала в качестве своего состояния, а область для рисования визуально примет свое исходное состояние.
Фортран: Спасибо, друзья. Напоследок я хочу рассказать еще об одном паттерне проектирования Prototype
. Иногда программе нужно создать копию объекта, но класс этого объекта заранее неизвестен. В качестве примера рассмотрим все туже программу для рисования, которая поддерживает несколько типов графических фигур (например, фигуры, определяемые классами Circle
, Line
и Box
). Мы должны иметь возможность копировать эти фигуры. Паттерн Prototype
дает нам такую возможность, при этом объект, который позволяет самого себя клонировать, называется прототипом. Если говорить точнее, то прототип – это класс, который реализует специальный интерфейс. Например, в стандартной библиотеке Java API этим интерфейсом является java.lang.Cloneable
в совокупности с методом clone
класса java.lang.Object
. Если класс реализует интерфейс java.lang.Cloneable
, то в нем нужно определить метод clone
. В методе clone
создается копия объекта-прототипа, а ссылка на эту копию возвращается на выходе из метода. В примере с программой для рисования класс Circle
является прототипом. Это означает, что он реализует интерфейс java.lang.Cloneable
и в нем определен метод clone
. Для того чтобы на нашем рисунке появился новый круг, созданный на основе старого, мы должны клонировать объект круга Circle
. Программисты часто применяют метод clone
, чтобы предотвратить изменения данных в исходном объекте. Это достигается за счет того, что метод clone
возвращает ссылку не на оригинал, а на копию объекта-прототипа.
Стоит отметить, что с момента первого издания книги «банды четырех» много утекло воды и появилось много новых паттернов проектирования. Часть из этих паттернов относится к, так называемым, распределенным или параллельным системам. Java, как язык программирования, позволяющий создавать и использовать много программных потоков, предоставляет программистам возможность выполнять параллельные задачи. Ошибки проектирования подобных параллельных систем могут привести к серьезным последствиям. Типичным примером подобной ошибки являются 2 объекта, которые пытаются параллельно поменять один и тот же объект с данными, что может привести к нарушению целостности последнего. Другой пример связан с возникновением тупиковой ситуации или deadlock
, когда 2 объекта ожидают друг друга и поэтому не могут закончить выполнять поставленные перед ними задачи. В связи с этим группа во главе с Дагом Ли разработала специальный набор паттернов для многопоточных архитектур. Набор классов, реализующий такие паттерны, сейчас можно найти в пакете java.util.concurrent
. Данных паттернов достаточно много, поэтому далее я приведу только несколько примеров:
- Паттерн
Balking
гарантирует, что метод будет отменен, а заложенные в нем действия не будут выполнены, при условии, что этот объект пребывает в таком состоянии, которое не позволяет методу отработать корректно. Возможна модификация этого паттерна, когда метод бросает исключение со служебной информацией, описывающей почему этот метод не выполнился. - Паттерн
Two-Phase Termination
описывает 2 фазы уничтожения объекта. На 1-ой фазе происходит освобождение ресурсов, например, уничтожение созданных дочерних потоков, на 2-ой фазе – уничтожение самого объекта. Можно привести пример с объектомThread
, который может применять данный паттерн в методеrun
реализации интерфейсаRunnable
. Представим для этого, что в методеrun
содержится бесконечный цикл, который завершает свое выполнение при наступлении определенных условий. Перед своим завершением методrun
вызывает другой метод, который отвечает за уничтожение дочерних потоков (1-ая фаза), а затем завершается сам (2-ая фаза). - Паттерн
Read/Write Lock
не позволяет двум или более программным потокам одновременно получать доступ к объекту для записи, при этом на операцию чтения объекта это правило не распространяется. Когда какой-то программный поток получает доступ к объекту для записи, для остальных потоков, которые тоже захотят произвести запись, этот объект окажется заблокированным. - Паттерн
Single-Threaded Execution
не позволяет разным программным потокам одновременно вызывать один и тот же метод. Программисты Java применяют с этой целью ключевое словоsynchronized
.
У меня все!
Икс: Профессор, а вы ничего не забыли? Ладно, отдохните, вы уже старенький. Я продолжу и коснусь темы архитектурных паттернов проектирования. Дело в том, что такие паттерны позволяют программистам проектировать отдельные части крупных систем, не касаясь таких мелких задач, как создание экземпляров классов. Архитектурные паттерны – это проверенные временем стратегии создания сложных программных подсистем и обеспечения взаимодействия подобных подсистем между собой.
Рассмотрим архитектурный паттерн Model-View-Controller
(модель-вид-контроллер). Он разделяет данные приложения (Model
), компоненты графического отображения (View
) и логику обработки данных (Controller
). Например, в текстовом редакторе мы набираем текст и форматируем его при помощи мышки. Программа сохраняет этот текст с информацией о его форматировании в специальной структуре данных (Model
) и выводит отформатированный текст на экран (View
). Если пользователь выполняет правки текста, то Controller
изменяет данные модели (Model
) в соответствии с этими правками. При изменении модели она посылает сигнал об этом в вид (View
), чтобы представление могло перерисовать себя на экране.
Фортран: Да, погоди ты, Икс. Я еще не впал в маразм. Я и сам все знаю про архитектурные паттерны. Например, архитектурный паттерн проектирования Layers
разделяет функциональные возможности на отдельные группы (слои или уровни), отвечающие за определенные задачи в системе. Например, 3-хуровневое приложение, в котором каждый уровень состоит из уникального системного компонента, является примером такого архитектурного паттерна. Приложение подобного типа содержит 3 уровня, каждый из которых выполняет уникальную задачу. Информационный уровень (нижний уровень) содержит данные для приложения, обычно храня их в базе данных. Клиентский уровень (верхний уровень) представляет собой пользовательский интерфейс приложения, например, веб-страницу в браузере. Средний уровень выступает для них посредником, беря на себя задачи по обработке запросов из клиентского уровня, и читая/записывая данные из/в базу данных.
Стоит отметить, что и в паттерне Model-View-Controller
, и в паттерне Layers
, и во многих других архитектурных паттернах есть много общего, а именно, 3 сущности: данные (модель/информационный уровень), интерфейс (вид/клиентский уровень) и бизнес-логика (контроллер/средний уровень).
Использование архитектурных паттернов способствует модифицируемости разрабатываемых систем, поскольку программисты могут изменять один компонент архитектуры без необходимости изменять другой компонент. Например, текстовый редактор, который использует паттерн Model-View-Controller
, является модифицируемым. Программисты могут изменить интерфейс (View
), которое отображает структуру документа, не меняя при этом модель и логику. Система, разработанная с использованием паттерна Layers
, также является модифицируемой. Программисты могут модифицировать информационный уровень, чтобы адаптировать программу к определенной базе данных, без необходимости изменять клиентский и средний уровни.
Теперь уж точно все!!!
P.S. Для ПАУК-а:
- Первая половина текста
- Вторая половина текста
- Картинка 1 - уникальна
- Картинка 2 - уникальна
- Картинка 3 - уникальна