Текучий интрефейс (fluent interface) - это способ организации функций в классе таким образом, чтобы их можно было вызывать через точку друг от друга, т.е. использовать цепочки методов
Возможно, если вы сталкивались с автоматизированным тестированием в 1С или OneScript, вроде YaxUnit, xUnitFor1C, Vanessa-Add, Asserts и т.п., то слышали термин "текучие утверждения". Так вот это частный случай текучего интерфейса, когда с его помощью реализуются разнообразные проверки истинности утверждений - например, результатов тестирования:
Ожидаем.Что(НекийМассив.Количество()).Минимум(9);
Но данный механизм применим не только там: любой процесс, предполагающий последовательный вызов методов от одного объекта может стать с ним сильно лучше. И сегодня мы посмотрим, как этого добиться.
ООП и 1С
Как было отмечено в определении текучего интерфейса - его использование подразумевает наличие класса. 1С, как мы знаем, объектно-ориентированным языком не является и эти ваши...
...не реализует. Но выход есть - использовать модуль объекта обработки
Обработка - тоже своего рода класс на минималках. Он не может by design в наследование (создание одного класса от другого с наследованием полей и методов) и рыбополиморфизм (если проблему обработки функцией параметров разных типов вообще можно записать в существующие для языка с динамической типизацией). Но зато может в инкапсуляцию - ограничение доступа ко всякой небезопасной внутрянке благодаря экспортным/неэскпортным переменным и методам, а главное может вернуть саму себя в качестве результата функции чем мы и воспользуемся
Пишем код
Основную идею нашей дальнейшей работы можно описать так: если обычные функции принимают параметры и возвращают результат, то функции в текучем интерфейсе всегда возвращают в качестве результата объект обработки, а настоящий результат, при необходимости, можно записать в переменные.
Для наглядности создадим новую обработку. Она будет представлять из себя некоторый "текучий калькулятор", благодаря которому мы сможем выполнять математические действия одно за другим через точку. Да, очень просто, но зато понятно.
Работать мы будем только в модуле обработки. Для начала, определим переменную для числа-результата и первую функцию - установку начального значения этой переменной
Перем Значение Экспорт;
#Область ПрограммныйИнтерфейс
// Функция - Установить значение
//
// Параметры:
// ЧисловоеЗначение - Число - Начальное значение
//
// Возвращаемое значение:
// ОбработкаОбъект.ТекучийКалькулятор - Этот объект
Функция УстановитьЗначение(Знач ЧисловоеЗначение) Экспорт
Значение = ПривестиЧисло(ЧисловоеЗначение);
Возврат ЭтотОбъект;
КонецФункции
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция ПривестиЧисло(Знач ЧисловоеЗначение) Экспорт
ОТЧ = Новый ОписаниеТипов("Число");
Возврат ОТЧ.ПривестиЗначение(ЧисловоеЗначение);
КонецФункции
#КонецОбласти
Не будем пока подробно на этом останавливаться, а сразу добавим еще две функции: пусть это будут сложение и вычитание.
// Функция - Прибавить
//
// Параметры:
// ЧисловоеЗначение - Число - Значение для прибавления
//
// Возвращаемое значение:
// ОбработкаОбъект.ТекучийКалькулятор - Этот объект
Функция Прибавить(Знач ЧисловоеЗначение) Экспорт
Значение = Значение + ПривестиЧисло(ЧисловоеЗначение);
Возврат ЭтотОбъект;
КонецФункции
// Функция - Отнять
//
// Параметры:
// ЧисловоеЗначение - Число - Значение для вычитания
//
// Возвращаемое значение:
// ОбработкаОбъект.ТекучийКалькулятор - Этот объект
Функция Отнять(Знач ЧисловоеЗначение) Экспорт
Значение = Значение - ПривестиЧисло(ЧисловоеЗначение);
Возврат ЭтотОбъект;
КонецФункции
Отлично - у нас есть некоторый базовый интерфейс. Рассмотрим его:
- Первое, что бросается в глаза - все функции возвращают ЭтотОбъект. Это, как мы уже обсуждали, и позволяет делать цепочки вызовов
- Второе - это переменная значение. Она служит для хранения результата между операциями и получения конечного значения
Попробуем сделать некоторые вычисления, используя нашу обработку
ТекучийКалькулятор = Обработки.ТекучийКалькулятор.Создать();
ТекучийКалькулятор.УстановитьЗначение(0)
.Прибавить(20) // 20
.Отнять(10) // 10
.Прибавить(5) // 15
.Отнять(3); // 12
Результат = ТекучийКалькулятор.Значение; // 12
Здесь я каждое действие записываю с новой строки: это не обязательно, но визуально наглядно и удобно для длинных цепочек.
Думаю, тут нет необходимости в особенных объяснениях: мы вызываем функции, которые меняют экспортную переменную Значение, а затем получаем ее содержимое. Таким образом можно реализовать множество разнообразных процессов, в том числе и сугубо 1Сных: мы используем обработку, как самый очевидный объект метаданных для подобных изысканий, но на самом деле ничего не мешает нам реализовывать подобное и в модулях объектов справочников или документов. Например, для их заполнения
Обработка ошибок
С самой базовой информацией мы разобрались. Теперь перейдем к другой немаловажной части - обработке ошибок.
Так как мы выполняем действия безостановочно, одно за другим, без возможности проверить корректность выполнения какой-либо из вызываемых функций в середине, нам необходимо реализовать обработку ошибок внутри нашего объекта.
Есть несколько вариантов организации этого процесса, я же предложу тот, который чаще всего видел и мне самому кажется наиболее удачным:
- В модуле обработки мы добавим еще одну переменную - Ошибка
- В каждую нашу функцию добавим Возврат в том случае, если Ошибка заполнена
Вот как выглядит наш обновленный модуль:
Перем Значение Экспорт;
Перем Ошибка Экспорт;
#Область ПрограммныйИнтерфейс
// Функция - Установить значение
//
// Параметры:
// ЧисловоеЗначение - Число - Начальное значение
//
// Возвращаемое значение:
// ОбработкаОбъект.ТекучийКалькулятор - Этот объект
Функция УстановитьЗначение(Знач ЧисловоеЗначение) Экспорт
Если ЗначениеЗаполнено(Ошибка) Тогда Возврат ЭтотОбъект КонецЕсли;
Значение = ПривестиЧисло(ЧисловоеЗначение);
Возврат ЭтотОбъект;
КонецФункции
// Функция - Прибавить
//
// Параметры:
// ЧисловоеЗначение - Число - Значение для прибавления
//
// Возвращаемое значение:
// ОбработкаОбъект.ТекучийКалькулятор - Этот объект
Функция Прибавить(Знач ЧисловоеЗначение) Экспорт
Если ЗначениеЗаполнено(Ошибка) Тогда Возврат ЭтотОбъект КонецЕсли;
Значение = Значение + ПривестиЧисло(ЧисловоеЗначение);
Возврат ЭтотОбъект;
КонецФункции
// Функция - Отнять
//
// Параметры:
// ЧисловоеЗначение - Число - Значение для вычитания
//
// Возвращаемое значение:
// ОбработкаОбъект.ТекучийКалькулятор - Этот объект
Функция Отнять(Знач ЧисловоеЗначение) Экспорт
Если ЗначениеЗаполнено(Ошибка) Тогда Возврат ЭтотОбъект КонецЕсли;
Значение = Значение - ПривестиЧисло(ЧисловоеЗначение);
Возврат ЭтотОбъект;
КонецФункции
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция ПривестиЧисло(Знач ЧисловоеЗначение) Экспорт
ОТЧ = Новый ОписаниеТипов("Число");
Возврат ОТЧ.ПривестиЗначение(ЧисловоеЗначение);
КонецФункции
#КонецОбласти
Для проверки необходимо спровоцировать какое-нибудь исключение. Добавим для этого функцию деления:
// Функция - Разделить на
//
// Параметры:
// ЧисловоеЗначение - Число - Делитель
//
// Возвращаемое значение:
// ОбработкаОбъект.ТекучийКалькулятор - Этот объект
Функция РазделитьНа(Знач ЧисловоеЗначение) Экспорт
Если ЗначениеЗаполнено(Ошибка) Тогда Возврат ЭтотОбъект КонецЕсли;
Делитель = ПривестиЧисло(ЧисловоеЗначение);
Попытка
Значение = Значение / Делитель;
Исключение
Ошибка = ОписаниеОшибки();
КонецПопытки;
Возврат ЭтотОбъект;
КонецФункции
Эта функция будет обрабатывать исключение при невозможности поделить одно число на другое, таким образом останавливая выполнение дальнейших процессов. Попробуем вклинить вызов РазделитьНа в нашу цепочку:
ТекучийКалькулятор = Обработки.ТекучийКалькулятор.Создать();
ТекучийКалькулятор.УстановитьЗначение(0)
.Прибавить(20) // 20
.Отнять(10) // 10
.Прибавить(5) // 15
.РазделитьНа(0) // Ошибка
.Отнять(3);
Ошибка = ТекучийКалькулятор.Ошибка;
Если ЗначениеЗаполнено(Ошибка) Тогда
Результат = Ошибка;
Иначе
Результат = ТекучийКалькулятор.Значение;
КонецЕсли;
// Деление на 0
Сообщить(Результат);
Таким же нехитрым образом, кроме текста ошибки, мы можем сохранить и другую информацию об исключении: например, добавить переменную для записи имени функции, которая привела к ошибке или счетчик, увеличивающийся на 1 после вызова каждой функции цепи, чтобы определить, какое "звено" было последним
Получение результата
Последний небольшой момент на сегодня - получение финального результата. Этот момент не очень важен, если вы пишите обработку для себя, но куда более важен для разработчиков универсальных решений.
Все это связано с той самой инкапсуляцией - ограничением доступа к данным извне для предотвращения манипуляции с ними теми методами и в тех местах, которые для этого не предназначены. Сейчас мы используем переменные с признаком Экспорт, что позволяет напрямую получать и менять их содержимое в любой момент и любым способом. Как правило, это не очень хорошо, так как увеличивает количество способов, которыми можно сломать нашу обработку.
Для избежания такой ситуации лучше сделать переменные не экспортными, а получение и установку значений вынести в экспортные функции. Внутри этих функций мы уже сможем контролировать данные, которые пытаются попасть в поля нашей обработки.
У нас уже есть функция для установки начального значения и там даже есть первая обработка: мы сразу приводим значение к числу, в то время как при наличии экспортных переменных там могло появиться значение любого типа. Остается лишь написать такую же функцию и для получения результата:
// Функция - Получить результат
//
// Возвращаемое значение:
// Структура - Описание результата
// * Ошибка - Булево - Признак ошибки выполнения
// * Данные - Строка,Число - Полученное значение или текст ошибки
Функция ПолучитьРезультат() Экспорт
ЭтоОшибка = ЗначениеЗаполнено(Ошибка);
Возврат Новый Структура("Ошибка,Данные"
, ЭтоОшибка
, ?(ЭтоОшибка, Ошибка, Значение));
КонецФункции
... и немного изменить вызов
ТекучийКалькулятор = Обработки.ТекучийКалькулятор.Создать();
// И никаких экспортных переменных :)
Результат = ТекучийКалькулятор.УстановитьЗначение(0)
.Прибавить(20)
.Отнять(10)
.Прибавить(5)
.РазделитьНа(0)
.Отнять(3)
.ПолучитьРезультат();
При подобном подходе, с исключением возможности напрямую влиять на данные извне, предусмотреть и обработать все возможные исключительные ситуации становится гораздо проще.
В заключение
Вот, собственно, и все. Сегодня немного познакомились с ООП в 1С и рассмотрели основанную на нем реализацию текучего интерфейса. Надеюсь, данный метод поможет вам в написании красивого и лаконичного кода. А полный текст модуля из примеров кода можно найти ниже
Спасибо за внимание!
Другие мои статьи:
Открытый пакет интеграций для популярных API: Telegram, VK, Viber, Bitrix24 и многих других
Open-source набор библиотек интеграции с популярными сервисами: методы для 20-ти популярных API, поставка в виде расширения, OneScript-пакета и даже полноценного приложения для командной строки, подробная документация. И все это абсолютно бесплатно!
Автоматизация редактирования изображений в ImageMagick - это просто!
На что способен ImageMagick и некоторые неочевидные моменты при интеграции его в 1С.
Мой GitHub: https://gitub.com/Bayselonarrend OpenYellow: https://openyellow.notion.site Лицензия MIT: https://mit-license.org