Удачность внутренней структуры программного продукта, если оставить в стороне его сложность, напрямую зависит от опытности создавших ее людей. То же можно сказать и о качестве программного кода; но если стиль программирования является сугубо индивидуальным, то в области структурных решений опытные разработчики довольно часто приходят к схожим решениям.
Для таких решений характерны:
Попытки найти и систематизировать наиболее удачные решения велись давно, и не только в области разработки программного обеспечения. В 70-х годах XX века архитектор Кристофер Александер (Christopher Alexander) написал серию книг, посвященных проектированию архитектурных сооружений. Одним из центральных в его теории было понятие шаблона.
Из сферы архитектуры этот термин перешел в проектирование программных продуктов, фактически дав имя тем гибким, эффективным и универсальным решениям, о которых шла речь выше. Кроме того, использование данного понятия в новой области добавило еще одно требование, необходимое для отнесения программного решения в разряд шаблонов,? требование "безымянного качества", несмотря на всю его расплывчатость. Структура шаблона не должна производить впечатления искусственности или избыточности; более того, его изучение и применение способно доставлять эстетическое удовольствие - как красиво и правильно построенный дом.
В то время еще отсутствовала четкая систематизация существующих шаблонов и перечень предъявляемых к ним требований, поэтому каждый разработчик использовал собственные конструкции, имена и обозначения. Такая систематизация появилась только в 1995 году, с выходом книги "Design Patterns: Elements of Reusable Object-Oriented Software" (в дальнейшем - просто "Design patterns" или "DP"). Это основополагающая книга по шаблонам проектирования и самая известная из всех изданных по этой тематике. Ее авторов часто называют "бандой четырех" ("Gang of Four", "GoF"). В своей книге они не только привели подробное описание 23-х шаблонов, но и дали формальное определение термину "шаблон проектирования" в сфере разработки программного обеспечения.
Согласно этому определению, шаблон проектирования представляет собой "описание взаимодействия объектов и классов, адаптированных для решения общей задачи проектирования в конкретном контексте" DP. Иными словами, шаблон - это описание структуры классов, которая может быть использована для решения задач определенного рода. Характерными чертами такого решения являются эффективность и гибкость.
Шаблон проектирования, согласно DP, состоит из четырех основных элементов:
В DP было включено описание 23-х базовых шаблонов. По назначению они объединяются в группы:
Группа | Шаблоны |
---|---|
Создающие шаблоны (Creational patterns) | Фабричный метод, Абстрактная фабрика, Одиночка, Прототип, Строитель |
Структурные шаблоны (Structural patterns) | Адаптер, Декоратор, Заместитель, Компоновщик, Мост, Приспособленец, Фасад |
Шаблоны поведения (Behavioural patterns) | Интерпретатор, Шаблонный метод, Итератор, Команда, Наблюдатель, Посетитель, Посредник, Состояние, Стратегия, Хранитель, Цепочка обязанностей |
Краткое описание шаблонов:
Мост (Bridge) - отделяет абстракцию от реализации, благодаря чему появляется возможность независимо изменять то и другое.
Итератор (Iterator) - дает возможно последовательно обойти все элементы составного объекта, не раскрывая его внутреннего представления.
Команда (Command) - инкапсулирует запрос в виде объекта, позволяя параметризовывать клиентов типом запроса, устанавливать очередность запросов, протоколировать их и поддерживать отмену выполнения операций.
Наблюдатель (Observer) - определяет между объектами зависимость типа один-ко-многим, так что при изменении состояния одного объекта все зависящие от него получают уведомление и автоматически обновляются.
Абстрактная фабрика (Abstract factory) - создает один или несколько объектов, принадлежащих заданным семействам.
Строитель (Builder) - отделяет конструирование сложного объекта от его представления, позволяя использовать один и тот же процесс конструирования для создания различных представлений.
Компоновщик (Composite) - группирует объекты в древовидные структуры для представления иерархий типа "часть-целое". Позволяет клиентам работать с единичными объектами так же, как с группами объектов.
Интерпретатор (Interpreter) - для заданного языка определяет представление его грамматики, а также интерпретатор предложений языка, использующий это представление.
Посетитель (Visitor) - представляет операцию, которую нужно выполнить над элементами объекта. Позволяет определить новую операцию, не меняя не меняя классы элементов, к которым она применяется.
Прототип (Prototype) - описывает виды создаваемых объектов с помощью прототипа и создает новые путем его копирования.
Заместитель (Proxy) - подменяет другой объект для контроля доступа к нему.
Приспособленец (Flyweight) - использует разделение для эффективной поддержки большого числа мелких объектов.
Стратегия (Strategy) - определяет семейство алгоритмов, инкапсулирует их и делает взаимозаменяемыми.
Фабричный метод (Factory method) - предоставляет интерфейс для создания объекта, реализуя само создание в производных классах.
Одиночка (Singleton) - гарантирует создание не более чем одного экземпляра класса и предоставляет глобальный доступ к этому экземпляру.
Адаптер (Adapter) - преобразует интерфейс класса к нужному клиенту виду.
Фасад (Facade) - предоставляет объединенный доступ к набору интерфейсов подсистемы.
Состояние (State) - позволяет объекту изменять свое поведение при смене внутреннего состояния.
Декоратор (Decorator) - позволяет динамически добавлять объекту функциональность.
Шаблонный метод (Template method) - определяет структуру алгоритма, возлагая обязанности по выполнению некоторых его шагов на подклассы.
Цепочка обязанностей (Chain of responsibility) - отделяет отправителя запроса от получателя, позволяя нескольким объектам обработать запрос.
Посредник (Mediator) - определяет объект, хранящий информацию о способах взаимодействия объектов.
Хранитель (Memento) - позволяет, не нарушая инкапсуляции, получить и сохранить во внешней памяти внутреннее состояние объекта.
От чисто ознакомительного описания шаблонов перейдем к более детальному анализу некоторых из них, использованных докладчиком при проектировании отдельных программных продуктов разного назначения.
Основное назначение данного программного продукта - обеспечение единообразного доступа к файлам, расположенным на локальном компьютере или на http-сервере (точнее говоря, доступ по протоколам file и http). Отсюда очевидна необходимость использования двух разных алгоритмов обращения к файлу; кроме того, к библиотеке предъявляются требования гибкости и расширяемости, так что добавление новых алгоритмов или замена существующих должны осуществляться как можно проще.
Реализованное решение можно назвать "фабрикой". Причиной выбора такого названия служит близость данного решения к шаблонам "Фабричный метод" и "Абстрактная фабрика". Однако реализация этих шаблонов обычно предполагает наличие задающего интерфейс абстрактного класса и его потомков, реализующих механизмы принятия решения о классе создаваемого объекта или группы объектов, в то время как включенный в состав библиотеки класс не имеет чисто виртуальных методов. Библиотечная фабрика содержит алгоритм принятия решения, используемый "по умолчанию" и определенный в виртуальном методе; для создания нового алгоритма, таким образом, нужно создать потомок фабрики, в котором этот метод будет переопределен.
Такое решение, включающее реализацию "по умолчанию", не входит в состав базовых 23 шаблонов, описанных в (хотя его и можно считать модификацией шаблона "Абстрактная фабрика"), но тем не менее довольно распространено и получило условное название "Фабрика классов" ("Class factory"). Условно оно потому, что "фабрикой классов", в зависимости от источника, называют самые разные конструкции: каждую из реализаций базового класса в шаблоне "Абстрактная фабрика" и даже сам этот шаблон; решение, которое заменяет экземпляр класса его интерфейсом, а создающий класс помещает в класс-"одиночку" (так поступает используемая в C#.NET фабрика классов); модель, в которой нужные объекты создаются статическим методом их же базового класса. Сказывается отсутствие общепризнанного систематизирующего ресурса, подобного DP.
На рисунке изображена упрощенная диаграмма классов библиотеки. Их назначение:
В число требований к данной системе входят наличие не менее двух типов контролируемых приложений (обычные исполнимые файлы и сервисы WinNT-подобных систем) с единым интерфейсом управления (гибкость и расширяемость можно не упоминать - они если не обязательны, то, во всяком случае, желательны для информационных систем вообще и для всех описываемых в частности). Эти требования, если отвлечься от назначения приложения, практически совпадают с требованиями к библиотеке работы с файлами, описанной в предыдущем разделе. Сходство требований позволяет предположить возможность использования схожих решений: создания базового абстрактного класса, содержащего набор зависящих от конкретного типа приложения операций, и нескольких его потомков, соответствующих возможным типам приложений и по-разному эти операции реализующих.
При реализации был создан управляющий класс, отвечающий одному из дополнительных требований: система должна работать как сервис или как обычное консольное приложение, в зависимости от выбора пользователя. Разработанный ранее проект позволяет легко создавать такие программы, но требует глобального доступа к работающему экземпляру контролирующего приложения класса. Обеспечить такой доступ позволяет шаблон "Одиночка".
Следующий рисунок иллюстрирует итоговую структуру системы:
Назначение классов на рисунке:
MachineCore - базовый класс. Он определяет набор и описание зависящих от типа приложения методов управления,
ProcessCore и ServiceCore - реализуют эти методы в отношении приложений, представленных только исполнимых файлом и сервисом соответственно,
Machine - управляет приложением независимо от его типа (точнее, проверяет тип приложения только один раз - при инициализации),
MachineSet - управляет произвольным числом приложений и поддерживает систему зависимостей между ними,
Storage - реализация шаблона "Одиночка", создает объект класса MachineSet и обеспечивает глобальный доступ к нему.
Проектируемая система должна обеспечивать простой и удобный доступ к ресурсам распознавания речи, предоставляемым продуктом SpeechPearl. Необходимость ее создания обусловлена сложностью использования этих ресурсов - собственный программный интерфейс SpeechPearl включает в себя несколько десятков функций. Полное программное руководство по ним занимает более сотни страниц, не считая примеров (среди которых нет ни одного, иллюстрирующего работу в режиме "клиент-сервер").
Еще одна причина разработки интерфейса - в наличии среди стандартных функций SpeechPearl синхронных функций. Синхронные функции на время своего выполнения блокируют работу программы и при интеграции в существующие системы могут снизить их производительность. Таким образом, нужно преобразовать те синхронные функции, выполнение которых может занять значительный промежуток времени, в асинхронные - функции, которые выполняются независимо от основной программы и лишь сообщают ей о своем завершении.
Упростить для пользователя процесс распознавания речи позволяет применение шаблона "Фасад", скрывающего сложное внутреннее устройство системы за простым интерфейсом.
Изменение типа функций требует более сложного решения. В результате анализа были определены три функции, которые вызываются неоднократно и требуют довольно значительного времени для выполнения. Все они различаются по назначению и набору входных и выходных параметров; кроме того, не исключена возможность добавления новых функций при расширении функциональности системы - например, для поддержки режима обучения. С учетом всего вышеперечисленного разумным представляется помещение вызова каждой функции в отдельный класс и наделение этих классов одинаковым интерфейсом. Для конкретной реализации этого решения был применен шаблон проектирования "Шаблонный метод" ("Template method").
Еще один класс предназначен для хранения данных, к которым необходимо обеспечить глобальный доступ. Последний, как и в предыдущем решении, обеспечивает использование шаблона "Одиночка".
Структура системы в целом представлена на следующем рисунке:
Базовые классы:
Отдельно изображена система SpeechPearl с ее собственным программным интерфейсом.
К числу важнейших характеристик при проектировании продукта были отнесены минимальная зависимость (а в идеале - полная независимость) от типа используемых телефонных плат (на тот момент - "Dialogic" или "Ольха") и протоколов передачи данных. В результате была разпаботана структура классов, обеспечивающих эту независимость, и, для тестовых целей, конкретная реализация для программного продукта Dialogic HMP.
В реализации управляющих классов активно используются шаблоны "Одиночка". Они контролируют телефонные каналы, голосовые и медиаресурсы.
Контроль работы телефонного канала обеспечивает набор классов, описывающих машину состояний. В реализации последней были использованы такие шаблоны, как "Состояние" и "Прототип": первый позволяет менять поведение динамически, а второй - создавать экземпляр объекта, класс которого заранее неизвестен. Механизм связи вспомогательных ресурсов с телефонным каналом включает в себя реализацию шаблона "Шаблонный метод".
Основные классы системы:
Теперь, когда некоторое представление о сущности и применении шаблонов проектирования дано, стоит сделать несколько замечаний: