Немного про паттерны.
В объектно-ориентированных языках программирования есть такая замечательная вещь, как шаблоны (паттерны) проектирования. В общих чертах паттерны проектирования можно представить как шаблоны эффективного решения однотипных задач, с которыми сталкиваются большинство программистов. Тем самым задачу определенного вида можно решить, подобрав подходящий паттерн, а не создавать свой велосипед. При этом нужно понимать, какой шаблон проектирования использовать для конкретного вида задач. Неправильно подобранный паттерн может значительно усложнить и снизить эффективность решения. У паттернов проектирования есть имена ("Стратегия", "Наблюдатель", "Посредник" и т. д ), благодаря которым, общаясь с коллегами, можно не описывать всю суть решения поставленной задачи, а обойтись фразой "а здесь я применил паттерн %ИмяПаттерна%".
Если проводить аналогию с реальным миром, то в качестве шаблона проектирования можно представить последовательность строительства большинства частных домов:
- Копка котлована
- Строительство фундамента
- Возведение коробки
- Подключение коммуникаций
- Внутренняя отделка
- Отделка фасада
Представленную последовательность можно назвать паттерном «Строительство частного дома». Теперь, если необходимо решить задачу строительства частного дома, можно воспользоваться паттерном «Строительство частного дома».
За более подробным раскрытием темы «Что такое паттерны проектирования?» рекомендую обратиться к источникам в Интернете.
Паттерн "Стратегия".
"Паттерн Стратегия определяет семейство алгоритмов, инкапсулирует каждый из них и обеспечивает их взаимозаменяемость. Он позволяет модифицировать алгоритмы независимо от их использования на стороне клиента."
Такое определение приведено в книге "Head First. Паттерны проектирования" (авторы Э. Фримен, Э. Робсон, К.Сьерра, Б.Бейтс).
Выделим ключевые моменты из приведенного определения:
1. Семейство алгоритмов. Паттерн "Стратегия" позволяет выделить процедуры и функции, которые выполняют схожий функционал, принимают одни и те же параметры и возвращают значение одного типа (если это функция) но имеют разную реализацию.
Например, у нас есть потребность преобразовать элемент справочника "Товары" в формат, понятный другим системам. В зависимости от запроса вызывающей стороны, элемент справочника необходимо преобразовать в строку формата XML или JSON. Было создано две функции: одна создает строковое описание товара в формате XML, другая - в формате JSON. Обе функции в качестве параметра принимают ссылку на справочник "Товары" а в результате возвращают строковое значение (описание товара в формате JSON или XML). Выбор между XML и JSON в данном случае - это выбор стратегии, по которой дальше будет выполняться алгоритм. Условно можно представить в виде следующей схемы:
2. Инкапсуляция алгоритмов. Для алгоритмов реализации конкретных стратегий предполагается выполнение следующих условий:
- Процедуры (функции) реализующие конкретные стратегии изолированы друг от друга;
- Изменение в одной процедуре (функции) никак не должно отражаться на поведении других процедур (функций) реализующих другие стратегии выполнения;
- Вызывающей стороне запрещено напрямую обращаться к алгоритму конкретной реализации. Обращение должно выполняться через отдельный интерфейс.
Вернемся к нашему примеру с преобразованием товара в XML и JSON. Следуя условиям инкапсуляции, нам понадобится:
- Реализовать методы преобразования товара в XML и JSON таким образом, чтобы изменение в алгоритме XML никак не отражалось на изменении в алгоритме JSON и наоборот;
- Создать общий модуль, содержащий программный интерфейс, через который вызывающая сторона может обращаться к функциям преобразования товара в XML и JSON.
Общий модуль, содержащий методы по преобразованию и программный интерфейс, можно представить следующим образом:
#Область ПрограммныйИнтерфейс
// Возвращает строковое описание
// переданного товара в запрашиваемом
// формате
//
// Параметры:
// Товар - СправочникСсылка.Товар - товар, который нужно
// описать в запрашиваемом формат
//
// ФорматСериализации - Строка - возможно одно из значений:
// "XML", "JSON"
//
// Возвращаемое значение:
// - Строка - описание товара в запрашиваемом формате сериализации
//
// Пример:
// ОписаниеТовараXML = СериализацияДанных.СериализованныйТовар(СсылкаНаТовар, "XML");
//
Функция СериализованныйТовар(Товар, ФорматСериализации) Экспорт
// Выбор стратегии
Если ФорматСериализации = "XML" Тогда
Возврат ТоварXML(Товар);
ИначеЕсли ФорматСериализации = "JSON" Тогда
Возврат ТоварJSON(Товар);
Иначе
ВызватьИсключение СтрШаблон(НСтр("ru = 'Формат %1 не поддерживается'"),
ФорматСериализации);
КонецЕсли;
КонецФункции
#КонецОбласти
Созданный общий модуль назовем «СериализацияДанных». Тогда для получения результата вызывающая сторона должна обратиться к нашему общему модулю:
ТоварВXML = СериализацияДанных.СериализованныйТовар(СсылкаНаТовар, "XML");
3. Взаимозаменяемость. Выбор стратегии, по которой продолжится выполнение алгоритма, можно делать прямо во время выполнения программы. В нашем примере мы можем выбирать стратегию выполнения путем передачи значения параметра «ФорматСериализации» методу программного интерфейса сериализации товара:
// Здесь мы выбрали стратегию ТоварXML
ТоварВXML = СериализацияДанных.СериализованныйТовар(СсылкаНаТовар, "XML");
// а здесь стратегию ТоварJSON
ТоварВJSON = СериализацияДанных.СериализованныйТовар(СсылкаНаТовар, "JSON");
Рецепт «Стратегии»
Предполагается что у читателя есть базовые представления ООП.
Для использования шаблона «Стратегия» нам потребуется:
* Класс контекста. Поскольку конкретные реализации алгоритмов требуемого действия / результата (по сути стратегии) недоступны вызывающей стороне (клиенту) напрямую, необходима точка взаимодействия, с помощью которой клиент может устанавливать стратегию выполнения, инициировать запуск стратегии. В процессе взаимодействия класс-контекст делегирует выполнение алгоритма конкретному классу-стратегии (вызывает выбранную клиентом реализацию).
* Общий интерфейс. Соглашение, содержащее описание методов (без реализации) которые должны быть реализованы в классах конкретных стратегий. Требуется для того, чтобы класс-контекст «не задумывался» о том, какая стратегия будет вызвана, т.к. все стратегии содержат методы, описанные в интерфейсе.
* Классы-реализации конкретных стратегий. Это объекты, в которых содержатся реализованные методы-стратегии, описанные в общем интерфейсе (п.2.). Данные методы вызываются, когда класс-контекст делегирует продолжение выполнение алгоритма классу реализации конкретной стратегии.
Способ применения:
1. Вызывающая сторона (клиент) выбирает стратегию выполнения алгоритма путем создания экземпляра класса-реализации конкретной стратегии.
2. Клиент обращается к классу контексту:
2.1 Сообщает классу-контексту, какую стратегию нужно выполнить. Для этого у класса-контекста должен присутствовать метод, сохраняющий ссылку на экземпляр выбранной стратегии из п.1 в реквизит класса-контекста.
2.2 Вызывает метод класса-контекста, инициирующий запуск выполнения стратегии. В этот момент класс-контекст «пробрасывает» вызов от клиента в метод класса реализации выбранной стратегии.
3. Для смены стратегии клиент создает экземпляр другого класса-реализации и обращается к классу-контексту как в п.2
Схема взаимодействия выглядит следующим образом:
Для понимания приведу пример из жизни. Мне понадобилось подстричься. Рассматриваю отзывы на стилистов из ближайшей парикмахерской и выбираю определенного мастера по стрижке. Придя в парикмахерскую, сообщаю администратору, что хочу подстричься у выбранного мной мастера. Администратор проводит меня в зал к определенному мастеру. Я усаживаюсь в кресле, и стилист принимается за работу. Закончив, я понимаю, что не мешало бы и подравнять бороду. Но у текущего мастера закончилась рабочая смена, и мне приходится выбрать другого стилиста. Определившись с парикмахером, подхожу к администратору, сообщаю, что хочу подровнять бороду у другого выбранного стилиста. Меня снова отводят в зал к выбранному парикмахеру, усаживают в кресло, и мастер выполняет стрижку бороды.
С точки зрения паттерна стратегии в описанной ситуации присутствуют следующие составляющие:
* Класс контекст. В качестве точки взаимодействия выступает администратор парикмахерской. Ему я сообщаю, какого мастера выбрал и прошу оказать мне услугу по стрижке. Далее администратор усаживает меня в кресло к выбранному мастеру и делегирует продолжение оказания услуги парикмахеру.
* Общий интерфейс. Все стилисты парикмахерской обладают навыком стрижки. Поэтому администратор, провожая клиента на стрижку, не задумывается, умеет ли стричь выбранный клиентом парикмахер.
* Конкретные реализации стратегии. В качестве стратегий в данном случае выступают стилисты парикмахерской. Все они умеют стричь, но каждый по-своему уникально. Кто-то держит ножницы в левой руке, кто-то в правой. Одни парикмахеры предпочитают большую часть работы выполнять машинкой, другие - ножницами и т.д.
* Выбор стратегии. Выбирая определенного стилиста, я выбирал стратегию.
* Взаимозаменяемость. Когда закончилась смена первого стилиста, для стрижки бороды я выбрал другого работающего стилиста. О своем выборе я сообщил администратору и попросил оказать услугу по стрижке бороды.
Получается следующая схема взаимодействия:
Думаю, становится очевидным, что все описание шаблона проектирования «Стратегия» было приведено с использованием понятий объектно-ориентированного программирования. Классы, интерфейсы, экземпляры класса – все эти понятия далеки от мира 1с. Но ничто не мешает нам выделить суть составляющих паттерна «Стратегия» и правильно их применить. Тогда с точки зрения механизма паттерна «Стратегия» нам понадобится:
1. Точка взаимодействия – объект, с которым будет взаимодействовать вызывающая сторона. Через точку взаимодействия клиент будет передавать информацию о выбранной стратегии выполнения и инициировать запуск алгоритма стратегии. Точкой взаимодействия может быть, например, общий модуль.
2. Соглашение по реализации методов стратегий. В соглашении должны быть перечислены и описаны методы, обязательные к реализации в конкретных стратегиях. Такое соглашение можно оформить в документации и в описании модуля точки взаимодействия.
3. Объекты, реализующие конкретные стратегии. Это объекты, содержащие методы из соглашения п.2, к которым будет переходить управление в момент делегирования вызова от точки взаимодействия. Объекты реализации конкретных стратегий должны содержать все методы из соглашения. Такими объектами-стратегиями могут быть, например, общие модули, модули-менеджеры объектов.
Тогда применение паттерна «Стратегия» можно описать так:
- Клиент обращается к точке взаимодействия, передав информацию о выбранной стратегии и входные параметры алгоритма выполнения стратегии.
- В точке взаимодействия на основании полученной информации о стратегии управление передается объекту-реализации – вызывается метод объекта-реализации, описанный в соглашении по реализации алгоритмов стратегий. Параметры от клиента «пробрасываются» в этот метод.
В качестве клиента может быть любое место конфигурации (модуль объекта, модуль менеджера, общий модуль и т.д.), из которого можно «дотянуться» до точки взаимодействия.
Пример
Пример реализован на версии платформы 8.3.23.2157. Для того чтобы полностью сосредоточиться на построении решения по шаблону "Стратегия" а не на нюансах реализации взят максимально простой пример, который в реальной жизни конечно же можно сделать в разы проще.
В качестве примера использования шаблона проектирования «Стратегия» рассмотрим следующую задачу: необходимо создать алгоритм расчета математического выражения суммы, разности, умножения и деления для двух чисел (такой же пример рассматривается в статье на википедии). Задачу будем решать на пустой конфигурации.
Исходя из описанных условий, можно выделить схожие алгоритмы – это расчет суммы, разности, произведения и частного двух чисел. На вход функциям поступают два числа, а в результате расчета возвращается другое число. Таким образом, есть четыре стратегии:
- Сумма
- Разность
- Умножение
- Деление
Создадим серверные общие модули, в которые в последующем поместим алгоритмы расчета математического выражения:
Общие модули «Сумма», «Разность», «Умножение», «Деление» - это объекты, реализующие конкретные стратегии.
Далее нам потребуется соглашение по реализации методов стратегии - во всех ранее созданных общих модулях обязательно должна присутствовать функция «РезультатВыражения» с двумя числовыми параметрами, возвращающая числовое значение. Вставим заготовку в созданные ранее общие модули:
#Область СлужебныйПрограммныйИнтерфейс
// Параметры:
// ПервоеЧисло - Число
// ВтороеЧисло - Число
//
// Возвращаемое значение:
// - Число
//
Функция РезультатВыражения(ПервоеЧисло, ВтороеЧисло) Экспорт
КонецФункции
#КонецОбласти
Теперь опишем алгоритмы расчета во вставленной заготовке для каждой стратегии.
#Область СлужебныйПрограммныйИнтерфейс
// Параметры:
// ПервоеЧисло - Число
// ВтороеЧисло - Число
//
// Возвращаемое значение:
// - Число - результат сложения первого и второго числа
//
Функция РезультатВыражения(ПервоеЧисло, ВтороеЧисло) Экспорт
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = НСтр("ru = 'Вызов стратегии Сумма'");
Сообщение.Сообщить();
РезультатВыражения = ПервоеЧисло + ВтороеЧисло;
Возврат РезультатВыражения;
КонецФункции
#КонецОбласти
#Область СлужебныйПрограммныйИнтерфейс
// Параметры:
// ПервоеЧисло - Число - уменьшаемое
// ВтороеЧисло - Число - вычитаемое
//
// Возвращаемое значение:
// - Число - разность первого и второго числа
//
Функция РезультатВыражения(ПервоеЧисло, ВтороеЧисло) Экспорт
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = НСтр("ru = 'Вызов стратегии Разность'");
Сообщение.Сообщить();
РезультатВыражения = ПервоеЧисло - ВтороеЧисло;
Возврат РезультатВыражения;
КонецФункции
#КонецОбласти
#Область СлужебныйПрограммныйИнтерфейс
// Параметры:
// ПервоеЧисло - Число
// ВтороеЧисло - Число
//
// Возвращаемое значение:
// - Число - произведение первого и второго числа
//
Функция РезультатВыражения(ПервоеЧисло, ВтороеЧисло) Экспорт
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = НСтр("ru = 'Вызов стратегии Умножение'");
Сообщение.Сообщить();
РезультатВыражения = ПервоеЧисло * ВтороеЧисло;
Возврат РезультатВыражения;
КонецФункции
#КонецОбласти
#Область СлужебныйПрограммныйИнтерфейс
// Параметры:
// ПервоеЧисло - Число - делимое
// ВтороеЧисло - Число - делитель
//
// Возвращаемое значение:
// - Число - частное первого числа от второго
//
Функция РезультатВыражения(ПервоеЧисло, ВтороеЧисло) Экспорт
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = НСтр("ru = 'Вызов стратегии Деление'");
Сообщение.Сообщить();
РезультатВыражения = ПервоеЧисло / ВтороеЧисло;
Возврат РезультатВыражения;
КонецФункции
#КонецОбласти
На текущем этапе мы завершили создание объектов-реализаций конкретных стратегий. Теперь нам нужен объект точка взаимодействия. В качестве точки взаимодействия создадим серверный общий модуль «МатематическиеВыражения»:
На вход точке взаимодействия нужно передать информацию о выбранной стратегии и параметры алгоритма. В нашем случае информация о стратегии – это выбранная математическая операция (сумма, разность, умножение, деление), параметры алгоритма – это первое и второе число. Тогда программный интерфейс модуля «МатематическиеВыражения» можно реализовать так:
#Область ПрограммныйИнтерфейс
// Возвращает результат выбранной математической
// операции двух чисел
//
// Параметры:
// Операция - Строка - математическое выражение.
// Допустимые значения: "Сумма", "Разность",
// "Умножение", "Деление".
//
// ПервоеЧисло - Число
// ВтороеЧисло - Число
//
// Возвращаемое значение:
// - Число - результат математической операции первого и второго числа
//
Функция РезультатМатематическойОперации(Операция, ПервоеЧисло, ВтороеЧисло) Экспорт
// 1. Получим объект-реализации стратегии математической операции (общий модуль)
МодульРеализацииРасчета = Вычислить(Операция);
// 2. Делегируем выполнение расчета объекту-реализации конкретной стратегии
РезультатОперации = МодульРеализацииРасчета.РезультатВыражения(ПервоеЧисло, ВтороеЧисло);
Возврат РезультатОперации;
КонецФункции
#КонецОбласти
Теперь можно приступать к проверке. Для этого нам понадобится клиент, который будет обращаться к нашей точке взаимодействия. В качестве клиента создадим обработку «ПроверкаМатематическихВыражений»:
Реквизиты обработки:
- МатематическаяОперация (Строка 99) – здесь мы будем указывать, какую операцию хотим выполнить;
- ПервоеЧисло (Число 15, 2);
- ВтороеЧисло (Число 15, 2);
- Результат (Число 15, 2) – результат расчета выбранного математического выражения
Откроем модуль объекта созданной обработки и опишем обращение к точке взаимодействия:
#Область ПрограммныйИнтерфейс
// Вычисляет результат выбранной математической
// операции двух чисел. Выражение указывается в
// реквизите "МатематическаяОперация";
// параметры - в реквизитах "ПервоеЧисло", "ВтороеЧисло";
// результат вычисления помещается в реквизит "Результат"
//
Процедура РасчитатьРезультатМатематическогоВыражения() Экспорт
Результат = МатематическиеВыражения.РезультатМатематическойОперации(МатематическаяОперация,
ПервоеЧисло, ВтороеЧисло);
КонецПроцедуры
#КонецОбласти
Создадим основную форму обработки, разместим реквизиты:
Для элемента формы «МатематическаяОперация» определим список выбора и установим свойство «Режим выбора из списка»:
Разместим на форме команду «Рассчитать выражение»:
Для команды создадим клиентский обработчик и напишем в нем следующий код:
#Область ОбработчикиКомандФормы
&НаКлиенте
Процедура РассчитатьВыражение(Команда)
Если НЕ ЗначениеЗаполнено(Объект.МатематическаяОперация) Тогда
ПоказатьПредупреждение(, НСтр("ru = 'Необходимо выбрать математическую операцию'"), 20);
Возврат;
КонецЕсли;
Если Объект.МатематическаяОперация = "Деление" И Объект.ВтороеЧисло = 0 Тогда
ПоказатьПредупреждение(, НСтр("ru = 'Деление на 0 недопустимо'"), 20);
Возврат;
КонецЕсли;
РассчитатьВыражениеНаСервере();
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
&НаСервере
Процедура РассчитатьВыражениеНаСервере()
ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
ОбработкаОбъект.РасчитатьРезультатМатематическогоВыражения();
ЗначениеВРеквизитФормы(ОбработкаОбъект, "Объект");
КонецПроцедуры
#КонецОбласти
Здесь мы проверяем заполнение обязательных параметров и вызываем метод расчета математического выражения, расположенный в модуле объекта.
Запустим наше приложение и проверим обработку:
В ходе проверки можно заметить, что алгоритм расчета мы можем менять прямо в ходе выполнения программы путем установки значения реквизита «Математическая операция».
Таким образом, в ходе построения примера мы реализовали следующие ключевые моменты шаблона проектирования «Стратегия»:
- Схожие алгоритмы. Выделили алгоритмы расчета суммы, разности, умножения и деления в отдельные функции, которые на входе принимают два числа, а возвращают другое число – результат математической операции.
- Инкапсуляция. Каждый алгоритм расчета выражения поместили в отдельный объект-реализацию – это общие модули «Сумма», «Разность», «Умножение», «Деление». Для внешней стороны (клиента) создали программный интерфейс (точку взаимодействия) по вызову функций расчета – это общий модуль «МатематическиеОперации».
- Взаимозаменяемость. Как было показано в ходе проверки, алгоритмы расчета мы можем менять прямо во время исполнения программы путем выбора значения реквизита «Математическая операция» обработки-клиента.
В виде схемы полученное решение можно представить так:
Реальные примеры использования
Применение паттерна «Стратегия» можно увидеть в подсистемах «Отправка SMS» (см. общий модуль «ОтправкаSMS») и «Базовая функциональность» (см. общий модуль «АдминистрированиеКластера») библиотеки стандартных подсистем.
Заключение
В данной статье мы познакомились с шаблоном проектирования «Стратегия» а также рассмотрели простой пример использования данного паттерна. «Стратегию» следует применять, когда:
- У нас есть схожие алгоритмы для решения поставленной задачи;
- Алгоритмы необходимо изолировать друг от друга, чтобы реализация одной «стратегии» не зависела от реализации другой. Это позволит изменять конкретные реализации алгоритмов без ущерба для других. Также подобный подход позволит легко расширить имеющийся функционал – для добавления новой «стратегии» нам понадобится просто добавить ещё один объект-реализацию;
- Реализация алгоритмов («стратегий») не должна зависеть от внутренней структуры клиента (вызывающей стороны). Взаимодействие должно выполняться с разными клиентами с различной структурой;
- Есть потребность менять алгоритмы прямо во время исполнения программы;
При выборе паттерна «Стратегия» следует учитывать:
- Для того чтобы в зависимости от условий выбрать правильную стратегию, вызывающая сторона должна знать о существовании всех возможных стратегий и чем они отличаются;
- Каждая новая стратегия – это новый объект-реализация (т.е. ещё один общий модуль, внешняя обработка и. т.д.), что приводит к разрастанию структуры конфигурации.
Рассмотренный пример может быть модифицирован и улучшен по желанию читателя. Конфигурация с примером прикреплена к данной статье.
Спасибо за внимание!