Проблема многих систем - тесное сплетение процедур и функций, отсутствие четких границ между слоями, слабая прослеживаемость внутренних операций и как результат - большое количество ресурсов для поддержания и расширения такой системы. Если для старых легаси такое допускается, то разработка новой функциональности в таком стиле - ни в коем случае.
В статье описывается необычный способ построения связей функций, выработанный мной в процессе многолетнего опыта разработки программных продуктов. В рамках статьи будет создана небольшая система, которую мы будем расширять, используя только рассматриваемый паттерн.
Следует понимать, что абстрактный пример для статьи максимально упрощен, при этом применение паттерна гипертрофировано. В реальной практике количество кода, узлов и условий намного больше, а использование любых паттернов, включая этот - взвешено и при необходимости.
Вводные
Нам поставлена задача - по содержимому файлов PDF создавать данные в информационной базе. Операция несложная, состоит из трех этапов - распознать содержимое документа PDF, создать по содержимому стандартизированные таблицы с данными, а затем реализовать бизнес-логику в учетной системе по этим данным.
Программный код, который это реализует, будет выглядеть примерно так:
Внутренняя реализация - простейший способ
Процедура ИмпортДокументаPDFВБазуДанных(Документ) Экспорт
ТекстИЛинииДокумента = ТекстИЛинииИзДокумента(Документ);
Если ТекстИЛинииДокумента = Неопределено Тогда
Возврат;
КонецЕсли;
ТаблицыДанных = ТаблицыДанныхИзТекстаИЛиний(ТекстИЛинииДокумента);
Если ТаблицыДанных = Неопределено Тогда
Возврат;
КонецЕсли;
СохранениеДанныхВИнформационнуюБазу(ТаблицыДанных, Документ);
КонецПроцедуры
Такой способ реализации прост, последователен, логичен, удобочитаем. Но все это начнет исчезать, когда система поработает некоторое время. Выяснится, что некоторые файлы не обрабатываются. Разработчик заменит процедуру на функцию для возвращения признака успешного выполнения. Затем потребуется видеть, на каких этапах операции возникают ошибки обработки. В коде начнут появляться вставки с выводом сообщений, находиться вставки будут в различных местах кода, на различных слоях.
И даже при таком расширении функциональности (с уже потерянной элегантностью кода) проблемы не решатся - выясниться, к примеру, что вызов производится в некоторых случаях не пользователем, а автоматически, и сообщения некому читать.
Обратная связь обработчиков
Решить такого рода эскалацию проблем можно, введя расширенный формат ответа обработчиков. Каждый обработчик в программном коде должен возвращать следующую информацию:
• Удалось ли выполнить операцию так, как от него это ожидалось?
• Информационные сообщения, появившиеся в процессе работы;
• Непосредственно сам результат обработчика (если требуется);
• Прочая дополнительная информация, требуемая в контексте функциональности (если требуется).
Удобно для этого использовать ответ в виде структуры со следующими ключами:
Ответ = Новый Структура;
Ответ.Вставить("ВыполненоУспешно", Ложь); // Односложный ответ, всегда булево
Ответ.Вставить("МассивСообщений", Новый Массив); // Всегда массив, может быть пустым
Ответ.Вставить("Результат", Неопределено); // То, что ожидается от функции. При неудаче - Неопределено
Система с таким паттерном информативна, удобна для отладки во всех слоях, замена обработчиков, возможно введение обработчиков возникающих проблем, сбор появившихся сообщений и их сортировка (мы ведь не собираемся выводить пользователю сообщения со всех слоев системы).
Реализация такого подхода:
Внутренняя реализация с обратной связью
Функция ИмпортДокументаPDFВБазуДанных(Документ) Экспорт
СтруктураОтвет = Новый Структура("ВыполненоУспешно, МассивСообщений", Ложь, Новый Массив);
Отказ = Ложь;
// Получение текста и линий из PDF
ТекстИЛинииДокумента = Неопределено;
ОтветОбработчика = ТекстИЛинииИзДокумента(Документ);
Если ОтветОбработчика.ВыполненоУспешно Тогда
ТекстИЛинииДокумента = ОтветОбработчика.ТекстИЛинииДокумента;
Иначе
Отказ = Истина;
КонецЕсли;
Для Каждого ТекстСообщенияОбработчика Из ОтветОбработчика.МассивСообщений Цикл
СтруктураОтвет.МассивСообщений.Добавить(ТекстСообщенияОбработчика);
КонецЦикла;
// Преобразование линий и текста в таблицы значений
ТаблицыДанных = Неопределено;
Если Не Отказ Тогда
ОтветОбработчика = ТаблицыДанныхИзТекстаИЛиний(ТекстИЛинииДокумента);
Если ОтветОбработчика.ВыполненоУспешно Тогда
ТаблицыДанных = ОтветОбработчика.ТаблицыДанных;
Иначе
Отказ = Истина;
КонецЕсли;
Для Каждого ТекстСообщенияОбработчика Из ОтветОбработчика.МассивСообщений Цикл
СтруктураОтвет.МассивСообщений.Добавить(ТекстСообщенияОбработчика);
КонецЦикла;
КонецЕсли;
// Создание документов в базе 1С по содержимому таблиц документа
Если Не Отказ Тогда
ОтветОбработчика = СохранениеДанныхВИнформационнуюБазу(ТаблицыДанных, Документ);
Если ОтветОбработчика.ВыполненоУспешно Тогда
СтруктураОтвет.ВыполненоУспешно = Истина;
Иначе
Отказ = Истина;
КонецЕсли;
Для Каждого ТекстСообщенияОбработчика Из ОтветОбработчика.МассивСообщений Цикл
СтруктураОтвет.МассивСообщений.Добавить(ТекстСообщенияОбработчика);
КонецЦикла;
КонецЕсли;
// Подготовка ответа
Если Отказ = Истина Тогда
ТекстСообщения = "Операция импорта не выполнена";
СтруктураОтвет.МассивСообщений.Добавить(ТекстСообщения);
КонецЕсли;
Возврат СтруктураОтвет;
КонецФункции
Взглянув на программный код выше, возникает разумный вопрос - не избыточна ли такая функциональность?
- Нет.
Атомарность
Увеличив количество строк кода более, чем в 4 раза, мы создали функцию, которая теперь не только поддерживает масштабируемость собственной внутренней реализации, но так-же может быть легко встроена в часть другой системы, использующей описываемый паттерн. Унифицированный формат ответа исключает различные виды связей процедур и функций, определенных разработчиком локально, и неприменимых в других местах. В некоторых случаях процедуры и функции настолько тесно сплетены, что превращаются в монолит, полностью исключающий их атомарность.
При правильном использовании паттерна напротив, система состоит из автономных обработчиков, которые получают некоторые данные в виде параметров, и всегда возвращают данные в едином формате.
Рассмотрим это на примере нашей системы:
Наша разработка по работе с файлами PDF нашла признание в глазах заказчика, и он расширяет ее применение. Поступают новые требования:
1. Файлов теперь будет очень много, система должна справляться с нагрузкой;
2. Поддержка файлов PDF с новым типом таблиц, разделенных табуляцией (TSV);
3. Полноценное использование модуля из внешних систем, в т.ч. с Интернет-сайта заказчика;
4. Просмотр состояния выполнения обработки каждого файла в интерактивном режиме.
Масштабируемость
Для системы, не поддерживающей расширение функциональности, пункты 3 и 4 означали бы полное перестроение архитектуры, в некоторых случаях - написание программы с нуля. В нашем же случае атомарность всех обработчиков позволяет разрывать их цепь в любом месте на разные потоки.
Пример реализации, когда обработка файла разорвана на отдельные последовательности операций:
• Получение файла и создание задания на отложенную обработку файла (API - POST);
• Преобразование данных файла в таблицы значений и сохранение их (регламентное задание 1);
• Запись данных таблиц значений в информационную базу (регламентное задание 2);
• Запрос подробного состояния обработки файла и формирование отчета о выполнении (API - GET).
Создание задания на обработку регламентными заданиями
Функция НовоеЗаданиеНаОбработкуФайла(АдресФайла) Экспорт
Ответ = Новый Структура("ВыполненоУспешно, МассивСообщений", Ложь, Новый Массив);
СтруктураЗаписи = РегистрыСведений.ФайлыКОбработке.ИнициализацияЗаписиРегистра();
СтруктураЗаписи.ИдентификаторЗадания = Новый УникальныйИдентификатор;
СтруктураЗаписи.АдресФайла = АдресФайла;
СтруктураЗаписи.ДатаСоздания = ТекущаяДата();
РезультатОбработчика = РегистрыСведений.ФайлыКОбработке.СоздатьОбновитьЗапись(СтруктураЗаписи);
Ответ.ВыполненоУспешно = РезультатОбработчика.ВыполненоУспешно;
Ответ.МассивСообщений = РезультатОбработчика.МассивСообщений;
Возврат Ответ;
КонецФункции
Преобразование файлов в данные
// Обработчик регламентного задания
Процедура ПреобразованиеФайловВДанные() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ФайлыКОбработке.ИдентификаторЗадания КАК ИдентификаторЗадания,
| ФайлыКОбработке.АдресФайла КАК АдресФайла,
| ФайлыКОбработке.НеудачныхПопытокОбработки КАК НеудачныхПопытокОбработки,
| ЛимитОбрабатываемыхЗаданийЗаВызов.Значение КАК ЛимитОбрабатываемыхЗаданийЗаВызов
|ИЗ
| РегистрСведений.ФайлыКОбработке КАК ФайлыКОбработке,
| Константа.ЛимитОбрабатываемыхЗаданийЗаВызов КАК ЛимитОбрабатываемыхЗаданийЗаВызов
|
|УПОРЯДОЧИТЬ ПО
| ФайлыКОбработке.ДатаПоследнейОбработки УБЫВ,
| ФайлыКОбработке.ДатаСоздания,
| ФайлыКОбработке.НеудачныхПопытокОбработки";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
СчетчикИтераций = 1;
Пока ВыборкаДетальныеЗаписи.Следующий()
И СчетчикИтераций <= ВыборкаДетальныеЗаписи.ЛимитОбрабатываемыхЗаданийЗаВызов Цикл
Отказ = Истина;
МассивСообщений = Новый Массив;
// Получение документа PDF из файла
ОтветОбработчика = ФайловыеОперации.ПолучитьДокументИзФайла(ВыборкаДетальныеЗаписи.АдресФайла);
Документ = Неопределено;
Если ОтветОбработчика.ВыполненоУспешно Тогда
Документ = ОтветОбработчика.Документ;
Иначе
Отказ = Истина;
КонецЕсли;
Для Каждого ТекстСообщенияОбработчика Из ОтветОбработчика.МассивСообщений Цикл
МассивСообщений.Добавить(ТекстСообщенияОбработчика);
КонецЦикла;
// Получение текста и линий из PDF
ТекстИЛинииДокумента = Неопределено;
Если Не Отказ Тогда
ОтветОбработчика = ТекстИЛинииИзДокумента(Документ);
Если ОтветОбработчика.ВыполненоУспешно Тогда
ТекстИЛинииДокумента = ОтветОбработчика.ТекстИЛинииДокумента;
Иначе
Отказ = Истина;
КонецЕсли;
Для Каждого ТекстСообщенияОбработчика Из ОтветОбработчика.МассивСообщений Цикл
МассивСообщений.Добавить(ТекстСообщенияОбработчика);
КонецЦикла;
КонецЕсли;
// Преобразование линий и текста в таблицы значений
ТаблицыДанных = Неопределено;
Если Не Отказ Тогда
Если ЭтоДокументСРазделениемТабуляцией(ТекстИЛинииДокумента) Тогда
ОтветОбработчика = ТаблицыДанныхИзТекстаИТабуляции(ТекстИЛинииДокумента);
Иначе
ОтветОбработчика = ТаблицыДанныхИзТекстаИЛиний(ТекстИЛинииДокумента);
КонецЕсли;
ОтветОбработчика = ТаблицыДанныхИзТекстаИЛиний(ТекстИЛинииДокумента);
Если ОтветОбработчика.ВыполненоУспешно Тогда
ТаблицыДанных = ОтветОбработчика.ТаблицыДанных;
Иначе
Отказ = Истина;
КонецЕсли;
Для Каждого ТекстСообщенияОбработчика Из ОтветОбработчика.МассивСообщений Цикл
МассивСообщений.Добавить(ТекстСообщенияОбработчика);
КонецЦикла;
КонецЕсли;
// Перенос задания из регистра ФайлыКОбработке в регистр ДанныеКОбработке
Если Не Отказ Тогда
СтруктураЗаписи = РегистрыСведений.ДанныеКОбработке.ИнициализацияЗаписиРегистра();
СтруктураЗаписи.ДатаСоздания = ТекущаяДата();
СтруктураЗаписи.ТаблицыДанных = ТаблицыДанных;
ОтветОбработчика = РегистрыСведений.ФайлыКОбработке.СоздатьОбновитьЗапись(СтруктураЗаписи);
Если ОтветОбработчика.ВыполненоУспешно Тогда
РегистрыСведений.ФайлыКОбработке.ПеренестиЗаписьВАрхив(ВыборкаДетальныеЗаписи.ИдентификаторЗадания);
Иначе
Отказ = Истина;
КонецЕсли;
Для Каждого ТекстСообщенияОбработчика Из ОтветОбработчика.МассивСообщений Цикл
МассивСообщений.Добавить(ТекстСообщенияОбработчика);
КонецЦикла;
КонецЕсли;
// Актуализация задания в списке заданий
ВыполненоУспешно = Отказ = Ложь;
Если Не ВыполненоУспешно Тогда
СтруктураЗаписи = РегистрыСведений.ФайлыКОбработке.ИнициализацияЗаписиРегистра();
СтруктураЗаписи.ИдентификаторЗадания = ВыборкаДетальныеЗаписи.ИдентификаторЗадания;
СтруктураЗаписи.ДатаПоследнейОбработки = ТекущаяДата();
СтруктураЗаписи.НеудачныхПопытокОбработки = ВыборкаДетальныеЗаписи.НеудачныхПопытокОбработки + 1;
РегистрыСведений.ФайлыКОбработке.СоздатьОбновитьЗапись(СтруктураЗаписи);
КонецЕсли;
// Сохранение сообщений обработчиков для отложенного просмотра
СохранениеСообщенийВСистеме(ВыборкаДетальныеЗаписи.ИдентификаторЗадания, МассивСообщений, ВыполненоУспешно);
СчетчикИтераций = СчетчикИтераций + 1;
КонецЦикла;
КонецПроцедуры
Формирование отчета о выполнении задания
Функция ОтчетОВыполненииЗадания(ИдентификаторЗадания) Экспорт
Ответ = Новый Структура("ВыполненоУспешно, Состояние, МассивСообщений", Ложь, "", Новый Массив);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ФайлыКОбработке.ИдентификаторЗадания КАК ИдентификаторЗадания
|ПОМЕСТИТЬ ФайлыКОбработке
|ИЗ
| РегистрСведений.ФайлыКОбработке КАК ФайлыКОбработке
|ГДЕ
| ФайлыКОбработке.ИдентификаторЗадания = &ИдентификаторЗадания
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ДанныеКОбработке.ИдентификаторЗадания КАК ИдентификаторЗадания
|ПОМЕСТИТЬ ДанныеКОбработке
|ИЗ
| РегистрСведений.ДанныеКОбработке КАК ДанныеКОбработке
|ГДЕ
| ДанныеКОбработке.ИдентификаторЗадания = &ИдентификаторЗадания
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ЗавершенныеЗадания.ИдентификаторЗадания КАК ИдентификаторЗадания,
| ЗавершенныеЗадания.ВыполненоУспешно КАК ВыполненоУспешно
|ПОМЕСТИТЬ ЗавершенныеЗадания
|ИЗ
| РегистрСведений.ЗавершенныеЗадания КАК ЗавершенныеЗадания
|ГДЕ
| ЗавершенныеЗадания.ИдентификаторЗадания = &ИдентификаторЗадания
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| НЕ ФайлыКОбработке.ИдентификаторЗадания ЕСТЬ NULL КАК ЕстьЗаданиеНаОбработкуФайла,
| НЕ ЗавершенныеЗадания.ИдентификаторЗадания ЕСТЬ NULL КАК ЕстьЗаданиеНаОбработкуДанных,
| НЕ ДанныеКОбработке.ИдентификаторЗадания ЕСТЬ NULL КАК ЕстьЗавершенноеЗадание,
| ЕстьNull(ЗавершенныеЗадания.ВыполненоУспешно, ЛОЖЬ) КАК ВыполненоУспешно
|ИЗ
| ФайлыКОбработке КАК ФайлыКОбработке,
| ЗавершенныеЗадания КАК ЗавершенныеЗадания,
| ДанныеКОбработке КАК ДанныеКОбработке
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| СообщенияОбработчиковВыполнения.ИдентификаторЗадания КАК ИдентификаторЗадания,
| СообщенияОбработчиковВыполнения.ИдентификаторСообщения КАК ИдентификаторСообщения,
| СообщенияОбработчиковВыполнения.ТекстСообщения КАК ТекстСообщения
|ИЗ
| РегистрСведений.СообщенияОбработчиковВыполнения КАК СообщенияОбработчиковВыполнения
|ГДЕ
| СообщенияОбработчиковВыполнения.ИдентификаторЗадания = &ИдентификаторЗадания
|
|УПОРЯДОЧИТЬ ПО
| ДатаСоздания";
Запрос.УстановитьПараметр("ИдентификаторЗадания", ИдентификаторЗадания);
РезультатЗапроса = Запрос.ВыполнитьПакет();
// Определение статуса выполнения задания по наличию его в регистрах
ВыборкаНаличиеЗаданий = РезультатЗапроса[3].Выбрать();
Если ВыборкаНаличиеЗаданий.Следующий() Тогда
Ответ.ВыполненоУспешно = Истина;
Если ВыборкаНаличиеЗаданий.ЕстьЗавершенноеЗадание Тогда
Если ВыборкаНаличиеЗаданий.ВыполненоУспешно Тогда
Ответ.Состояние = "Выполнено успешно";
Иначе
Ответ.Состояние = "Завершено с ошибками";
КонецЕсли;
ИначеЕсли ВыборкаНаличиеЗаданий.ЕстьЗаданиеНаОбработкуДанных Тогда
Ответ.Состояние = "Обработка данных";
ИначеЕсли ВыборкаНаличиеЗаданий.ЕстьЗаданиеНаОбработкуДанных Тогда
Ответ.Состояние = "Обработка файла";
Иначе
Ответ.Состояние = "Неизвестно";
КонецЕсли;
Иначе
Ответ.ВыполненоУспешно = Ложь;
Ответ.Состояние = "Отсутствует задание";
КонецЕсли;
// Вывод сохраненных сообщений от обработчиков задания
ВыборкаСообщения = РезультатЗапроса[4].Выбрать();
Пока ВыборкаСообщения.Следующий() Цикл
Ответ.МассивСообщений.Добавить(ВыборкаСообщения.ТекстСообщения);
КонецЦикла;
Возврат Ответ;
КонецФункции
Вызов обработчиков и сами они никак не изменились, хотя сама система теперь значительно расширена. Задачи для регламентных заданий мы регистрируем записями в регистрах сведений, появляющиеся сообщения обработчиков и промежуточные результаты обработчиков сохраняем для передачи в другие потоки. Получаем эти данные другим потоком и продолжаем работу.
Если проанализировать код, видно, что он весь пронизан таким способом построения кода. Код стал шаблонным, однообразным, и как следствие, удобочитаем.
Заключение
Описываемый способ особенно актуален при работе с внешними сервисами и/или в многослойной архитектуре приложения. Однако использование его в тех местах, где это не требуется, может привести к определенным проблемам. Разработчик определяет, опираясь на систему сдержек и противовесов, собственного опыта и правил разработки, как паттерны использовать.