|
РАССЫЛКА САЙТА
RSDN.RU |
Здравствуйте!
Практика применения паттернов проектирования Авторы: Беркович Вадим
|
Паттерн | Абстрактная фабрика |
---|---|
Имя в проекте | TModuleFactory |
Задача | Создавать конкретные модули проекта. Скрыть от главной формы все конкретные классы модулей проекта. |
Решение | Скрыть знание о конкретных классах |
Результат | Главная форма не знает конкретных классов модулей, и ее код остается неизменен при добавлении новых модулей в проект. |
На рисунке 3 показана диаграмма классов «скелета» проекта. Главный модуль (TAppConsole) создает множество модулей через абстрактную фабрику TModuleFactory. Кроме этого TAppConsole создает экземпляр TAppConsoleForm – главного окна программы.
Паттерн | Одиночка |
---|---|
Имя в проекте | TappConsole |
Задача | Создать единый класс для построения интерфейса пользователя. |
Решение | Главная форма приложения. Инкапсулирует функции создания модулей системы через абстрактную фабрику и их вызов, содержит логику главной формы. |
Результат | Главная форма не знает конкретных классов модулей, и ее код остается неизменным при добавлении новых модулей в проект. |
Для перечисления всех модулей программы используется перечисление TModuleType. TAppConsole пробегается по значениям этого типа TModuleType = (mtPriceList, mtOrders, mtSupplies) и вызывает фабрику для создания конкретного модуля. Затем запрашивает у модуля имя (с помощью функции GetName) и поддерживаемые операции (GetOperations). На основании этих данных строится двухуровневое дерево tvCommands: TTreeView. Элементы первого уровня - имена модулей, второго – поддерживаемые ими операции. При этом свойству Data элементов дерева назначается ссылка на соответствующий модуль. Вместо дерева можно строить и меню или панель инструментов. Таким образом, реализуется решение, похожее на оснастку Microsoft Management Console, только не в рамках всей ОС, а в конкретном приложении. Код создания экземпляров модулей через фабрику модулей приложения приведен в листинге 1.
Листинг 1 - создание экземпляров модулей через theModuleFactory
{ Создает все модули программы и наполняет ими дерево команд консоли } procedure TAppConsole.CreateModules; var Module: TAppModule; begin ... ... { TModuleType - типы модулей } for i := low(TModuleType) to high(TModuleType) do begin { Создание модуля через фабрику } Module := theModuleFactory.CreateModule(i, theMediator); theMediator.ModuleList.Add(Module); { Добавление модуля в дерево } Node := tvCommands.Items.Add(nil, Module.GetName); with Node do begin Data := Module; sl := TStringList.Create; { Получение списка операций модуля. Добавление их в дерево } Module.GetOperations(sl); for j := 0 to sl.Count-1 do tvCommands.Items.AddChild(Node, sl[j]); sl.Free; end; end; ... end;
Рассмотрим подробнее работу абстрактной фабрики TModuleFactory (листинг 2). Она осведомлена о всех конкретных модулях и способна порождать их экземпляры. Ее единственный метод CreateModule создает модуль заданного типа и назначает ему медиатор. Медиатор используется для связи между модулями - для передачи команд. О его использовании будет сказано ниже.
Листинг 2 – работа фабрики модулей TModuleFactory
{ TModuleType - типы модулей } TModuleType = (mtPriceList, mtOrders, mtSupplies); function TModuleFactory.CreateModule(ModuleType: TModuleType; Mediator: TMediator): TAppModule; var ModuleClass: TModuleClass; begin { выбор класса } case ModuleType of mtPriceList: ModuleClass := TPriceListAppModule; mtOrders: ModuleClass := TOrdersAppModule; mtSupplies: ModuleClass := TSuppliesAppModule; else Assert(false, 'TModuleFactory.CreateModule: ' + 'не определен класс для создания модуля'); end; { создание модуля по выбранному классу } Application.CreateForm(ModuleClass, Result); Result.Mediator := Mediator; end;
Диаграмма последовательности, связывающая приведенные фрагменты кода, приведена на рис. 5. В ней показана последовательность создания конкретного модуля PriceListAppModule.
Таким образом, в рассмотренной схеме главный модуль остается неизменным при добавлении/удалении модулей TAppModule в проекте. Минимальные изменения необходимы только в фабрике модулей. Добавляемый модуль будет автоматически увиден и его интерфейс будет подключен в дерево команд (или главное меню) главной формы. Тем самым достигается эффект слабой связанности (Low Coupling) как между модулями TAppModuleглавным модулем TAppConsole. Тут необходимо подробнее рассказать о базовом классе модуля проекта – TAppModule.
Модули проекта являются носителями бизнес-логики в проекте. Это, к примеру, может быть TOrdersAppModule – логика работы с заказами, TSuppliesAppModule – логика работы с поставками и т.д. Все они наследуются от абстрактного TAppModule, который содержит всю необходимую служебную функциональность (рис.6).
Паттерн | Нет |
---|---|
Имя в проекте | TappModule |
Задача | Обеспечить абстрактный интерфейс для работы со всеми модулями |
Решение | Реализовать базовый класс для всех модулей (рис. 3.) |
Результат | Обеспечивает единый интерфейс для консоли приложения при создании различных модулей (TPriceListAppModule, TOrdersModule, …). Включает ссылку на медиатор для «общения» с остальными модулями. Инкапсулирует свой уровень представления. |
TAppModule владеет ссылкой на медиатор, что позволяет ему обмениваться сообщениями с другими модулями, не зная при этом их конкретной реализации. Такая архитектура делает модуль независимым от всех прочих модулей приложения. При этом каждый модуль сам реализует свой уровень представления (рис.2). Методы GetName и GetOperations позволяют главному модулю TAppConsole получить имя модуля и множество поддерживаемых им функций для формирования интерфейса пользователя TAppConsoleForm. Через метод Execute осуществляется вызов функции модуля с соответствующим порядковым номером. Этот вызов происходит, когда пользователь вызывает пункт меню, соответствующий одной из функций модуля (рис. 4). Метод ExecCommand обеспечивает возможность передачи модулю команды и используется только медиатором, мотивацию существования которого мы и разберем далее.
Паттерн | Медиатор |
---|---|
Имя в проекте | TMediator |
Задача | Обеспечить способ взаимодействия между модулями |
Решение | Использовать медиатор для обмена сообщениями |
Результат | Медиатор «знает» обо всех модулях приложения, и все модули «знают» о Медиаторе (рис. 7). Модули могут «общаться», не «зная» друг друга |
В результате анализа требований предметной области возникла необходимость взаимодействия различных модулей друг с другом. Самый простой путь – это реализовать ассоциативную связь «каждый-с-каждым» (рисунок 7).
При небольшом количестве модулей (2-3) такое решение оправдано вследствие простоты исполнения, но при увеличении числа модулей до 5-10 и больше возникают следующие проблемы:
Для решения данных проблем вводится дополнительная сущность – Медиатор (рис. 8). Он реализуется как Singleton.
Взаимодействие между модулями осуществляется созданием нужной команды и отправкой ее через медиатор. Поскольку отправитель не знает класса получателя, и медиатор не знает, кому предназначено сообщение, он вызывает все модули последовательно, т.е. отправляет команду широковещательно всем модулям. Команда представляет собой объект. Получатели распознают тип команды, и обрабатывают только те, что предназначаются им.
В интерфейсе модуля присутствует публичный метод ExecCommand (рис. 6). Именно через него медиатор и осуществляет передачу команд. Пример обработчика команд приведен в листинге 3.
Листинг 3 - обработчик команд в модуле TOrdersAppModule
{ обрабочик команд TOrdersAppModule } procedure TOrdersAppModule.ExecCommand(Command: TCustomCommand); begin if Command is TNewOrderCommand then // создать заказ OnCommand_NewOrder( TNewOrderCommand(Command) ); if Command is TNewOrderItemCommand then // добавить позицию заказа OnCommand_NewOrderItem( TNewOrderItemCommand(Command) ); if Command is TGetOrderCommand then // получить данные об активном заказе OnCommand_GetOrderCommand( TGetOrderCommand(Command) ); end;
Модуль-отправитель создает объект-команду для модуля-получателя и вызывает метод медиатора SendMessage(TCustomCommand). Медиатор последовательно просматривает все зарегистрированные в нем модули и вызывает их команду ExecCommand(TCustomCommand). Вызываемая процедура Execute поверяет тип команды и или выполняет ее, или отвергает. Команды выполняются в синхронном режиме. Данный подход напоминает паттерн Publisher-Subscriber. На рис. 8 показана структура классов команд и их взаимосвязь с Медиатором.
Паттерн | Команда |
---|---|
Имя в проекте | TCustomCommand |
Задача | Обеспечить синхронный способ передачи сообщений между модулями |
Решение | Инкапсулировать необходимые данные в объект команды и создавать наследников для всех типов команд (рис.9). Использовать медиатор для обмена сообщениями, которые содержат ссылку на конкретную команду. |
Результат | Передача сообщений между модулями без прямых вызовов |
Листинг 4 – Пример классов команд
TCommandResult= (crUndefined, crOk, crCancel, crException, crIgnored); { базовая команда } TCustomCommand = class public CommandResult: TCommandResult; // результат обработки команды ExceptionMessage, ExceptionClassName: String; // данные об исключении end; { TGetClientCommand – обработчик команды по заданному ИНН (INN) должен найти клиента в БД, и вернуть его данные и TCommandResult = crOK. Иначе вернуть TCommandResult = crUndefined } TGetClientCommand = class(TCustomOrderCommand) private { секция private и спецификации чтения-записи свойств опущены } ... public property INN: double; property CompanyName: string; property EMail: string; property Address: string; property Phone: string; end;
Примером применения команды может служить загрузка заказа из файла, когда нужно проверить, существует ли в БД клиентов соответствующий отправитель заказа, и если такового нет, предложить пользователю добавить его.
Листинг 5. Использование команды для проверки регистрации заказчика.
var ... GetClientCommand: TGetClientCommand; begin ... { Проверить наличие клиента в базе данных } GetClientCommand.INN := ‘1234567890’; theMediator.SendMessage(GetClientCommand); case GetClientCommand.CommandResult of crUndefined: if MessageDlg( 'Не могу загрузить данные прайслиста. Поставщик неизвестен: ' + GetClientCommand.INN + '. Зарегистрировать поставщика? ', mtConfirmation, [mbOKCancel], 0) = mrOK then { Регистрируем клиента в базе данных через соответствующую команду }; crException: Exception.Raise('Не могу загрузить данные прайслиста. Ошибка: ' + GetClientCommand.ExceptMessage); end; { end case } ... end;
Эта команда будет последовательно отправлена медиатором всем модулям, но обработчик для нее существует только в модуле клиентов TClientAppModule. Поскольку вся логика работы с клиентами скрыта в TclientAppModule, то он самастоятельно решает, как обрабатывать каждую команду. В данном случае, он произведет поиск в БД клиентов. Можно изменить логику обработки этой команды и совместить в ней запрос на поиск клиента по ИНН и автоматический запуск мастера регистрации нового клиента в случаи неудачи поиска. Тогда код команды упростится, и отдельная команда для создания клиента не понадобится, но тем самым мы потеряем отдельную команду для проверки существования клиента. Какой вариант лучше – решается, исходя из требований к конкретной функциональности разрабатываемой программы.
При запуске приложения сначала создается AppConsole (Singleton), которая в свою очередь создает theMediator (Singleton) и theModuleFactory (Singleton). Затем в цикле создаются модули, посредством вызова функции CreateModule, в которой один из параметров имеет тип модуля (TModuleType), а второй – это ссылка на Медиатор. Перечисление TModuleType = (mtPriceList, mtClients,…) необходимо для фабрики модулей, которая должна знать, когда и какой модуль передавать. Ссылку на Медиатор фабрика классов передает вновь созданному модулю для обеспечения передачи сообщений. Тем самым достигается эффект слабой связанности (Low Coupling) между модулями. Затем консоль от вновь созданного модуля (Module) получает его имя (GetName) и поддерживаемые операции (GetOperations). По этим данным консоль строит меню и панели управления.
Рассмотренный подход позволяет, на наш взгляд, реализовать достаточно универсальную базовую архитектуру для построения расширяемых приложений. Для создания дополнительного модуля в приведенном проекте всего лишь необходимо:
Низкая связность между модулями и стандартизация интерфейсов модулей позволяет значительно упростить групповую разработку проекта. Для построения прикладного ПО при среднем размере проектов мы рекомендуем попробовать использовать архитектуру, рассмотренную в данной статье. Исходные тексты на Delphi и C++Builder готовой заготовки проекта с подробными комментариями кода можно найти на прилагаемом к журналу CD. Код проекта содержит дополнительную функциональность, не рассмотренную в данной статье. Так, в нем реализован механизм работы с конверторами, позволяющий каждому модулю сохранять и загружать данные (прайс-листы, заказы, накладные) в нужных ему форматах (текст, DBase, XML). Там же вы сможете найти также электронную копию этого материала и соответствующий проект RationalRose.
В качестве дальнейшего развития архитектуры рекомендуется перейти к работе через интерфейсы при вызове между различными уровнями программы. Для построения приложений, которые масштабируются до уровня распределенных систем, следует более четко выстраивать трехуровневую (или даже пятиуровневую) [3] структуру.
Ведущий рассылки: Алекс Jenter jenter@rsdn.ru
Публикуемые в рассылке материалы принадлежат сайту RSDN.