“И тут я сказал малышу:
- Вот тебе ящик. А в нем сидит твой барашек.
И как же я удивился, когда мой строгий судья вдруг просиял:
- Вот такого мне и надо!...”
Антуан де Сент-Экзюпери
Как это часто бывает, при проектировании нового функционала мы не знаем, каким образом его потребуется расширить в процессе эксплуатации.
Например, нужно реализовать рассылку уведомлений нашим пользователям. В процессе согласования требований было решено, что для рассылки будет использоваться электронная почта.
В процессе реализации были созданы объекты для хранения настроек (адреса пользователей, серверов и т.п) и общий модуль РассылкаУведомлений, содержащий процедуру ОтправитьУведомление(), вызов которой был добавлен в тех местах конфигурации, где требуется отправлять уведомления.
Внезапно выясняется, что пользователи поголовно используют “модную” программу мгновенного обмена сообщениями и предпочли бы получать оповещения в ней.
Традиционно, для реализации нового требования потребуется модифицировать процедуру ОтправитьУведомление(), добавив дополнительную ветку кода для нового способа отправки.
И так будет происходить при каждом новом поступившем пожелании, что через некоторое время приведет к усложнению поддержки и модификации механизма.
Чтобы упростить расширение функционала в будущем, необходимо отделить общий код механизма от кода конкретных способов доставки уведомлений. Как это можно сделать, читаем ниже.
Почему “фасады”?
При подготовке статьи я обратился к старшим товарищам поискал информацию об используемых подходах к проектированию и выяснил, что именно так, facade, называется паттерн проектирования, который наиболее близок к предлагаемому мной подходу.
Широко распространенным примером этого паттерна сейчас можно назвать разработку с использованием интерфейсов (interface). При таком подходе отдельно объявляется интерфейс, который является публичным контрактом для взаимодействия с объектами, и отдельно существуют объекты, реализующие такой интерфейс. Т.е. для взаимодействия с объектами достаточно иметь представление об интерфейсе, опуская детали реализации каждого конкретного объекта.
Например, объявляем интерфейс «РассылкаУведомлений», который содержит метод ОтправитьУведомление() и создаем объекты ОтправительEmail, ОтправительMessenger, которые реализуют созданный интерфейс.
В дальнейшем при необходимости отправки оповещений достаточно вызвать метод ОтправитьУведомление(), состав параметров которого (сигнатура) определяется интерфейсом.
Где может пригодиться такой подход к разработке в 1С?
Навскидку, несколько вариантов распространенных задач:
-
Логирование – настраиваемые точки и способы вывода логов
-
Доставка оповещений – настраиваемые способы отправки оповещений
-
Версионирование данных – использование различных способов хранения версий данных
-
События и обработка событий – настраиваемое обнаружение возникших событий и реакция системы на события
-
Обмен данными – обработки трансформации данных и транспорт файлов с данными
-
Присоединяемые файлы - использование различных способов хранения, получения и версионирования присоединяемых файлов
-
Адаптивные формы – возможность настройки различных форм для объектов одного типа
You name it…
Как это может выглядеть в 1С?
В своих реализациях в качестве интерфейсов я чаще всего использую общие модули, в которых определяю фиксированный набор процедур и функций (методов) для вызова механизмов реализуемой подсистемы.
Если есть необходимость хранить состояния объекта между вызовами, можно создать интерфейс не в общем модуле, а в модуле обработки.
Логика работы конкретных объектов реализуется в виде обработок, которые могут быть как встроенными , так и внешними (например, на время отладки).
Для связи интерфейса с этими обработками служит справочник, который также предлагает пользователю выполнить «тонкую» настройку объектов, реализующих интерфейс, т.е. указать значения параметров, которые будут использованы при вызове обработки.
Пример с логированием
Для наглядности рассмотрим пример реализации механизма логирования. Демонстрационная конфигурация прилагается (на GitHub).
С учетом того, что конфигурация может быть встроена в существующие (да, я знаю про расширения, но все-таки...), приняты следующие правила наименования:
-
префикс объектов фасадов: ктв - мой личный префикс объектов
-
для обработок-объектов: <ПостоянныйПрефиксОбъектов>_<ТипРеализуемогоИнтерфейса>_<ИмяРеализации>, в нашем случае, например ктв_Лог_ЖурналРегистрации
Интерфейс
Интерфейс представлен общим модулем ктв_Логирование, а также обработкой ктв_Логирование, для возможности хранения промежуточных состояний и сокращения кода вызова.
Т.к. вывод логов может использоваться как для хранения, так и для просмотра актуальной информации пользователем, мы предусмотрели возможность вызова интерфейсных методов как на сервере, так и на клиенте. Поэтому модуль ктв_Логирование допускает вызов как на клиенте, так и на сервере.
Главным методом нашего интерфейса является ктв_Логирование.ЗаписатьВЛог()
Процедура ЗаписатьВЛог(ИмяЛога, Текст, УровеньЛога = 1, ПараметрыЗаписи = Неопределено) Экспорт
где:
-
ИмяЛога - строковый идентификатор лога
-
Текст - собственно текст сообщения для помещения в журнал
-
УровеньЛога - числовой уровень лога, “чем больше, тем страшнее”
-
ПараметрыЗаписи - структура произвольных параметров, которые могут потребоваться обработкам, реализующим способ вывода лога.
В зависимости от “места вывода”, из этой процедуры вызывается процедура ЗаписатьВЛог() клиентского (ктв_ЛогированиеКлиент) или серверного (ктв_ЛогированиеВызовСервера) модуля.
Метод ПолучитьЛог() возвращает объект (для вывода лога на сервере) или форму (для вывода на клиенте) интерфейсной (фасадной) обработки ктв_Логирование и позволяет заранее установить имя лога и набор постоянных параметров, что упрощает вызов записи в лог:
Лог = ктв_Логирование.ПолучитьЛог(“1c.iface.demo”, Новый Структура(“Параметр”, “ЗначениеПараметра”));
Лог.ЗаписатьВЛог(“Запись в лог”, ктв_Логирование.УровниЛога().Ошибка);
А куда пишем-то?
Тут появляется справочник ктв_СпособыЛогирования и сами обработки-объекты, реализующие механизм логирования.
Основные реквизиты этого справочника:
-
Обработка - содержит имя встроенной обработки или саму внешнюю обработку
-
ОбработкаНастройки” - здесь могут находиться параметры вывода лога, которые нужны конкретно для выбранной обработки.
-
ИспользоватьДляВсех - определяет, что данный способ логирования будет использован для вывода в любом случае, независимо от значения параметра ИмяЛога, которое было передано при обращении к интерфейсу.
Если нужно указать, что конкретный способ вывода лога используется только для определенных имен логов, то список обрабатываемых имен можно указать, перейдя на закладку Имена логов.
Информация о назначении имен логов способам логирования сохранится в служебном регистре.
В итоге при вызове метода ЗаписатьВЛог() (общего модуля или обработки), система найдет в справочнике способы вывода логов, соответствующие имени лога, и вызовет экспортируемую процедуру с таким же названием из указанной в элементе справочника обработки.
Что в обработке?
Обработки, которые реализуют логику записи информации в лог, должны содержать следующие экспортируемые методы:
Функция ЭтоОбработкаЛогирования() Экспорт
Возврат Истина;
КонецФункции // ЭтоОбработкаЛогирования()
При поиске обработок в конфигурации и при добавлении внешней обработки, проверяем, что это “правильная” обработка.
Функция ВыводитьНаКлиенте() Экспорт
Возврат Истина;
КонецФункции // ВыводитьНаКлиенте()
При попытке вывода информации в лог на клиенте проверяем, что обработка поддерживает вывод на клиенте.
Функция ВыводитьНаСервере() Экспорт
Возврат Ложь;
КонецФункции // ВыводитьНаСервере()
При попытке вывода информации в лог на сервере проверяем, что обработка поддерживает вывод на сервере.
Функция ПолучитьФормуОбработчиковКоманд() Экспорт
Возврат "Форма";
КонецФункции // ПолучитьФормуОбработчиковКоманд()
При нажатии кнопки открытия в поле Обработка справочника способов логирования, обработка команды будет выполняться в форме с именем, возвращенным этой функцией.
Функция ПолучитьСписокДействийКнопкиОткрытия() Экспорт
СписокДействий = Новый СписокЗначений();
СписокДействий.Добавить("ВыполнитьКоманду_КаталогЛогированияОткрытие(Неопределено, Ложь)", "Открыть каталог логов...", , БиблиотекаКартинок.ОткрытьФайл);
СписокДействий.Добавить("ОткрытьФорму_Форма", "Настройка...", , БиблиотекаКартинок.ИзменитьФорму);
Возврат СписокДействий;
КонецФункции // ПолучитьСписокДействийКнопкиОткрытия()
При нажатии кнопки открытия в поле Обработка справочника способов логирования, будет показано меню, список пунктов которого вернет эта функция.
Процедура ЗаписатьВЛог(Текст, СпособЛогирования, ПараметрыЗаписи) Экспорт
Если НЕ Настройка.Свойство("КаталогЛогирования") Тогда
ПараметрыЗаписи.Вставить("ТекстОшибки", СтрШаблон("Не указан каталог логирования для способа ""%1""!", СокрЛП(СпособЛогирования)));
Возврат;
КонецЕсли;
ИмяФайлаЛога = ПолучитьИмяФайлаЛога(Настройка.КаталогЛогирования);
мФайлЛога = Новый ЗаписьТекста(ИмяФайлаЛога, "UTF-8", , Истина);
мФайлЛога.ЗаписатьСтроку(Текст);
мФайлЛога.Закрыть();
КонецПроцедуры // ЗаписатьВЛог()
Основная процедура, реализующая логику записи информации в лог. Всегда должна содержать параметры:
-
Текст - текст для записи в лог
-
СпособЛогирования - элемент справочника вфт_СпособыЛогирования, для которого была вызвана обработка. Используется для получения настроек способа логирования.
-
ПараметрыЗаписи - структура произвольных параметров, которые могут быть использованы в алгоритме записи в лог.
Как убедиться, что все заработало?
Есть простенькая встроенная демо-обработка:
Указываем имя лога, не забывая что оно должно быть назначено одному из способов в справочнике (ну или есть “всеядный” способ). Указываем уровень лога и текст сообщения. Жмем Записать... (на клиенте будут отработаны только те способы вывода лога, которые предусматривают работу на клиенте).
В итоге получаем такую картинку:
или такую:
или такую:
ну или все сразу.
Что в итоге?
-
Есть подход, который может быть применен для решения многих задач более-менее единообразным способом.
-
Демо-конфигурация, которая содержит как пример, так и готовые механизмы для “быстрой” реализации подхода. Справочник ктв_Логирование может быть использован как шаблон, а основная логика взаимодействия с обработками-объектами находится в общих модулях:
-
Конкретно механизм логирования приведен, конечно, больше в качестве примера, но вполне может быть развит до необходимого уровня.
Кстати!
-
Если представить, что нам необходимо отправлять логи в Google Cloud Platform, как описал Дмитрий Шерстобитов в свой статье //infostart.ru/public/796913/, - то нам остается только реализовать еще один способ логирования, а фасад во всей системе переписывать не придется.
-
Различные архитектурные идеи будут обсуждаться на Винзаводе 17-18 мая, где в рамках Хакатона по 1С будет рассказано о механизме событийной интеграции подсистем, который также использует описанный выше подход. Если кто не в курсе - то это вот это мероприятие https://isthisdesign.org,