Для кого: для начинающих разработчиков, кто еще не сформировал свой архитектурный стиль; для опытных, кто может увидеть недочеты и поделиться своими идеями и приемами в комментариях.
Хотел бы я написать: «Меня зовут Шляхин Сергей, разработал столько-то сервисов, помог бизнесу таким-то компаниям». К сожалению, пока не могу). В 1С долго, но с большим перерывом, уходил в смежную область, не сложилось, поэтому возвращаюсь.
Пару месяцев назад прошел замечательный курс (автор: Матвей Серегин), который дал много идей, было время все осмыслить. На основе синтетического примера из курса: «Управление задачами», представляю очередной пет-проект, делюсь результатом систематизации полученных знаний.
Слои
На мой взгляд для работы сервиса необходимо минимум 4 модуля, условно назвал так: Модуль сервиса, Контроллер, Реализация, Валидация.
Пусть наш сервис обслуживает выдуманный документ «Задача Сотрудника», тогда модули будут называться РаботаСЗадачами, РаботаСЗадачамиHTTP, РаботаСЗадачамиРеализация, ВалидацияДанных.
| Модуль | Описание |
|---|---|
| РаботаСЗадачами |
Входная точка сервиса, максимально типизированный код методов + обработка ошибок и ответы «матрешки». Общается только с модулем «РаботаСЗадачамиHTTP», ожидает получить требуемые данные, настроенные заголовки или известный КодОшибки.
|
| РаботаСЗадачамиHTTP |
Мозг и главный контроллер сервиса.
|
| РаботаСЗадачамиРеализация |
Доступ к БД (вернее необходимой для сервиса части) и подсистемам конфигурации. Ничего не знает о сервисе, почти не имеет никакой логики, предоставляет данные и все.
|
| ВалидацияДанных |
Самый независимый модуль, программный интерфейс которого заточен на проверку входящих значений по определенным правилам или схемам. Ничего не знает о сервисе, БД, может быть использован любой подсистемой ИС.
|
Обозначили границы, закрепили функции - разрабатывать, тестировать можно независимо.
Модифицировать и искать ошибки тоже легко.
Модуль сервиса РаботаСЗадачами
-
Типизированный код методов
-
Обработка ошибок
-
Ответы матрешки
Типизированный код методов
Разберем на примерах:
Функция СоздатьЗадачу(Запрос)
// Область блокировок: условие и реакция ответ
// примеры:
// - требуется обновление
Если РаботаСЗадачамиHTTP.ЗаблокированоОбновлением() Тогда Возврат ОтветЗаблокированоОбновлением() КонецЕсли;
// - превышено число запросов
Заголовки = Новый Соответствие; // результирующие заголовки
Если РаботаСЗадачамиHTTP.ПревышеноЧислоЗапросов(Заголовки) Тогда
Возврат ОтветПревышеноЧислоЗапросов(Заголовки)
КонецЕсли;
// - идентификация пользователя, см. ЗУП, сервис: ЗагрузкаКандидатов
КодОшибки = ""; Код = 201; // результирующие код ошибки и код состояния
// Получение данных через попытку
Попытка
// ожидаем свойства новой задачи, Код, Заголовки или КодОшибки
НоваяЗадача = РаботаСЗадачамиHTTP.НоваяЗадачаСотрудника(Запрос, КодОшибки, Заголовки, Код)
Исключение
// Обработка исключения
Возврат ОтветВнутренняяОшибка(ИнформацияОбОшибке())
КонецПопытки;
// Обработка ошибки
Если КодОшибки <> "" Тогда Возврат ОтветТипизированнаяОшибка(КодОшибки, Заголовки) КонецЕсли;
// Область результата, разбор состояний:
// - переадресация - реакция ответ
Если Код = 303 Тогда Возврат НовыйОтвет(Код, Заголовки) КонецЕсли;
// - ожидаемое состояние - реакция ответ
Возврат Ответ(, "Новая задача создана", НоваяЗадача, Заголовки, Код)
КонецФункции
Пример попроще, без комментариев:
Функция ПолучитьЗадачи(Запрос)
Если РаботаСЗадачамиHTTP.ЗаблокированоОбновлением() Тогда Возврат ОтветЗаблокированоОбновлением() КонецЕсли;
Заголовки = Новый Соответствие;
Если РаботаСЗадачамиHTTP.ПревышеноЧислоЗапросов(Заголовки) Тогда
Возврат ОтветПревышеноЧислоЗапросов(Заголовки)
КонецЕсли;
КодОшибки = "";
Попытка
СписокЗадач = РаботаСЗадачамиHTTP.СписокЗадач(Запрос, КодОшибки, Заголовки)
Исключение
Возврат ОтветВнутренняяОшибка(ИнформацияОбОшибке())
КонецПопытки;
Если КодОшибки <> "" Тогда Возврат ОтветТипизированнаяОшибка(КодОшибки, Заголовки) КонецЕсли;
Возврат Ответ(, "Список задач", СписокЗадач, Заголовки)
КонецФункции
И так оформляем каждый метод
Обработка ошибок
Важно разработать "говорящую" систему ошибок для сервиса. Такой минимальный набор в моем сервисе:
#Область УправлениеОшибками
Функция НоваяОшибка(Описание, Код, ЕстьДетали = Ложь)
Возврат Новый Структура("Описание,Код,ЕстьДетали", Описание, Код, ЕстьДетали)
КонецФункции
Функция ТипизированныеОшибки()
Ошибки = Новый Соответствие;
// ошибки запроса
Ошибки.Вставить("ТипТела", НоваяОшибка("Тело запроса не верного типа", 400));
Ошибки.Вставить("ФорматТела", НоваяОшибка("Нарушен формат тела", 400));
Ошибки.Вставить("ЗадачаНеНайдена", НоваяОшибка("Задача не найдена", 404));
Ошибки.Вставить("ПустоеТелоЗадания", НоваяОшибка("Пустое тело задания", 400));
Ошибки.Вставить("Разделитель", НоваяОшибка("Не указан разделитель тела запроса", 400));
Ошибки.Вставить("НетФайлов", НоваяОшибка("Тело запроса не содержит файлов", 400));
Ошибки.Вставить("РазныеВладельцы", НоваяОшибка("Файл не принадлежит указанной задаче", 404));
// ошибки сервиса
Ошибки.Вставить("ЗадачаНеСоздана", НоваяОшибка("Задача не создана", 500));
Ошибки.Вставить("ЗаданиеНеСоздано", НоваяОшибка("Задание не создано", 500));
// ошибки, у которых есть какие-то детали
Ошибки.Вставить("ВалидацияДанных", НоваяОшибка("Не пройдена валидация входных данных", 400, Истина));
Возврат Ошибки
КонецФункции
// Механизм сбора ошибок, использую при валидации входных данных
Функция СписокСообщенийПользователю()
Список = Новый Массив;
Сообщения = ПолучитьСообщенияПользователю(Истина);
Для Каждого Сообщение Из Сообщения Цикл
Список.Добавить(Сообщение.Текст)
КонецЦикла;
Возврат Список;
КонецФункции
// Некоторые ошибки желательно зафиксировать для разбора
Процедура СделатьЗаписьЖРОбОшибке(ИнформацияОбОшибке)
ИмяСобытия = "РаботаСЗадачамиHTTP.Ошибка";
Уровень = УровеньЖурналаРегистрации.Ошибка;
Объект = Метаданные.HTTPСервисы.РаботаСЗадачами;
Комментарий = "";
Если ТипЗнч(ИнформацияОбОшибке) = Тип("ИнформацияОбОшибке") Тогда
Комментарий = ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке)
Иначе
Комментарий = Строка(ИнформацияОбОшибке)
КонецЕсли;
ЗаписьЖурналаРегистрации(ИмяСобытия, Уровень, Объект,, Комментарий);
КонецПроцедуры
#КонецОбласти
Ответы матрешки
Минимальный ответ требует код состояния и заголовки:
Функция НовыйОтвет(Код, Заголовки)
Ответ = Новый HTTPСервисОтвет(Код);
Если ТипЗнч(Заголовки) = Тип("Соответствие") Тогда
Ответ.Заголовки = Заголовки;
КонецЕсли;
// место, где можно обогатить каждый ответ специальными заголовками, например CORS
Возврат Ответ
КонецФункции
Будем использовать как самую маленькую матрешку для других вариантов.
Базовый ответ с телом JSON как самый распространённый транспорт:
Функция Ответ(Успех = Истина, Описание = "", ТелоДанные = Неопределено, Заголовки = Неопределено, Код = 200)
Ответ = НовыйОтвет(Код, Заголовки);
Если ПустаяСтрока(Описание) Тогда Возврат Ответ КонецЕсли;
Результат = Новый Структура;
Результат.Вставить("result", Успех);
Результат.Вставить("description", Описание);
Если ТелоДанные <> Неопределено Тогда
Ключ = ?(Успех, "data", "errors");
Результат.Вставить(Ключ, ТелоДанные);
КонецЕсли;
Ответ.Заголовки.Вставить("Content-Type", "application/json");
Ответ.УстановитьТелоИзСтроки(ЗаписатьЗначениеJSON(Результат));
Возврат Ответ
КонецФункции
Использую как для успешных ответов, так и для ответов с информацией об ошибке.
Другие варианты с телом:
#Область ОтветыСТелом
Функция ОтветТекст(ТелоДанные, Заголовки = Неопределено, Код = 200, ТипСодержимого = "text/plain")
Ответ = НовыйОтвет(Код, Заголовки);
Ответ.Заголовки.Вставить("Content-Type", ТипСодержимого);
Ответ.УстановитьТелоИзСтроки(ТелоДанные);
Возврат Ответ
КонецФункции
Функция ОтветДвоичныеДанные(ТелоДанные, Заголовки = Неопределено, Код = 200, ТипСодержимого = "application/octet-stream")
Ответ = НовыйОтвет(Код, Заголовки);
Ответ.Заголовки.Вставить("Content-Type", ТипСодержимого);
Ответ.УстановитьТелоИзДвоичныхДанных(ТелоДанные);
Возврат Ответ
КонецФункции
#КонецОбласти
Еще одна матрешка - ошибки сервиса:
#Область ОшибкиСервиса
Функция ОтветОшибкаСервиса(Описание = "Ошибка сервиса", Предложение = Неопределено, Код = 500)
Возврат Ответ(Ложь, Описание, Предложение,, Код)
КонецФункции
Функция ОтветЗаблокированоОбновлением()
Описание = "Заблокировано обновлением";
Предложение = "Сервис не доступен, попробуйте позже";
Возврат ОтветОшибкаСервиса(Описание, Предложение, 503)
КонецФункции
Функция ОтветВнутренняяОшибка(ИнформацияОбОшибке)
СделатьЗаписьЖРОбОшибке(ИнформацияОбОшибке);
Возврат ОтветОшибкаСервиса("Внутренняя ошибка")
КонецФункции
#КонецОбласти
И специальные:
Функция ОтветПревышеноЧислоЗапросов(Заголовки)
Возврат Ответ(Ложь, "Превышено число запросов", Неопределено, Заголовки, 429)
КонецФункции
Функция ОтветТипизированнаяОшибка(КодОшибки, Заголовки, ПараметрЗапроса = "?errorText=")
ОписаниеОшибки = ТипизированныеОшибки().Получить(КодОшибки);
Если ОписаниеОшибки = Неопределено Тогда
ОписаниеОшибки = НоваяОшибка("Не типизированная ошибка сервиса", 500);
СделатьЗаписьЖРОбОшибке(СтрШаблон("Не типизированная ошибка ""%1""", КодОшибки));
КонецЕсли;
Детали = Неопределено;
Если ОписаниеОшибки.ЕстьДетали Тогда
Детали = СтрСоединить(СписокСообщенийПользователю(), Символы.ПС);
КонецЕсли;
Location = Заголовки.Получить("Location"); // если указан, то ошибку передать через параметр, с кодом 303
Если Location <> Неопределено Тогда
ТекстОшибки = ОписаниеОшибки.Описание + ?(ОписаниеОшибки.ЕстьДетали, ":" + Символы.ПС + Детали, "");
ТекстОшибки = КодироватьСтроку(ТекстОшибки, СпособКодированияСтроки.КодировкаURL);
Заголовки.Вставить("Location", Location + ПараметрЗапроса + ТекстОшибки);
Возврат НовыйОтвет(303, Заголовки);
КонецЕсли;
Возврат Ответ(Ложь, ОписаниеОшибки.Описание, Детали, Заголовки, ОписаниеОшибки.Код)
КонецФункции
Ответ с типизированной ошибкой:
-
разбирает известные ошибки и выводит в ответе их описание, детали, устанавливает соответствующий код;
-
если ошибка не найдена, то об этом будет сделана запись в журнале регистрации;
-
показан вариант, как можно обработать сразу несколько ошибок (Детали);
-
включает возможность представить ответ в HTML-форме, через код и заголовки переадресации.
Замечания
-
Подготовка заголовков ответов разделена:
-
«РаботаСЗадачамиHTTP» готовит специфические для бизнес-логики;
-
«РаботаСЗадачами» общие для всех ответов (CORS), тип содержимого, а также те, которые требуют включить текст ошибки в значение (показ ответа в HTML-форме)
-
-
Ввод нескольких вариантов методов для подготовки ответов обусловлен удобством и повышением читаемости кода. На примере ошибок сервиса, в методы зашиты или код, или дополнительные описание или функционал.
-
Вопрос о месте размещения методов генерации типизированных ошибок. По идее, им место в «РаботаСЗадачамиHTTP», где они, собственно, и появляются. А модулю «РаботаСЗадачами» должно быть все равно, какую ошибку обрабатывать. Разместил здесь для наглядности.
Хочу верить, что данный шаблон слоя можно переиспользовать от сервиса к сервису.
На сегодня все. Если тема интересна, понравился формат и стоит продолжать раскрывать слои, то дайте знать в комментариях или лайками).
Продолжение: Валидация данных
Вступайте в нашу телеграмм-группу Инфостарт