Для разработки сложного программного обеспечения, фирма 1С рекомендует использовать библиотечный подход. Он описан, например, в Системе стандартов и методик разработки. Одной из особенностей этого подхода является использование в библиотеках переопределяемых модулей, содержимое которых должно быть изменено при внедрении библиотеки. Сами разработчики методики признают, что существует
…трудоемкость последующих обновлений переопределяемых модулей в конфигурации-потребителе…
а также
При обновлении версии библиотеки в конфигурации-потребителе особого внимания требуют модули корневого объекта конфигурации и переопределяемые общие модули, так как автоматическое обновление таких «узких мест» конфигурации-потребителя невозможно.
А почему бы нам не сделать то, что кажется невозможным? Попробуем это на простом примере.
Итак, предположим нашей целью является автоматизация процесса продаж. Допустим, что в нашей системе должен быть документ "Продажа" с табличной частью "Товары", содержащей реквизиты "Номенклатура", "Цена", "Количество" и "Сумма". Базовым функционалом будет являться расчет
Сумма = Количество * Цена
Этот расчет – функционал базовой библиотеки, которая будет лежать в основе остальных библиотек.
Возможно нам понадобится расчет налогов. Не вдаваясь в тонкости налогового учета, допустим что налог – это некая сумма, рассчитанная по ставке и просто добавляемая к основной сумме.
Сумма = Сумма * (100 + Ставка) / 100
Аналогичным образом формализуем учет скидок:
Сумма = Сумма * (100 - Скидка) / 100
Каждая из этих формул, а также объекты, содержащие все необходимые для расчета данные, реализованы в своей библиотеке. Но проблема в том, что запуск расчета производится в форме документа, в базовой библиотеке, в которой может быть ничего не известно о функционале, да и вообще о наличии других библиотек. Классический подход подразумевает реализацию в базовой библиотеке переопределяемого модуля расчета суммы, который в итоговой конфигурации должен быть дополнен (изменен) вызовами методов других библиотек.
Но можно поступить иначе. Предположим, в итоговой конфигурации у нас есть некий перечень методов, вызывая которые, мы сделаем все необходимые расчеты. Список этих методов может быть различным, в зависимости от конкретной конфигурации. Каждый метод реализован в модуле своей библиотеки. Все что нужно сделать при запуске расчета в базовой библиотеке – это передать такой перечень механизму, который обработает каждый элемент этого списка. Но где задать нам этот список методов? Опять использовать переопределяемый модуль и получить проблемы с обновлением? А пусть он создается сам, в зависимости от наличия той или иной библиотеки! Давайте придумаем некий механизм, который в зависимости от наличия каких-нибудь объектов метаданных, поймет, какие методы каких модулей надо включить в список для выполнения расчета. Здесь появляется небольшая проблема: для поиска среди объектов метаданных можно использовать только имя, а объекты одного вида с одинаковым именем создавать нельзя. Т.е. нельзя в каждой библиотеке создать свой общий модуль с именем "РасчетСуммы" и объединить их в одну конфигурацию. Но можно создавать подсистемы с одинаковыми именами, если они принадлежат разным родителям. Получается следующая схема:
Для каждой библиотеки создана своя подсистема, в которой есть подсистема с именем "РасчетСумм". В составе последних включены общие модули, в которых реализован экспортный метод, выполняющий расчет (имена методов также одинаковы). Подсистемы библиотек лежат в пределах одной группы, для упрощения поиска. Таким образом, ориентируясь на имя подсистем "РасчетСумм" мы легко можем получить список модулей и методов для проведения расчета.
Несколько слов по поводу выполнения этих методов. Самый простой вариант – создание некоего "менеджера", который будет последовательно вызывать каждую процедуру. Но я решил остановиться на другом способе – вызывать самую последнюю процедуру из списка и передавать ей управление. Решение о вызове предыдущего метода полностью возлагается на эту процедуру. Такой вариант более гибок – предыдущий метод может быть вызван в любом месте, а может и быть просто проигнорирован, если в нем нет нужды. Порядок методов в списке определяется порядком расположения подсистем библиотек внутри "ПереопределяемыеОбъектыБиблиотек".
Как это выглядит на практике:
В документе "Продажа" реализована команда:
&НаСервере
Процедура РассчитатьНаСервере(ИД)
ПараметрыРасчета = Объект.Товары.НайтиПоИдентификатору(ИД);
Последовательность = УправлениеБиблиотекамиКлиентСервер.ПоследовательностьМодулейПроцедур("РасчетСумм");
УправлениеБиблиотеками.ВыполнитьПредыдущуюПроцедуруНаСервере(ПараметрыРасчета, Последовательность);
КонецПроцедуры
&НаКлиенте
Процедура Рассчитать(Команда)
ТекущиеДанные = Элементы.Товары.ТекущиеДанные;
Если ТекущиеДанные <> Неопределено Тогда
РассчитатьНаСервере(ТекущиеДанные.ПолучитьИдентификатор());
КонецЕсли;
КонецПроцедуры
В серверной процедуре получаем последовательность описаний методов и запускаем выполнение последнего из них. В нашем случае это будет РасчетСуммСоСкидкой.РасчетСумм
Процедура РасчетСумм(Параметр,Последовательность)Экспорт
УправлениеБиблиотеками.ВыполнитьПредыдущуюПроцедуруНаСервере(Параметр, Последовательность);
Скидка = РегистрыСведений.СкидкиНоменклатуры.Получить(новый Структура("Номенклатура",Параметр.Номенклатура)).Скидка;
Параметр.Сумма = Параметр.Сумма * (100 - Скидка) / 100;
КонецПроцедуры
В процессе расчета мы сначала выполняем предыдущий метод РасчетСуммСНалогом.РасчетСумм, а затем производим обработку скидки.
Перед налоговым расчетом мы так же вызываем предыдущий метод РасчетСуммБазовый.РасчетСумм:
Процедура РасчетСумм(Параметр,Последовательность)Экспорт
УправлениеБиблиотеками.ВыполнитьПредыдущуюПроцедуруНаСервере(Параметр, Последовательность);
Параметр.Сумма = Параметр.Сумма * (100 + Константы.СтавкаНалога.Получить()) / 100;
КонецПроцедуры
В котором выполняем самый первый расчет.
Процедура РасчетСумм(Параметр,Последовательность)Экспорт
УправлениеБиблиотеками.ВыполнитьПредыдущуюПроцедуруНаСервере(Параметр, Последовательность);
Параметр.Сумма = Параметр.Количество * Параметр.Цена;
КонецПроцедуры
в последнем случае метод УправлениеБиблиотеками.ВыполнитьПредыдущуюПроцедуруНаСервере не выполнит ничего, так как предыдущих процедур не осталось.
Описанный выше механизм можно применять и для других целей, например для создания общего списка строковых или табличных ресурсов. Вот так реализовано описание конфигурации, которое создается на основе табличных макетов, принадлежащих разным библиотекам:
В обработке "Инфо" создается общий табличный документ:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ОписанияМакетов = УправлениеБиблиотекамиВызовСервера.ДопОбъектыПоВиду("Инфо");
Для каждого ОписаниеМакета Из ОписанияМакетов Цикл
Таблица.Вывести(ПолучитьОбщийМакет(ОписаниеМакета.ИмяОбъекта));
КонецЦикла;
КонецПроцедуры
Как внедрять или обновлять получившиеся библиотеки.
Процесс внедрения и обновления библиотек – обычное сравнение объединение конфигураций. В этом режиме надо отметить объекты по подсистемам файла:
и выполнить объединение. При этом никаких других действий, таких как редактирования модулей, делать не надо.
Подобное объединение можно проводить в автоматическом режиме. Для этого служит специальная команда пакетного режима конфигуратора. Пример такой команды:
1cv8.exe DESIGNER /F"c:asesprod" /MergeCfg"c:aseslib2.cf" -Settings"c:asesUpdLib2Settings.xml"
здесь c:asesprod — путь к файловой базе, c:aseslib1.cf — конфигурация библиотеки, c:asesUpdLib1Settings.xml — файл настроек объединения.
Файл настроек нужен для того, чтобы указать платформе, какие объекты следует объединить и правила такого объединения. Описание формата файла: https://its.1c.ru/db/v8314doc#bookmark:adm:TI000000713 . Пример файла:
<?xml version="1.0" encoding="UTF-8"?>
<Settings xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://v8.1c.ru/8.3/config/merge/settings" version="1.1">
<Parameters>
<AllowMainConfigurationObjectDeletion>true</AllowMainConfigurationObjectDeletion>
</Parameters>
<Objects>
<Configuration>
<MergeRule>DoNotMerge</MergeRule>
</Configuration>
<Object fullNameInSecondConfiguration="Подсистема.Скидки">
<MergeRule>GetFromSecondConfiguration</MergeRule>
<Subsystem configuration="Second" includeObjectsFromSubordinateSubsystems="true">
<MergeRule>GetFromSecondConfiguration</MergeRule>
</Subsystem>
</Object>
<Object fullName="Подсистема.Скидки">
<MergeRule>GetFromSecondConfiguration</MergeRule>
<Subsystem configuration="Main" includeObjectsFromSubordinateSubsystems="true">
<MergeRule>GetFromSecondConfiguration</MergeRule>
</Subsystem>
</Object>
<Object fullNameInSecondConfiguration="Подсистема.ПереопределяемыеОбъектыБиблиотек.Подсистема.Скидки">
<MergeRule>GetFromSecondConfiguration</MergeRule>
<Subsystem configuration="Second" includeObjectsFromSubordinateSubsystems="true">
<MergeRule>GetFromSecondConfiguration</MergeRule>
</Subsystem>
</Object>
<Object fullName="Подсистема.ПереопределяемыеОбъектыБиблиотек.Подсистема.Скидки">
<MergeRule>GetFromSecondConfiguration</MergeRule>
<Subsystem configuration="Main" includeObjectsFromSubordinateSubsystems="true">
<MergeRule>GetFromSecondConfiguration</MergeRule>
</Subsystem>
</Object>
</Objects>
</Settings>
здесь мы указываем, что не меняем свойства конфигурации, разрешаем удалять объекты, помечаем для объединения объекты нужных нам подсистем. Обратите внимание, что каждую подсистему мы указываем дважды: в новой конфигурации (для добавленных и измененных объектов) и в основной конфигурации (для измененных и удаленных объектов).
В результате обновления мы получим итоговую конфигурацию, в которой может быть (а может и не быть) одна или две библиотеки. Существуют несложные средства, которые позволят нам так же автоматически поместить эти изменения в хранилище 1С или же в Git.
Итак, вот что мы получили: базовую библиотеку и две дополнительные библиотеки, на основе которых можем в автоматическом режиме собрать четыре разные конфигурации для разных заказчиков. Эти конфигурации различаются по функциональным возможностям и могут иметь разную продажную стоимость, что позволит более гибко составлять коммерческие предложения.
Конечно же, предлагаемый подход не панацея. Так, он не поможет при работе с определяемыми типами, с переопределяемыми макетами и некоторыми другими объектами, но облегчить работу он может значительно. Надо отметить, что и стандартные возможности платформы включают в себе нечто подобное, хотя и менее функциональное. Это — подписки на события.
Рассмотренный пример вы можете увидеть в приложенном к статье файле. Это архив, содержащий в себе:
- Папка base – базовая библиотека
- Папки lib1, lib2 – 2 библиотеки
- Папка prod – итоговая конфигурация
- 1_CreateBase.bat – пакетный файл для создания итоговой конфигурации на основе базовой библиотеки
- 2_AddLib1.bat, 3_AddLib2.bat – пакетные файлы для первоначального внедрения в итоговую конфигурацию библиотек lib1, lib2
- 4_UpdLib1.bat, 5_UpdLib2.bat – пакетные файлы для обновления в итоговой конфигурации библиотек lib1, lib2
- *Settings.xml – файлы с настройками объединения
Желательно чтобы все эти папки и файлы находились в каталоге c:ases, так как в пакетных файлах используются абсолютные пути. Также надо указать путь к файлу 1cv8.exe в зависимости от версии платформы.
где и как определяется порядок выполнения процедур
(1)
Первой вызывается последняя процедура списка. В каждой процедуре вызов предыдущей может располагаться как в начале, до ее выполнения, так и в конце, как впрочем и в любом другом месте. Вызов предыдущей процедуры может и вовсе отсутствовать.
Супер!
Вы изобрели наследование классов в 1с 🙂
кстати — в аксапте именно super() вызывает родительский класс (из вышестоящего слоя)
см. напр
(3)Спасибо за ссылки! Все-таки это не совсем (а точнее совсем не) наследование классов. У меня не было цели моделировать соответствующий подход ООП. В Вашем примере, насколько я понял, дочерний класс унаследован от родительского и точно известно, метод какого родительского класса будет выполнен при вызове из дочернего. В моем же случае итоговая конфигурация может быть собрана из произвольного набора библиотек, поэтому вызов «предыдущего» метода означает вызов метода какой-то библиотеки, которая стоит предыдущей в последовательности. Больше всего это похоже на механизм hook-ов или подписок на события в 1С – мы выполняем свою реализацию какого-то существующего функционала, и при необходимости передаем управление дальше по цепочке стандартному обработчику. Или наоборот – выполняем стандартный обработчик, а потом свой. Причем в общем случае мы не знаем ни о наличии других обработчиков, ни об их порядке.