Предисловие
Когда я впервые услышал про принципы SOLID в контексте 1С, моя реакция была простой: "Что это такое?" И, судя по всему, не у меня одного. Стоит завести разговор о SOLID среди 1С-разработчиков, как в ответ, либо недоуменные взгляды, либо попытки найти хоть что-то в интернете, после которых вопросов становится только больше. Сегодня эти слова всё чаще мелькают на собеседованиях, в требованиях компаний и в поисковых строках разработчиков. Материалов, которые бы ясно объясняли, как эти принципы работают в нашей платформе, почти нет. В лучшем случае попадаются общие материалы про ООП, не учитывающие специфику платформы. В худшем, разрозненные публикации, оставляющие больше вопросов, чем ответов. И вот вы уже сидите, уставившись в экран, пытаясь понять, как эти принципы вообще применимы к 1С? А ведь на самом деле SOLID давно живёт в типовых конфигурациях.
Интерес к этим принципам растёт, и не зря. Мы всё чаще сталкиваемся с кодом, который превращается в хаос: процедуры, переплетённые как клубок, где правка одной строки в методе рушит другой модуль, а доработка запроса тянет за собой часы отладки. Был похожий опыт? Или хоть раз приходили мысли: "Всё, пора искать новую работу, этот код меня доконает"? Начинающие разработчики смотрят на такой "наследственный" беспорядок и теряются, как писать "правильно", если вокруг нет ориентиров? А те, кто заглядывал в типовые решения, задаются вопросом: почему они такие сложные, но при этом так надёжно работают и масштабируются?
Эта статья для опытных разработчиков, которые ищут способ укротить сложные проекты и сделать их гибкими. Для новичков, стремящихся с самого начала заложить крепкую основу. Для тех, кто хочет углубиться и понять, какие принципы помогают сложным конфигурациям оставаться устойчивыми и масштабируемыми. Для всех нас, кто не любит "заплатки" в коде в духе "и так сойдет", и хочет понять, как принципы SOLID могут в этом помочь. Мы разберем каждый принцип SOLID на примерах 1С-разработки, который вы сможете взять за основу для своих задач. Хочется подчеркнуть, что все примеры здесь, лишь иллюстрации принципов, и не для того, чтобы кого-то задеть.
Это про наше общее желание сделать код чище, а работу приятнее. И если после этих страниц у вас появится ощущение или даже легкий азарт, что SOLID не просто модное слово, а реальный путь к порядку, значит, всё было не зря. Эту статью можно читать не целиком сразу, а по частям, по блокам. Начнем.
Что такое SOLID
SOLID — это пять принципов объектно-ориентированного программирования, которые помогают создавать код, способный расти, меняться и оставаться понятным.
Вместе эти правила формируют подход, где структура программы становится не просто набором строк, а продуманной системой.
Зачем это нужно? Программирование, это не только написание кода, но и его развитие. Без чётких принципов даже простая задача может превратиться в головоломку, когда требования меняются, а сроки поджимают. SOLID решает эту проблему, он даёт инструменты для написания кода, который легко поддерживать, расширять и передавать другим. Это не абстрактная теория, а практическая основа, которая помогает избежать путаницы и сделать разработку предсказуемой.
Начнём с первого принципа — и посмотрим, как одна простая мысль меняет всё.
Посмотрите на этот чайник, он стоит на столе, в нём букет цветов, плавают рыбки. Звучит, как бред? А теперь представьте, что это ваш код в 1С — один модуль или метод, который считает налоги, рисует интерфейс и ещё логи пишет. Примерно так выглядят многие проекты на 1С. "Принцип единственной ответственности" обещает порядок. Узнаем, как выгнать рыбок из чайника.
Описание
Первый принцип SOLID звучит: у каждого модуля, класса или объекта должна быть только одна причина для изменения. Проще говоря, одна чётко определённая задача. Это значит, что чайник должен только кипятить воду, а не быть также сразу в роли аквариума или вазы. В коде это работает так же, если модуль или метод отвечает за расчёт налогов, он не должен параллельно формировать отчёты или работать с настройками прав пользователей.
Важно понимать, что "одна ответственность", это не совсем про одну строку кода и не про крошечные функции. Это про логическую цель. Например, валидация данных, введённых пользователем, — это одна задача. Сохранение их в базу — уже другая. Смешивать их в одном модуле или методе — всё равно что ситуация на изображении.
Применение в 1С
Данный принцип в 1С тесно связан с архитектурой платформы, которая изначально предполагает распределение обязанностей между различными типами модулей и объектов конфигурации, где каждый элемент системы отвечает за свою задачу, минимизируя смешивание обязанностей.
Объекты конфигурации имеют чётко определённые роли и назначение, что само по себе является примером применения принципа на уровне архитектуры.
Также платформа предоставляет разные типы модулей, каждый из которых, в свою очередь, тоже имеет свою чётко определённую роль.
Нарушение принципа:
Посмотрим на метод, который обрабатывает заказ клиента. Он проверяет сумму, меняет статус, пишет лог и создаёт файл. Казалось бы, всё логично, но давайте разберём, почему этот 'монстр' — тот самый чайник, в котором цветы завянут, рыбкам тоже недолго жить и чай получится плохой. Вот он:
// Вызывающий код
Процедура ОбработатьЗаказХаос(ЗаказСсылка)
ЗаказОбъект = ЗаказСсылка.ПолучитьОбъект();
// 1. Проверка суммы с влиянием на статус
Если ЗаказОбъект.СуммаДокумента < 0 Тогда
ТекущийСтатус = Перечисления.СтатусыЗаказов.Отменен;
Иначе
ТекущийСтатус = Перечисления.СтатусыЗаказов.Подтвержден;
КонецЕсли;
// 2. Заполнение реквизитов, формирование комментария, запись объекта
ЗаказОбъект.Статус = ТекущийСтатус;
ЗаказОбъект.Комментарий = "Статус: " + ТекущийСтатус + ", Сумма: " + ЗаказОбъект.СуммаДокумента;
ЗаказОбъект.Записать();
// 3. Запись лога, зависящего от статуса и комментария
Лог = РегистрыСведений.ЛогЗаказов.СоздатьМенеджерЗаписи();
Лог.Дата = ТекущаяДата();
Лог.Документ = ЗаказСсылка;
Лог.Событие = ТекущийСтатус + " (" + Лев(ЗаказОбъект.Комментарий, 10) + ")";
Лог.Записать();
// 4. Создание файла, зависящего от лога и статуса
ТекстФайла = Новый ТекстовыйДокумент;
ТекстФайла.УстановитьТекст(ЗаказОбъект.Комментарий + ", Лог: " + Лог.Событие);
ИмяФайла = "C:\Orders\" + ЗаказОбъект.Номер + "_" + ТекущийСтатус + ".txt";
ТекстФайла.Записать(ИмяФайла);
КонецПроцедуры
Что плохого в этом примере:
Множественные обязанности:
Процедура выполняет сразу несколько разнородных задач, проверку суммы заказа, установку статуса, логирование и создание файла. Это нарушает принцип, так как у метода более одной причины для изменения.
Высокая связанность кода:
Логика проверки суммы, изменения статуса, записи лога и создания файла тесно переплетена. Как пример, переменная "ТекущийСтатус" используется на всех этапах, что создает зависимость между блоками. Изменение в одной части вынуждает переписывать всю процедуру.
Сложность поддержки и тестирования:
Из-за смешивания разных функций в одном месте код трудно читаемый и тестируемый. Например, чтобы протестировать логику проверки суммы, нужно учитывать влияние на логирование и создание файла, что усложняет изоляцию ошибок.
Следование принципу:
Давайте теперь прислушаемся к этому принципу и перестроим код в соответствии с ним. Разделим обязанности между модулями и посмотрим, как это работает. Напомню, что необязательно делить код так, чтобы методы состояли из одной строки, главное, чтобы они выполняли одну логическую задачу, но для яркости примера, максимально разделим ответственность между модулями и методами:
ОбщийМодуль "УправлениеЗаказами":
// ОбщийМодуль "УправлениеЗаказами"
Функция ОпределитьСтатус(ЗаказОбъект) Экспорт
Результат = Перечисления.СтатусыЗаказов.Подтвержден;
Если ЗаказОбъект.СуммаДокумента < 0 Тогда
Результат = Перечисления.СтатусыЗаказов.Отменен;
КонецЕсли;
Возврат Результат;
КонецФункции
Функция СформироватьКомментарий(ЗаказОбъект) Экспорт // если предполагается одинаковая логика формирования комментария для всех заказов системы, иначе можно оставить в служебных методах вызывающего модуля
Результат = "Статус: " + ЗаказОбъект.Статус + ", Сумма: " + ЗаказОбъект.СуммаДокумента;
Возврат Результат;
КонецФункции
ОбщийМодуль "УправлениеДокументами":
// ОбщийМодуль "УправлениеДокументами"
Процедура ЗаписатьДокумент(ДокументОбъект) Экспорт // единая точка для записи всех документов, сейчас это одна строка, но метод имеет потенцинал для расширения
ДокументОбъект.Записать();
КонецПроцедуры
ОбщийМодуль "Логирование":
// ОбщийМодуль "Логирование"
Процедура ЗаписатьЛог(ДокументСсылка, Событие, ДатаЛога) Экспорт
Лог = РегистрыСведений.ЛогЗаказов.СоздатьМенеджерЗаписи();
Лог.Дата = ДатаЛога;
Лог.Документ = ДокументСсылка;
Лог.Событие = Событие;
Лог.Записать();
КонецПроцедуры
ОбщийМодуль "ЭкспортФайлов":
// ОбщийМодуль "ЭкспортФайлов"
Процедура СоздатьФайл(ТекстФайла, ИмяФайла) Экспорт
ТекстовыйДокумент = Новый ТекстовыйДокумент;
ТекстовыйДокумент.УстановитьТекст(ТекстФайла);
ТекстовыйДокумент.Записать(ИмяФайла);
КонецПроцедуры
Вызывающий код:
// Вызывающий код
Процедура ОбработатьЗаказПорядок(ЗаказСсылка)
ЗаказОбъект = ЗаказСсылка.ПолучитьОбъект();
Статус = УправлениеЗаказами.ОпределитьСтатусЗаказа(ЗаказОбъект);
Комментарий = УправлениеЗаказами.СформироватьКомментарий(ЗаказОбъект);
ЗаказОбъект.Статус = Статус;
ЗаказОбъект.Комментарий = Комментарий;
УправлениеДокументами.ЗаписатьДокумент(ЗаказОбъект);
Событие = СформироватьСобытие(Статус, Комментарий);
ДатаЛога = ТекущаяДата();
Логирование.ЗаписатьЛог(ЗаказСсылка, Событие, ДатаЛога);
ТекстФайла = СформироватьТекстФайла(Комментарий, Событие);
ИмяФайла = СформироватьИмяФайла(ЗаказОбъект, Статус);
ЭкспортФайлов.СоздатьФайл(ТекстФайла, ИмяФайла);
КонецПроцедуры
// Служебные методы модуля вызывающего кода, считаем, что методы специфичны только для конкретного случая, однако если их логика может быть общей, следует вынести в соответствующие общие модули
Функция СформироватьСобытие(Статус, Комментарий)
Результат = Статус + " (" + Лев(Комментарий, 10) + ")";
Возврат Результат;
КонецФункции
Функция СформироватьТекстФайла(Комментарий, Событие) Экспорт
Результат = Комментарий + ", Лог: " + Событие;
Возврат Результат;
КонецФункции
Функция СформироватьИмяФайла(ДокументОбъект, Статус) Экспорт
Результат = "C:\Orders\" + ДокументОбъект.Номер + "_" + Статус + ".txt";
Возврат Результат;
КонецФункции
Что решилось после исправления:
Соответствие принципу:
Каждая задача вынесена в отдельный метод с одной причиной для изменения. Например, метод "ЗаписатьЛог" отвечает только за запись лога, "СоздатьФайл" — только за запись файла и т.д.. Это делает код предсказуемым и устойчивым к изменениям.
Снижение связанности:
Методы изолированы и общаются через чётко определённые параметры. Например, "СоздатьФайл" не знает, откуда берётся текст или имя файла, они передаются извне. Это снижает зависимость между частями кода и упрощает их переиспользование.
Улучшение читаемости и тестируемости:
Каждый метод короткий и выполняет одну функцию. Например, "ОпределитьСтатус" можно протестировать отдельно, подставляя разные суммы, без необходимости создавать файлы или писать логи. Это делает код понятным и удобным для unit-тестов.
Гибкость и расширяемость:
Логика изолирована в методы и функции. Например, изменение формата комментария требует правки только "СформироватьКомментарий", а смена пути к файлу, только "СформироватьИмяФайла". Это делает код легко адаптируемым к новым требованиям.
1. УчетНДС.НоваяСтрокаИтоговКнигиПокупок()
- Одна задача: создаёт и возвращает структуру с фиксированным набором ключей и значений, инициализированных нулями. Это единственная ответственность.
- Одна причина для изменения: изменится только при изменении структуры итогов книги покупок (например, если нужно добавить или убрать ключ). Других причин для модификации нет.
- Изолированность: не зависит от внешних данных, не выполняет побочных действий и не смешивает разные обязанности (например, заполнение данными).
// Возвращает структуру итогов книги покупок.
//
// Возвращаемое значение:
// Структура - Содержит ключи для строки итогов книги покупок.
//
Функция НоваяСтрокаИтоговКнигиПокупок() Экспорт
Результат = Новый Структура();
Результат.Вставить("ВсегоПокупок", 0);
Результат.Вставить("НДС", 0);
Результат.Вставить("СуммаБезНДС18", 0);
Результат.Вставить("НДС18", 0);
Результат.Вставить("СуммаБезНДС10", 0);
Результат.Вставить("НДС10", 0);
Результат.Вставить("НДС0", 0);
Результат.Вставить("СуммаСовсемБезНДС", 0);
Возврат Результат;
КонецФункции
2. ДоставкаТоваров.СпособыДоставкиПеревозчикомОтОтправителя()
- Одна задача: создаёт и возвращает массив с фиксированным набором элементов перечисления СпособыДоставки, связанных с доставкой от отправителя. Это единственная ответственность.
- Одна причина для изменения: изменится только при изменении списка способов доставки (например, добавление или удаление способа). Других причин для модификации нет.
- Изолированность: не зависит от внешних данных, не выполняет побочные действия и не смешивает разные обязанности (например, обработку).
// Возвращаемое значение:
// Массив из ПеречислениеСсылка.СпособыДоставки
//
Функция СпособыДоставкиПеревозчикомОтОтправителя()
СпособыДоставкиПеревозчикомОтОтправителя = Новый Массив; // Массив из ПеречислениеСсылка.СпособыДоставки
СпособыДоставкиПеревозчикомОтОтправителя.Добавить(Перечисления.СпособыДоставки.СиламиПеревозчикаДоНашегоСклада);
СпособыДоставкиПеревозчикомОтОтправителя.Добавить(Перечисления.СпособыДоставки.ОтОтправителяОпределяетСлужбаДоставки);
СпособыДоставкиПеревозчикомОтОтправителя.Добавить(Перечисления.СпособыДоставки.ПоручениеЭкспедиторуВПункте);
Возврат СпособыДоставкиПеревозчикомОтОтправителя;
КонецФункции
3. ОбщегоНазначенияУТ.РазмерВременнойТаблицы()
- Одна задача: подсчитывает и возвращает количество записей во временной таблице, выполняя только необходимые шаги (создание запроса, его выполнение, извлечение результата). Это единственная ответственность.
- Одна причина для изменения: изменится только при изменении логики подсчёта записей (например, изменение синтаксиса запроса). Других причин для модификации нет.
- Изолированность: не имеет побочных эффектов, не изменяет состояние системы, не смешивает разные обязанности (например, создание таблицы), хотя зависит от входных параметров, что ожидаемо для её задачи.
// Возвращает количество записей во временной таблице
//
// Параметры:
// МенеджерВременныхТаблиц - МенеджерВременныхТаблиц -
// ИмяВременнойТаблицы - Строка -
//
// Возвращаемое значение:
// Число - Количество записей
//
Функция РазмерВременнойТаблицы(МенеджерВременныхТаблиц, ИмяВременнойТаблицы) Экспорт
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| КОЛИЧЕСТВО(*) КАК КоличествоСтрок
|ИЗ
| &ИмяТаблицы КАК Т";
Запрос.Текст = СтрЗаменить(Запрос.Текст, "&ИмяТаблицы", ИмяВременнойТаблицы);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Возврат Выборка.КоличествоСтрок;
КонецФункции
1. ОбщегоНазначенияУТ.ЗаписатьВЖурналСообщитьПользователю()
- Выполняет несколько задач:
- Записывает событие в журнал регистрации, формируя запись с переданными параметрами (уровень, имя события, комментарий и т.д.).
- Сообщает пользователю об ошибке или предупреждении (если уровень журнала — Ошибка или Предупреждение), вызывая ОбщегоНазначенияКлиентСервер.СообщитьПользователю.
- Имеет несколько причин для изменения:
- Изменение логики записи в журнал: например, добавление новых полей в запись журнала или изменение формата комментария (замена ПодробноеПредставлениеОшибки на другой метод).
- Изменение логики уведомления пользователя: например, смена способа вывода сообщений (замена ОбщегоНазначенияКлиентСервер.СообщитьПользователю на другой, т.к. текущий метод устаревший) или изменение формата текста сообщения.
- Изменение условий уведомления: например, добавление уведомления для уровня Информация или исключение уведомления для уровня Предупреждение.
- Не изолирован:
- Зависит от внешнего механизма уведомления пользователя (ОбщегоНазначенияКлиентСервер.СообщитьПользователю), что связывает его с клиент-серверным взаимодействием.
- Смешивает разные обязанности: логирование (серверная операция) и уведомление пользователя (клиентская операция).
- Выполняет побочные действия: помимо записи в журнал, генерирует сообщения пользователю, что является отдельной ответственностью.
// Процедура делает запись в журнал регистрации и сообщает пользователю, если это сообщение об ошибке
// Параметры:
// ПараметрыЖурнала - Структура - параметры записи в журнал регистрации:
// * ГруппаСобытий - Строка - префикс для имени события журнала регистрации
// * Метаданные - ОбъектМетаданных - метаданные для записи в журнал регистрации
// * Данные - Произвольный - данные для записи в журнал регистрации
// УровеньЖурнала - УровеньЖурналаРегистрации - Уровень журнала регистрации
// ИмяСобытия - Строка - имя события (в журнал событие записывается в формате ГруппаСобытий.ИмяСобытия)
// Комментарий - Строка - комментарий о событии
// ИнформацияОбОшибке - ИнформацияОбОшибке -
// - Строка - Информация об ошибке, которую так же необходимо задокументировать в комментарии журнала регистрации.
//
Процедура ЗаписатьВЖурналСообщитьПользователю(ПараметрыЖурнала, УровеньЖурнала, ИмяСобытия, Знач Комментарий = "", ИнформацияОбОшибке = Неопределено) Экспорт
Если ТипЗнч(ИнформацияОбОшибке) = Тип("ИнформацияОбОшибке") Тогда
Если Комментарий = "" Тогда
ТестСообщенияПользователю = КраткоеПредставлениеОшибки(ИнформацияОбОшибке);
Комментарий = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке);
Иначе
ТестСообщенияПользователю = Комментарий + Символы.ПС + КраткоеПредставлениеОшибки(ИнформацияОбОшибке);
Комментарий = Комментарий + Символы.ПС + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке);
КонецЕсли;
Иначе
Если ТипЗнч(ИнформацияОбОшибке) = Тип("Строка")
И Не ПустаяСтрока(ИнформацияОбОшибке) Тогда
Если Комментарий = "" Тогда
Комментарий = ИнформацияОбОшибке;
Иначе
Комментарий = Комментарий + Символы.ПС + ИнформацияОбОшибке;
КонецЕсли;
КонецЕсли;
ТестСообщенияПользователю = Комментарий;
КонецЕсли;
// Журнал регистрации
УстановитьПривилегированныйРежим(Истина);
ЗаписьЖурналаРегистрации(
СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(
НСтр("ru = '%1'", ОбщегоНазначения.КодОсновногоЯзыка()),
ПараметрыЖурнала.ГруппаСобытий + ?(ИмяСобытия = "", "", "." + ИмяСобытия)),
УровеньЖурнала,
ПараметрыЖурнала.Метаданные,
ПараметрыЖурнала.Данные,
Комментарий);
УстановитьПривилегированныйРежим(Ложь);
Если УровеньЖурнала = УровеньЖурналаРегистрации.Ошибка
Или УровеньЖурнала = УровеньЖурналаРегистрации.Предупреждение Тогда
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(СокрЛП(ТестСообщенияПользователю),ПараметрыЖурнала.Данные);
КонецЕсли;
КонецПроцедуры
2. ЗаказыСервер.ОтменитьНеотработанныеСтрокиПоОтгрузке()
- Выполняет несколько задач:
- Создает и описывает структуру таблицы ТаблицаВыбранныхСтрок для хранения данных о строках документа.
- Заполняет таблицу данными из табличной части документа.
- Распределяет количество по накладным.
- Распределяет количество по ордерам.
- Выполняет свертку таблицы.
- Переносит отмененные строки в документ.
- Имеет несколько причин для изменения:
- Изменение структуры таблицы ТаблицаВыбранныхСтрок.
- Изменение логики фильтрации строк: например, изменение условий для исключения строк (сейчас исключаются строки с ВариантОбеспечения = ПереданРанее).
- Изменение логики распределения количества по накладным.
- Изменение логики распределения количества по ордерам.
- Изменение логики свертки таблицы.
- Изменение логики переноса строк в документ.
- Не изолирован:
- Зависит от внешних данных и модулей: регистр накопления ТоварыКОтгрузке, НакладныеСервер, справочник Назначения и др.
- Выполняет побочные действия: изменяет состояние документа через ПеренестиВТаблицуДокументаОтмененныеСтроки, что является модификацией внешнего объекта.
- Смешивает разные обязанности: создание таблицы, обработка данных, распределение количества, модификация документа — это разные уровни ответственности, которые должны быть разделены.
// Параметры:
// ДокументОбъект - ДокументОбъект
// ПараметрыЗаполнения - см. ПараметрыЗаполненияДляОтменыСтрок
// ПараметрыОтмены - см. ПараметрыОтменыСтрокЗаказов
// Возвращаемое значение:
// см. ЗаказыСервер.РезультатОтменыНеотработанныхСтрок
//
Функция ОтменитьНеотработанныеСтрокиПоОтгрузке(ДокументОбъект, ПараметрыЗаполнения, ПараметрыОтмены) Экспорт
// 1. Описание таблицы
ТаблицаВыбранныхСтрок = Новый ТаблицаЗначений();
ТаблицаВыбранныхСтрок.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("ДокументСсылка." + ДокументОбъект.Ссылка.Метаданные().Имя));
ТаблицаВыбранныхСтрок.Колонки.Добавить("КодСтроки", Новый ОписаниеТипов("Число"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("Характеристика", Новый ОписаниеТипов("СправочникСсылка.ХарактеристикиНоменклатуры"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("Склад", Новый ОписаниеТипов("СправочникСсылка.Склады"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("Серия", Новый ОписаниеТипов("СправочникСсылка.СерииНоменклатуры"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("Назначение", Новый ОписаниеТипов("СправочникСсылка.Назначения"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("НазначениеСклада", Новый ОписаниеТипов("СправочникСсылка.Назначения")); // без старых назначений
ТаблицаВыбранныхСтрок.Колонки.Добавить("Идентификатор", Новый ОписаниеТипов("Число"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("КоличествоВНакладной", Новый ОписаниеТипов("Число"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("КоличествоВОрдере", Новый ОписаниеТипов("Число"));
ТаблицаВыбранныхСтрок.Колонки.Добавить("Порядок", Новый ОписаниеТипов("Число")); // для адекватного распределения ордеров
// 2. Заполнение данными документа
Идентификатор = 0;
ЕстьТаблицаЗамен = ЗначениеЗаполнено(ПараметрыЗаполнения.ТаблицаЗамен);
ДокументОбъектТЧ = ДокументОбъект[ПараметрыЗаполнения.ИмяТабличнойЧасти]; // ТабличнаяЧасть
Для Каждого Строка Из ДокументОбъектТЧ Цикл
Если (ПараметрыОтмены.УдалятьСтроки Или Не Строка.Отменено)
И Не Строка.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.ПереданРанее Тогда
НоваяСтрока = ТаблицаВыбранныхСтрок.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Строка);
НоваяСтрока.Ссылка = ДокументОбъект.Ссылка;
НоваяСтрока.Идентификатор = Идентификатор;
Для каждого ПутьКДанным Из ПараметрыЗаполнения.ПутиКДанным Цикл
НоваяСтрока[ПутьКДанным.Ключ] = ДокументОбъект[ПутьКДанным.Значение];
КонецЦикла;
Если ЕстьТаблицаЗамен Тогда
ЗаполнитьЗначенияСвойств(НоваяСтрока, ПараметрыЗаполнения.ТаблицаЗамен[Строка.НомерСтроки-1]);
КонецЕсли;
Если Строка.ВариантОбеспечения <> Перечисления.ВариантыОбеспечения.Отгрузить Или Не Строка.Обособленно Тогда
НоваяСтрока.Назначение = Справочники.Назначения.ПустаяСсылка();
КонецЕсли;
Если Строка.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.Отгрузить Тогда
НоваяСтрока.Порядок = 0;
ИначеЕсли Строка.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.СоСклада Тогда
НоваяСтрока.Порядок = 1;
Иначе
НоваяСтрока.Порядок = 2;
КонецЕсли;
КонецЕсли;
Идентификатор = Идентификатор + 1;
КонецЦикла;
ТаблицаВыбранныхСтрок.Сортировать("Порядок");
СвойстваНазначений = Справочники.Назначения.СвойстваНазначений(ТаблицаВыбранныхСтрок.ВыгрузитьКолонку("Назначение"));
Для каждого СтрокаТаблицы Из ТаблицаВыбранныхСтрок Цикл
Если ЗначениеЗаполнено(СтрокаТаблицы.Назначение)
И СвойстваНазначений[СтрокаТаблицы.Назначение].УчитываетсяВСкладскойПодсистеме Тогда
СтрокаТаблицы.НазначениеСклада = СтрокаТаблицы.Назначение;
КонецЕсли;
КонецЦикла;
// 3. Распределение количества накладных
ОформленоПоНакладным = ПараметрыЗаполнения.МенеджерРегистра.ТаблицаОформлено(ТаблицаВыбранныхСтрок, ПараметрыЗаполнения.ОтборПоИзмерениям);
КоличествоКолонка = ОформленоПоНакладным.Колонки.Количество; // КолонкаТаблицыЗначений
КоличествоКолонка.Имя = "КоличествоВНакладной";
Условие = "ПО [Количество]";
Ключ = "КодСтроки";
НакладныеСервер.РаспределитьКоличество(ОформленоПоНакладным, ТаблицаВыбранныхСтрок, "КоличествоВНакладной", Ключ, Условие, Истина);
// 4. Распределение количества ордеров
Корректировка = ТаблицаВыбранныхСтрок.Скопировать(); // пустая таблица для передачи в регистры
Корректировка.Колонки.Добавить("КОтгрузке", Новый ОписаниеТипов("Число"));
ТаблицаВыбранныхСтрок.Колонки.Назначение.Имя = "НазначениеДокумента";
ТаблицаВыбранныхСтрок.Колонки.НазначениеСклада.Имя = "Назначение";
ОформленоПоОрдерам = РегистрыНакопления.ТоварыКОтгрузке.ТаблицаОформлено(ТаблицаВыбранныхСтрок, Корректировка, Истина);
КоличествоКолонка = ОформленоПоОрдерам.Колонки.Количество; // КолонкаТаблицыЗначений
КоличествоКолонка.Имя = "КоличествоВОрдере";
// Для взаимосвязанного распределения количества ордеров учтем предшествующее распределение количества накладных по кодам строк
Условие = "ПО [КоличествоВНакладной]";
Ключ = "Номенклатура, Характеристика, Серия, Назначение";
НакладныеСервер.РаспределитьКоличество(ОформленоПоОрдерам, ТаблицаВыбранныхСтрок, "КоличествоВОрдере", Ключ, Условие);
Условие = "ПО [Количество]";
Ключ = "Номенклатура, Характеристика, Серия, Назначение";
НакладныеСервер.РаспределитьКоличество(ОформленоПоОрдерам, ТаблицаВыбранныхСтрок, "КоличествоВОрдере", Ключ, Условие, Истина);
// Если в ордере не указаны назначения или указаны неверно
Ключ = "Номенклатура, Характеристика, Серия";
НакладныеСервер.РаспределитьКоличество(ОформленоПоОрдерам, ТаблицаВыбранныхСтрок, "КоличествоВОрдере", Ключ, Условие, Истина);
ТаблицаВыбранныхСтрок.Колонки.Удалить("Назначение");
КолонкаНазначениеДокумента = ТаблицаВыбранныхСтрок.Колонки.НазначениеДокумента; // КолонкаТаблицыЗначений
КолонкаНазначениеДокумента.Имя = "Назначение";
// 5. Свертка таблицы до отменяемых строк
КоличествоИндексов = ТаблицаВыбранныхСтрок.Количество() - 1;
Для Индекс = 0 По КоличествоИндексов Цикл
Строка = ТаблицаВыбранныхСтрок[КоличествоИндексов - Индекс];
Если Строка.Количество < Строка.КоличествоВНакладной Тогда
Оформлено = Строка.КоличествоВНакладной;
Иначе
Оформлено = Макс(Строка.КоличествоВНакладной, Строка.КоличествоВОрдере);
КонецЕсли;
Строка.Количество = Строка.Количество - Оформлено;
Если Строка.Количество = 0 Тогда
ТаблицаВыбранныхСтрок.Удалить(Строка);
КонецЕсли;
КонецЦикла;
КоличествоСтрокКОтмене = ПеренестиВТаблицуДокументаОтмененныеСтроки(ДокументОбъект,
ДокументОбъект[ПараметрыЗаполнения.ИмяТабличнойЧасти], ТаблицаВыбранныхСтрок,
ПараметрыОтмены, Перечисления.ТипыДвиженияЗапасов.Отгрузка);
Возврат ЗаказыСервер.РезультатОтменыНеотработанныхСтрок(КоличествоСтрокКОтмене);
КонецФункции
3. ЗакупкиСервер. ПолучитьУсловияЗакупокПоУмолчанию()
- Выполняет несколько задач:
- Формирует и заполняет структуру параметров отбора.
- Выполняет сложный запрос к базе данных с динамической подстановкой условий и обрабатывает его результат.
- Обрабатывает выборку запроса и заполняет структуру условий закупок.
- Имеет несколько причин для изменения:
- Изменение параметров отбора.
- Изменение логики запроса.
- Изменение логики обработки результата.
- Не изолирован:
- Зависит от внешних данных: динамическое формирование текста запроса.
- Смешивает разные обязанности: формирование параметров (подготовка данных), выполнение запроса (работа с базой данных) и обработка результата (бизнес-логика) — это разные уровни ответственности.
- Выполняет побочные действия: изменяет текст запроса и корректирует параметры на основе внешних условий, что выходит за рамки основной задачи (получение условий закупок).
// Возвращает структуру условий закупок по партнеру.
//
// Параметры:
// Партнер - СправочникСсылка.Партнеры - партнер, для которого необходимо получить условия закупок,
// ПараметрыОтбора - Структура - содержит параметры отбора соглашения.
//
// Возвращаемое значение:
// Структура - структура, включающая условия закупок.
//
Функция ПолучитьУсловияЗакупокПоУмолчанию(Знач Партнер, Знач ПараметрыОтбора = Неопределено) Экспорт
ВсеПараметрыОтбора = Новый Структура();
ВсеПараметрыОтбора.Вставить("ТолькоДействующее", Истина);
ВсеПараметрыОтбора.Вставить("УчитыватьГруппыСкладов", Ложь);
ВсеПараметрыОтбора.Вставить("ИсключитьГруппыСкладовДоступныеВЗаказах", Ложь);
ВсеПараметрыОтбора.Вставить("ХозяйственныеОперации", Неопределено);
ВсеПараметрыОтбора.Вставить("ВыбранноеСоглашение", Справочники.СоглашенияСПоставщиками.ПустаяСсылка());
ВсеПараметрыОтбора.Вставить("ИспользуютсяДоговорыКонтрагентов", Неопределено);
Если ПараметрыОтбора <> Неопределено Тогда
ЗаполнитьЗначенияСвойств(ВсеПараметрыОтбора, ПараметрыОтбора);
КонецЕсли;
Запрос = Новый Запрос();
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 2
| СоглашениеСПоставщиком.Ссылка КАК Соглашение,
| СоглашениеСПоставщиком.Партнер КАК Партнер,
| СоглашениеСПоставщиком.Контрагент КАК Контрагент,
| СоглашениеСПоставщиком.Организация КАК Организация,
| СоглашениеСПоставщиком.ВидЦеныПоставщика КАК ВидЦеныПоставщика,
| СоглашениеСПоставщиком.Валюта КАК Валюта,
| ВЫБОР
| КОГДА СоглашениеСПоставщиком.ИспользуютсяДоговорыКонтрагентов
| ТОГДА СоглашениеСПоставщиком.Валюта
| ИНАЧЕ СоглашениеСПоставщиком.ВалютаВзаиморасчетов
| КОНЕЦ КАК ВалютаВзаиморасчетов,
| СоглашениеСПоставщиком.ЦенаВключаетНДС КАК ЦенаВключаетНДС,
| СоглашениеСПоставщиком.ХозяйственнаяОперация КАК ХозяйственнаяОперация,
| СоглашениеСПоставщиком.РегистрироватьЦеныПоставщика КАК РегистрироватьЦеныПоставщика,
| ВЫБОР
| КОГДА
| НЕ СоглашениеСПоставщиком.Склад.ЭтоГруппа
| ТОГДА
| СоглашениеСПоставщиком.Склад
| КОГДА
| СоглашениеСПоставщиком.Склад.ЭтоГруппа
| И &УчитыватьГруппыСкладов
| И СоглашениеСПоставщиком.Склад.ВыборГруппы В (&ВыборГруппыСкладов)
| ТОГДА
| СоглашениеСПоставщиком.Склад
| ИНАЧЕ
| ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)
| КОНЕЦ КАК Склад,
| СоглашениеСПоставщиком.ФормаОплаты КАК ФормаОплаты,
| СоглашениеСПоставщиком.ОплатаВВалюте КАК ОплатаВВалюте,
| СоглашениеСПоставщиком.СрокПоставки КАК СрокПоставки,
| СоглашениеСПоставщиком.ГруппаФинансовогоУчета КАК ГруппаФинансовогоУчета,
| СоглашениеСПоставщиком.СтатьяДвиженияДенежныхСредств КАК СтатьяДвиженияДенежныхСредств,
| СоглашениеСПоставщиком.СпособРасчетаВознаграждения КАК СпособРасчетаВознаграждения,
| СоглашениеСПоставщиком.ПроцентВознаграждения КАК ПроцентВознаграждения,
| СоглашениеСПоставщиком.УдержатьВознаграждение КАК УдержатьВознаграждение,
|
| СоглашениеСПоставщиком.ИспользуютсяДоговорыКонтрагентов КАК ИспользуютсяДоговорыКонтрагентов,
| СоглашениеСПоставщиком.ПорядокРасчетов КАК ПорядокРасчетов,
| СоглашениеСПоставщиком.ВозвращатьМногооборотнуюТару КАК ВозвращатьМногооборотнуюТару,
| СоглашениеСПоставщиком.СрокВозвратаМногооборотнойТары КАК СрокВозвратаМногооборотнойТары,
| СоглашениеСПоставщиком.РассчитыватьДатуВозвратаТарыПоКалендарю КАК РассчитыватьДатуВозвратаТарыПоКалендарю,
| СоглашениеСПоставщиком.КалендарьВозвратаТары КАК КалендарьВозвратаТары,
| СоглашениеСПоставщиком.ТребуетсяЗалогЗаТару КАК ТребуетсяЗалогЗаТару,
| СоглашениеСПоставщиком.НаправлениеДеятельности КАК НаправлениеДеятельности,
| СоглашениеСПоставщиком.МинимальнаяСуммаЗаказа КАК МинимальнаяСуммаЗаказа
|ИЗ
| Справочник.СоглашенияСПоставщиками КАК СоглашениеСПоставщиком
|ГДЕ
| НЕ СоглашениеСПоставщиком.ПометкаУдаления
| И &ИспользоватьСоглашенияСПоставщиками
| И ((НЕ &ОтборХозяйственныеОперации
| И СоглашениеСПоставщиком.ХозяйственнаяОперация <> ЗНАЧЕНИЕ(Перечисление.ХозяйственныеОперации.ОказаниеАгентскихУслуг)
| И СоглашениеСПоставщиком.ХозяйственнаяОперация <> ЗНАЧЕНИЕ(Перечисление.ХозяйственныеОперации.ПриемНаХранениеСПравомПродажи))
| ИЛИ СоглашениеСПоставщиком.ХозяйственнаяОперация В (&ХозяйственныеОперации)) И
| &УсловиеИспользуютсяДоговорыКонтрагентов И
| &УсловиеСоглашениеСПоставщикомСтатус И
| СоглашениеСПоставщиком.Партнер = &Партнер;
|
|////////////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ РАЗРЕШЕННЫЕ
| СоглашениеСПоставщиком.Ссылка КАК Соглашение,
| СоглашениеСПоставщиком.Партнер КАК Партнер,
| СоглашениеСПоставщиком.Контрагент КАК Контрагент,
| СоглашениеСПоставщиком.Организация КАК Организация,
| СоглашениеСПоставщиком.Валюта КАК Валюта,
| ВЫБОР
| КОГДА СоглашениеСПоставщиком.ИспользуютсяДоговорыКонтрагентов
| ТОГДА СоглашениеСПоставщиком.Валюта
| ИНАЧЕ СоглашениеСПоставщиком.ВалютаВзаиморасчетов
| КОНЕЦ КАК ВалютаВзаиморасчетов,
| СоглашениеСПоставщиком.ЦенаВключаетНДС КАК ЦенаВключаетНДС,
| СоглашениеСПоставщиком.ХозяйственнаяОперация КАК ХозяйственнаяОперация,
| СоглашениеСПоставщиком.ВидЦеныПоставщика КАК ВидЦеныПоставщика,
| СоглашениеСПоставщиком.РегистрироватьЦеныПоставщика КАК РегистрироватьЦеныПоставщика,
| ВЫБОР
| КОГДА
| НЕ СоглашениеСПоставщиком.Склад.ЭтоГруппа
| ТОГДА
| СоглашениеСПоставщиком.Склад
| КОГДА
| СоглашениеСПоставщиком.Склад.ЭтоГруппа
| И &УчитыватьГруппыСкладов
| И СоглашениеСПоставщиком.Склад.ВыборГруппы В (&ВыборГруппыСкладов)
| ТОГДА
| СоглашениеСПоставщиком.Склад
| ИНАЧЕ
| ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)
| КОНЕЦ КАК Склад,
| СоглашениеСПоставщиком.ФормаОплаты КАК ФормаОплаты,
| СоглашениеСПоставщиком.ОплатаВВалюте КАК ОплатаВВалюте,
| СоглашениеСПоставщиком.СрокПоставки КАК СрокПоставки,
| СоглашениеСПоставщиком.ГруппаФинансовогоУчета КАК ГруппаФинансовогоУчета,
| СоглашениеСПоставщиком.СтатьяДвиженияДенежныхСредств КАК СтатьяДвиженияДенежныхСредств,
| СоглашениеСПоставщиком.СпособРасчетаВознаграждения КАК СпособРасчетаВознаграждения,
| СоглашениеСПоставщиком.ПроцентВознаграждения КАК ПроцентВознаграждения,
| СоглашениеСПоставщиком.УдержатьВознаграждение КАК УдержатьВознаграждение,
|
| СоглашениеСПоставщиком.ИспользуютсяДоговорыКонтрагентов КАК ИспользуютсяДоговорыКонтрагентов,
| СоглашениеСПоставщиком.ПорядокРасчетов КАК ПорядокРасчетов,
| СоглашениеСПоставщиком.ВозвращатьМногооборотнуюТару КАК ВозвращатьМногооборотнуюТару,
| СоглашениеСПоставщиком.СрокВозвратаМногооборотнойТары КАК СрокВозвратаМногооборотнойТары,
| СоглашениеСПоставщиком.РассчитыватьДатуВозвратаТарыПоКалендарю КАК РассчитыватьДатуВозвратаТарыПоКалендарю,
| СоглашениеСПоставщиком.КалендарьВозвратаТары КАК КалендарьВозвратаТары,
| СоглашениеСПоставщиком.ТребуетсяЗалогЗаТару КАК ТребуетсяЗалогЗаТару,
| СоглашениеСПоставщиком.НаправлениеДеятельности КАК НаправлениеДеятельности,
| СоглашениеСПоставщиком.МинимальнаяСуммаЗаказа КАК МинимальнаяСуммаЗаказа
|ИЗ
| Справочник.СоглашенияСПоставщиками КАК СоглашениеСПоставщиком
|ГДЕ
| НЕ СоглашениеСПоставщиком.ПометкаУдаления
| И &ИспользоватьСоглашенияСПоставщиками
| И ((НЕ &ОтборХозяйственныеОперации И СоглашениеСПоставщиком.ХозяйственнаяОперация <> ЗНАЧЕНИЕ(Перечисление.ХозяйственныеОперации.ОказаниеАгентскихУслуг))
| ИЛИ СоглашениеСПоставщиком.ХозяйственнаяОперация В (&ХозяйственныеОперации))
| И СоглашениеСПоставщиком.Ссылка = &ВыбранноеСоглашение И
| &УсловиеИспользуютсяДоговорыКонтрагентов И
| &УсловиеСоглашениеСПоставщикомСтатус И
| СоглашениеСПоставщиком.Партнер = &Партнер
|";
Если ВсеПараметрыОтбора.ТолькоДействующее Тогда
Запрос.Текст = СтрЗаменить(Запрос.Текст, "&УсловиеСоглашениеСПоставщикомСтатус", "СоглашениеСПоставщиком.Статус = ЗНАЧЕНИЕ(Перечисление.СтатусыСоглашенийСПоставщиками.Действует)");
Иначе
Запрос.Текст = СтрЗаменить(Запрос.Текст, "&УсловиеСоглашениеСПоставщикомСтатус", "ИСТИНА");
КонецЕсли;
Если ВсеПараметрыОтбора.ИспользуютсяДоговорыКонтрагентов = Неопределено Тогда
Запрос.Текст = СтрЗаменить(Запрос.Текст, "&УсловиеИспользуютсяДоговорыКонтрагентов", "ИСТИНА");
Иначе
Запрос.Текст = СтрЗаменить(Запрос.Текст, "&УсловиеИспользуютсяДоговорыКонтрагентов", "СоглашениеСПоставщиком.ИспользуютсяДоговорыКонтрагентов = &ИспользуютсяДоговорыКонтрагентов");
КонецЕсли;
Запрос.УстановитьПараметр("Партнер", Партнер);
Запрос.УстановитьПараметр("ВыборГруппыСкладов", Справочники.Склады.ВариантыВыбораГруппыСкладов(ВсеПараметрыОтбора.ИсключитьГруппыСкладовДоступныеВЗаказах));
Запрос.УстановитьПараметр("УчитыватьГруппыСкладов", ВсеПараметрыОтбора.УчитыватьГруппыСкладов);
Запрос.УстановитьПараметр("ОтборХозяйственныеОперации", ВсеПараметрыОтбора.ХозяйственныеОперации <> Неопределено);
Запрос.УстановитьПараметр("ХозяйственныеОперации", ВсеПараметрыОтбора.ХозяйственныеОперации);
Запрос.УстановитьПараметр("ВыбранноеСоглашение", ВсеПараметрыОтбора.ВыбранноеСоглашение);
Запрос.УстановитьПараметр("ИспользоватьСоглашенияСПоставщиками", ПолучитьФункциональнуюОпцию("ИспользоватьСоглашенияСПоставщиками"));
Запрос.УстановитьПараметр("ИспользуютсяДоговорыКонтрагентов", ВсеПараметрыОтбора.ИспользуютсяДоговорыКонтрагентов);
РезультатЗапроса = Запрос.ВыполнитьПакет();
Если РезультатЗапроса[0].Пустой() Тогда
Возврат Неопределено;
КонецЕсли;
Выборка = РезультатЗапроса[0].Выбрать();
// Если в выборке одно соглашение - возвращаем его
Если Выборка.Количество() = 1 Тогда
Выборка.Следующий();
ИначеЕсли Выборка.Количество() > 1 Тогда
Если НЕ РезультатЗапроса[1].Пустой() Тогда
Выборка = РезультатЗапроса[1].Выбрать();
Выборка.Следующий();
Иначе
Возврат Неопределено;
КонецЕсли;
Иначе
Возврат Неопределено;
КонецЕсли;
СтруктураРеквизитов = ШаблонУсловияЗакупок();
ЗаполнитьЗначенияСвойств(СтруктураРеквизитов, Выборка);
Возврат СтруктураРеквизитов;
КонецФункции
Результат применения
Принцип направлен на создание кода, который легко читать, модифицировать, тестировать и расширять. Каждый модуль или метод должен иметь ровно одну задачу, что снижает связанность, минимизирует побочные эффекты и упрощает повторное использование кода. Это особенно важно в больших системах, таких как типовые конфигурации 1С, где смешивание обязанностей (например, бизнес-логики, работы с интерфейсом и доступа к данным) приводит к запутанности, усложняет отладку и превращает код в "спагетти".
При следовании принципу код становится более прозрачным и управляемым. Изменения в одной части системы не приводят к неожиданным сбоям в других, тестирование упрощается за счёт изоляции функциональности, а разработчики могут быстрее вносить доработки или расширять систему без риска сломать существующую логику.
Представьте себе утро в большом городе. Вы садитесь в машину, чтобы добраться до работы, но вместо привычного ритма главной магистрали вас встречает хаос: дорога перекопана, развязки превратились в лабиринт, машины сигналят, а пробка тянется до горизонта. Что пошло не так? Казалось бы, хотели улучшить движение, добавить новые пути, но вместо этого старые маршруты стали непроходимыми, а новые — бесполезными. Почему простое изменение привело к такому коллапсу? И главное — можно ли было этого избежать?
А теперь представьте другую картину: та же магистраль, но уже с новыми развязками, которые не ломают привычный поток. Машины едут плавно, пробок нет, а дорога остаётся такой же надёжной, как раньше. В чём секрет? Ответ кроется в одном простом принципе, который мы, часто упускаем из виду. Разберем дальше, как "принцип открытости/закрытости" может спасти разработки от "дорожного хаоса" и сделать код гибким, как идеальная магистраль.
Описание
Второй принцип SOLID звучит: программные сущности, будь то модули, классы или функции, должны быть открыты для расширения, но закрыты для модификации. Представьте, что ваш код — это главная магистраль: вы можете строить новые развязки, не перекапывая саму дорогу. Это значит, что новая функциональность добавляется через механизмы вроде наследования, интерфейсов или подключаемых модулей, а старый, проверенный код остаётся нетронутым. В итоге система становится гибкой, как сеть дорог, где новые маршруты не ломают старые пути. Разберем дальше.
Применение в 1С
Переопределяемые модули: дают возможность дополнить типовую логику.
Расширения конфигурации: выносят новую функциональность в отдельный слой — типовой код и объекты конфигурации остаются нетронутыми.
Подписки на события: механизм для подключения дополнительной логики, без изменений в базовом объекте.
Общие модули с настройками в пользовательском режиме: вместо правки кода под новые требования логика выносится в настройки. Скажем, правила расчёта скидок задаются в справочнике или регистре сведений, а общий модуль просто использует эти данные — код остаётся неизменным.
Внешние отчёты и обработки: для нового отчета или обработки создаётся внешний файл и подключается "дополнительно" к конфигурации. Добавляется новая функциональность, типовая конфигурация не меняется.
У нас есть документ "Реализация товаров и услуг", и в нём есть процедура расчёта скидки на основе типа клиента. Со временем появляются новые типы скидок, и нужно добавлять их в логику. Я продемонстрирую, как это выглядит с нарушением принципа и затем с соблюденим.
Нарушение принципа:
В конфигурациях часто встречаются процедуры, которые обрастают условиями при доработках. Вот пример, как это выглядит:
// Модуль объекта документа "РеализацияТоваровУслуг"
Процедура РассчитатьСкидку(Сумма, ТипКлиента) Экспорт
Если ТипКлиента = "Обычный" Тогда
Скидка = Сумма * 0.05; // 5% для обычных клиентов
ИначеЕсли ТипКлиента = "VIP" Тогда
Скидка = Сумма * 0.15; // 15% для VIP
ИначеЕсли ТипКлиента = "Партнёр" Тогда
Скидка = Сумма * 0.10; // 10% для партнёров (новое требование)
ИначеЕсли ТипКлиента = "Оптовик" Тогда
Скидка = Сумма * 0.20; // 20% для оптовиков (ещё одно новое требование)
Иначе
Скидка = 0;
КонецЕсли;
СуммаСоСкидкой = Сумма - Скидка;
КонецПроцедуры
Что плохого в этом примере:
В данном случае, каждый раз, когда появляется новый тип клиента или скидки, разработчик вынужден лезть в эту процедуру и добавлять новое условие ИначеЕсли. Это ломает "закрытость" метода.
Соблюдение принципа:
Теперь перепишем этот код с учетом требований принципа двумя способами, используя подходы характерные для 1С.
Вариант 1
Добавим регистр сведений "НастройкиСкидок" (Измерение: ТипКлиента, Ресурс: ПроцентСкидки) для хранения правил и общий модуль "РасчетСкидок" для расчёта:
// Общий модуль "РасчетСкидок"
Функция ПолучитьСкидкуПоТипуКлиента(ТипКлиента) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| НастройкиСкидок.ПроцентСкидки
|ИЗ
| РегистрСведений.НастройкиСкидок КАК НастройкиСкидок
|ГДЕ
| НастройкиСкидок.ТипКлиента = &ТипКлиента";
Запрос.УстановитьПараметр("ТипКлиента", ТипКлиента);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.ПроцентСкидки / 100;
Иначе
Возврат 0;
КонецЕсли;
КонецФункции
Модуль объекта документа "Реализация товаров и услуг":
// Модуль объекта документа "РеализацияТоваровУслуг"
Процедура РассчитатьСкидку(Сумма, ТипКлиента) Экспорт
ПроцентСкидки = РасчетСкидок.ПолучитьСкидкуПоТипуКлиента(ТипКлиента);
Скидка = Сумма * ПроцентСкидки;
СуммаСоСкидкой = Сумма - Скидка;
КонецПроцедуры
Что решилось после исправления:
Теперь, что добавить новую скидку, допустим, появился тип клиента "Сотрудник" с 25% скидкой, добавляем запись в регистр сведений "НастройкиСкидок" в пользовательском режиме: ТипКлиента = "Сотрудник", ПроцентСкидки = 25. Код процедуры РассчитатьСкидку остаётся нетронутым!
Закрытость для модификации: процедура РассчитатьСкидку больше не меняется при добавлении новых типов скидок. Логика изолирована в общем модуле и данных.
Открытость для расширения: новые скидки добавляются через настройки в регистре сведений — это может сделать даже пользователь без правки кода.
Применение на практике: использование регистров сведений для хранения настроек — типичная практика в типовых конфигурациях.
Вариант 2
Теперь используем переопределяемый модуль для вынесения логики расчёта скидок. В типовых конфигурациях часто есть общие модули с пометкой "Переопределяемый", которые предназначены для таких задач. Мы создадим базовую функцию в обычном модуле и дадим возможность переопределить её поведение.
Базовый метод:
// Общий модуль "РасчетСкидок"
Функция РассчитатьСкидкуПоТипуКлиента(Сумма, ТипКлиента) Экспорт
ПроцентСкидки = РасчетСкидокПереопределяемый.ПолучитьПроцентСкидки(ТипКлиента);
Скидка = Сумма * ПроцентСкидки;
Возврат Скидка;
КонецФункции
Переопределяемый метод:
// Общий модуль "РасчетСкидокПереопределяемый"
Функция ПолучитьПроцентСкидки(ТипКлиента) Экспорт
// Здесь можно задать базовые значения или оставить пустым для переопределения
Если ТипКлиента = "Обычный" Тогда
Возврат 0.05; // 5%
ИначеЕсли ТипКлиента = "VIP" Тогда
Возврат 0.15; // 15%
Иначе
Возврат 0; // По умолчанию без скидки
КонецЕсли;
КонецФункции
Вызывающий код:
// Модуль объекта документа "Реализация товаров и услуг"
Процедура РассчитатьСкидку(Сумма, ТипКлиента) Экспорт
Скидка = РасчетСкидок.РассчитатьСкидкуПоТипуКлиента(Сумма, ТипКлиента);
СуммаСоСкидкой = Сумма - Скидка;
КонецПроцедуры
Что решилось после исправления:
В данном случае для добавления новой скидки, мы не трогаем основной метод "РассчитатьСкидкуПоТипуКлиента" в модуле "РасчетСкидок", а вместо этого модифицируем процедуру в переопределяемом методе.
Закрытость для модификации: исходная процедура "РассчитатьСкидку" в модуле объекта и базовый модуль "РасчетСкидок" не меняются.
Открытость для расширения: новая логика добавляется через переопределяемый модуль.
Применение на практике: переопределяемые модули — стандартный механизм в типовых конфигурациях.
1. ОбщегоНазначения.ЗначениеРеквизитаОбъекта()
Открыт для расширения: расширяется через добавление новых объектов и реквизитов в метаданные.
Закрыт от модификации: код не требует изменений при добавлении новых объектов или реквизитов.
// Возвращает значения реквизита, прочитанного из информационной базы по ссылке на объект.
//
// Параметры:
// Ссылка - ЛюбаяСсылка - объект, значения реквизитов которого необходимо получить.
// - Строка - полное имя предопределенного элемента, значения реквизитов которого необходимо получить.
// ИмяРеквизита - Строка - имя получаемого реквизита.
// Допускается указание имени реквизита через точку, но при этом параметр КодЯзыка для
// такого реквизита учитываться не будет.
// ВыбратьРазрешенные - Булево - если Истина, то запрос к объекту выполняется с учетом прав пользователя;
// если есть ограничение на уровне записей, то возвращается Неопределено;
// если нет прав для работы с таблицей, то возникнет исключение;
// если Ложь, то возникнет исключение при отсутствии прав на таблицу
// или любой из реквизитов.
// КодЯзыка - Строка - код языка для мультиязычного реквизита. Значение по умолчанию - основной язык конфигурации.
//
// Возвращаемое значение:
// Произвольный - если в параметр Ссылка передана пустая ссылка, то возвращается Неопределено.
// Если в параметр Ссылка передана ссылка несуществующего объекта (битая ссылка),
// то возвращается Неопределено.
//
Функция ЗначениеРеквизитаОбъекта(Ссылка, ИмяРеквизита, ВыбратьРазрешенные = Ложь, Знач КодЯзыка = Неопределено) Экспорт
Если ПустаяСтрока(ИмяРеквизита) Тогда
ВызватьИсключение(СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(
НСтр("ru = 'Неверный второй параметр %1 в функции %2:
|Имя реквизита должно быть заполнено.'"),
"ИмяРеквизита", "ОбщегоНазначения.ЗначениеРеквизитаОбъекта"),
КатегорияОшибки.ОшибкаКонфигурации);
КонецЕсли;
Результат = ЗначенияРеквизитовОбъекта(Ссылка, ИмяРеквизита, ВыбратьРазрешенные, КодЯзыка);
Возврат Результат[СтрЗаменить(ИмяРеквизита, ".", "")];
КонецФункции
2. УчетнаяПолитика. ПлательщикЕНВД()
Открыт для расширения: логика метода расширяется через модуль УчетнаяПолитикаПереопределяемый.
Закрыт от модификации: исходный код БСП не изменяется.
// Параметры учетной политики по ЕНВД
Функция ПлательщикЕНВД(Организация, Период) Экспорт
Возврат УчетнаяПолитикаПереопределяемый.ПлательщикЕНВД(Организация, Период);
КонецФункции
УчетнаяПолитикаПереопределяемый. ПлательщикЕНВД()
// Возвращает признак, применяет ли организация единый налог на вмененный доход (ЕНВД).
//
// Параметры:
// Организация - СправочникСсылка.Организации - организация, для которой определяется применяет ли она ЕНВД;
// Период - Дата - дата, на которую определяется применение ЕНВД.
//
// Возвращаемое значение:
// Булево - Истина, если организация на дату является плательщиком единого налога на вмененный доход. В противном случае - ложь.
//
Функция ПлательщикЕНВД(Организация, Период) Экспорт
ПараметрыУчетнойПолитики = НастройкиНалоговУчетныхПолитик.ДействующиеПараметрыНалоговУчетныхПолитикНаДату(
"НастройкиСистемыНалогообложения",
Организация,
Период,
Ложь);
Если НЕ ПараметрыУчетнойПолитики = Неопределено Тогда
Возврат ПараметрыУчетнойПолитики.ПрименяетсяЕНВД;
Иначе
Возврат Ложь;
КонецЕсли;
КонецФункции
3. КонтрольВеденияУчета.ВыполнитьПроверку()
Открыт для расширения: Вместо создания новых процедур для каждой проверки разработчик передает новое правило (Проверка) и параметры (ПараметрыВыполненияПроверки). Логика расширяется через добавление новых правил в справочник ПравилаПроверкиУчета.
Закрыт от модификации: код процедуры остаётся неизменным.
// Выполняет указанную проверку ведения учета с заданными параметрами.
//
// Параметры:
// Проверка - СправочникСсылка.ПравилаПроверкиУчета
// - Строка - правило проверки,
// которая будет выполняться либо строковый идентификатор указанного правила.
// ПараметрыВыполненияПроверки - Структура
// - Массив - произвольные дополнительные параметры проверки,
// которые уточняют, что и как именно проверять.
// См. КонтрольВеденияУчета.ПараметрыВыполненияПроверки.
//
Процедура ВыполнитьПроверку(Знач Проверка, Знач ПараметрыВыполненияПроверки = Неопределено, ПроверяемыеОбъекты = Неопределено) Экспорт
Если ТипЗнч(Проверка) = Тип("Строка") Тогда
ВыполняемаяПроверка = ПроверкаПоИдентификатору(Проверка);
Если ВыполняемаяПроверка.Пустая() Тогда
ВызватьИсключение СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(
НСтр("ru = 'Проверка ведения учета с идентификатором ""%1"" не существует (см. %2)'"),
Проверка,
"КонтрольВеденияУчетаПереопределяемый.ПриОпределенииПроверок");
КонецЕсли;
Иначе
ОбщегоНазначенияКлиентСервер.ПроверитьПараметр("КонтрольВеденияУчета.ВыполнитьПроверку", "Проверка",
Проверка, Тип("СправочникСсылка.ПравилаПроверкиУчета"));
ВыполняемаяПроверка = Проверка;
КонецЕсли;
Если ПараметрыВыполненияПроверки <> Неопределено Тогда
ПроверитьПараметрыВыполненияПроверки(ПараметрыВыполненияПроверки, "КонтрольВеденияУчета.ВыполнитьПроверку");
КонецЕсли;
КонтрольВеденияУчетаСлужебный.ВыполнитьПроверку(ВыполняемаяПроверка, ПараметрыВыполненияПроверки, ПроверяемыеОбъекты);
КонецПроцедуры
1. ДенежныеСредстваСервер.НастройкиЭлементовБанков()
Закрытость для модификации: нет, т.к. процедура требует правок при любом изменении или добавлении настроек.
Открытость для расширения: нет, т.к. нет механизма для добавления новых настроек без изменения кода, через настройки или переопределение.
// Дополняет настройки данные настроек для банковских счетов
//
// Параметры:
// Настройки - ТаблицаЗначений - таблица с колонками:
// * Поля - Массив из Строка - поля, для которых определяются настройки отображения
// * Условие - ОтборКомпоновкиДанных - условия применения настройки
// * Свойства - Структура - имена и значения свойств
//
Процедура НастройкиЭлементовБанков(Настройки) Экспорт
Финансы = ФинансоваяОтчетностьСервер;
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("ОтступРеквизитыБанка");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.ИспользуетсяБанкДляРасчетов", Истина);
Элемент.Свойства.Вставить("Ширина", 12);
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("ОтступРеквизитыБанка");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.ИспользуетсяБанкДляРасчетов", Ложь);
Элемент.Свойства.Вставить("Ширина", 9);
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("БанкДляРасчетовВГруппе");
Финансы.НовыйОтбор(Элемент.Условие, "РучноеИзменениеРеквизитовБанкаДляРасчетов", Ложь);
Элемент.Свойства.Вставить("Доступность");
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("СчетВБанкеДляРасчетов");
Элемент.Поля.Добавить("БИКБанкаДляРасчетов");
Элемент.Поля.Добавить("НаименованиеБанкаДляРасчетовМеждународное");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.ИспользуетсяБанкДляРасчетов", Истина);
Элемент.Свойства.Вставить("Доступность");
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("ГруппаВыборБанкаДляРасчетов");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.ИспользуетсяБанкДляРасчетов", Истина);
Элемент.Свойства.Вставить("Видимость");
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("БИКБанка");
Элемент.Поля.Добавить("БИКБанкаПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "ИностранныйБанк", Ложь);
Элемент.Свойства.Вставить("Заголовок", НСтр("ru = 'БИК'"));
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("БИКБанка");
Элемент.Поля.Добавить("БИКБанкаПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "ИностранныйБанк", Истина);
Элемент.Свойства.Вставить("Заголовок", НСтр("ru = 'Код'"));
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("БИКБанкаДляРасчетов");
Элемент.Поля.Добавить("БИКБанкаДляРасчетовПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "ИностранныйБанк", Ложь);
Элемент.Свойства.Вставить("Заголовок", НСтр("ru = 'БИК'"));
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("БИКБанкаДляРасчетов");
Элемент.Поля.Добавить("БИКБанкаДляРасчетовПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "ИностранныйБанк", Истина);
Элемент.Свойства.Вставить("Заголовок", НСтр("ru = 'Код'"));
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("КоррСчетБанка");
Элемент.Поля.Добавить("КоррСчетБанкаДляРасчетов");
Финансы.НовыйОтбор(Элемент.Условие, "ИностранныйБанк", Ложь);
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.ВалютныйСчет", Ложь);
Элемент.Свойства.Вставить("Видимость");
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("СчетВБанкеДляРасчетов");
Финансы.НовыйОтбор(Элемент.Условие, "ИностранныйБанк", Ложь);
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.ВалютныйСчет", Ложь);
Элемент.Свойства.Вставить("Видимость", Ложь);
ОсновнаяСтрана = Справочники.СтраныМира.НайтиПоКоду("643");
Если ЗначениеЗаполнено(ОсновнаяСтрана) Тогда
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("ЭтоIBAN");
Финансы.НовыйОтбор(Элемент.Условие, "СтранаБанка", ОсновнаяСтрана);
Элемент.Свойства.Вставить("Доступность", Ложь);
КонецЕсли;
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("СтранаБанкаМеждународный");
Элемент.Поля.Добавить("СтранаБанкаМеждународныйПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.НациональныеРеквизитыБанковскихСчетов", Истина);
Элемент.Свойства.Вставить("Видимость", Ложь);
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("ЭтоIBAN");
Элемент.Поля.Добавить("ГруппаБанкМеждународный");
Элемент.Поля.Добавить("ГруппаБанкМеждународныйПоСсылке");
Элемент.Поля.Добавить("ГруппаБанкДляРасчетовМеждународный");
Элемент.Поля.Добавить("ГруппаБанкДляРасчетовМеждународныйПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.МеждународныеРеквизитыБанковскихСчетов", Истина);
Элемент.Свойства.Вставить("Видимость");
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("ГруппаБанкМеждународный");
Элемент.Поля.Добавить("ГруппаБанкМеждународныйПоСсылке");
Элемент.Поля.Добавить("ГруппаБанкДляРасчетовМеждународный");
Элемент.Поля.Добавить("ГруппаБанкДляРасчетовМеждународныйПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.НациональныеРеквизитыБанковскихСчетов", Истина);
Элемент.Свойства.Вставить("ОтображатьЗаголовок");
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("ГруппаБанк");
Элемент.Поля.Добавить("ГруппаБанкПоСсылке");
Элемент.Поля.Добавить("ГруппаБанкДляРасчетов");
Элемент.Поля.Добавить("ГруппаБанкДляРасчетовПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.МеждународныеРеквизитыБанковскихСчетов", Ложь);
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.НациональныеРеквизитыБанковскихСчетов", Истина);
Элемент.Свойства.Вставить("ОтображатьЗаголовок", Ложь);
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("ГруппаБанк");
Элемент.Поля.Добавить("ГруппаБанкПоСсылке");
Элемент.Поля.Добавить("ГруппаБанкДляРасчетов");
Элемент.Поля.Добавить("ГруппаБанкДляРасчетовПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.НациональныеРеквизитыБанковскихСчетов", Истина);
Элемент.Свойства.Вставить("Видимость");
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("КоррСчетБанкаДляРасчетовМеждународный");
Элемент.Поля.Добавить("КоррСчетБанкаДляРасчетовМеждународныйПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.НациональныеРеквизитыБанковскихСчетов", Истина);
Элемент.Свойства.Вставить("Видимость", Ложь);
Элемент = Настройки.Добавить();
Элемент.Поля.Добавить("СтранаБанкаДляРасчетовМеждународный");
Элемент.Поля.Добавить("СтранаБанкаДляРасчетовМеждународныйПоСсылке");
Финансы.НовыйОтбор(Элемент.Условие, "Дополнительно.НациональныеРеквизитыБанковскихСчетов", Истина);
Элемент.Свойства.Вставить("Видимость", Ложь);
КонецПроцедуры
2. Ценообразование.ПараметрыДляПроведенияДокумента()
Закрытость для модификации: нет, т.к. процедура требует правок для добавления нового регистра учета цен.
Открытость для расширения: нет, т.к. нет механизма для добавления новых настроек без изменения кода, через настройки или переопределение.
// Формирует параметры для проведения документа по регистрам учетного механизма через общий механизм проведения.
//
// Параметры:
// Документ - ДокументОбъект - записываемый документ
// Свойства - См. ПроведениеДокументов.СвойстваДокумента
//
// Возвращаемое значение:
// Структура - См. ПроведениеДокументов.ПараметрыУчетногоМеханизма
//
Функция ПараметрыДляПроведенияДокумента(Документ, Свойства) Экспорт
Параметры = ПроведениеДокументов.ПараметрыУчетногоМеханизма();
// Проведение
Если Свойства.РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
Параметры.ПодчиненныеРегистры.Добавить(Метаданные.РегистрыСведений.ЦеныНоменклатуры);
Параметры.ПодчиненныеРегистры.Добавить(Метаданные.РегистрыСведений.ЦеныНоменклатуры25);
Параметры.ПодчиненныеРегистры.Добавить(Метаданные.РегистрыСведений.ЦеныНоменклатурыПоставщиков);
Параметры.ПодчиненныеРегистры.Добавить(Метаданные.РегистрыНакопления.БонусныеБаллы);
Параметры.ПодчиненныеРегистры.Добавить(Метаданные.РегистрыСведений.УсловияЗакупок);
КонецЕсли;
Возврат Параметры;
КонецФункции
3. СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку()
Закрытость для модификации: нет, т.к. процедура жестко ограничена фиксированным числом аргументов (9) и требует изменения для добавления большего количества.
Открытость для расширения: нет, т.к. нет возможности использования большего числа аргументов без изменения кода.
// Подставляет параметры в строку. Максимально возможное число параметров - 9.
// Параметры в строке задаются как %<номер параметра>. Нумерация параметров начинается с единицы.
//
// Параметры:
// ШаблонСтроки - Строка - шаблон строки с параметрами (вхождениями вида "%<номер параметра>",
// например "%1 пошел в %2");
// Параметр1 - Строка - значение подставляемого параметра.
// Параметр2 - Строка
// Параметр3 - Строка
// Параметр4 - Строка
// Параметр5 - Строка
// Параметр6 - Строка
// Параметр7 - Строка
// Параметр8 - Строка
// Параметр9 - Строка
//
// Возвращаемое значение:
// Строка - текстовая строка с подставленными параметрами.
//
// Пример:
// СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(НСтр("ru='%1 пошел в %2'"), "Вася", "Зоопарк") = "Вася пошел
// в Зоопарк".
//
Функция ПодставитьПараметрыВСтроку(Знач ШаблонСтроки,
Знач Параметр1, Знач Параметр2 = Неопределено, Знач Параметр3 = Неопределено,
Знач Параметр4 = Неопределено, Знач Параметр5 = Неопределено, Знач Параметр6 = Неопределено,
Знач Параметр7 = Неопределено, Знач Параметр8 = Неопределено, Знач Параметр9 = Неопределено) Экспорт
ЕстьПараметрыСПроцентом = СтрНайти(Параметр1, "%")
Или СтрНайти(Параметр2, "%")
Или СтрНайти(Параметр3, "%")
Или СтрНайти(Параметр4, "%")
Или СтрНайти(Параметр5, "%")
Или СтрНайти(Параметр6, "%")
Или СтрНайти(Параметр7, "%")
Или СтрНайти(Параметр8, "%")
Или СтрНайти(Параметр9, "%");
Если ЕстьПараметрыСПроцентом Тогда
Возврат ПодставитьПараметрыСПроцентом(ШаблонСтроки, Параметр1,
Параметр2, Параметр3, Параметр4, Параметр5, Параметр6, Параметр7, Параметр8, Параметр9);
КонецЕсли;
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%1", Параметр1);
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%2", Параметр2);
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%3", Параметр3);
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%4", Параметр4);
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%5", Параметр5);
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%6", Параметр6);
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%7", Параметр7);
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%8", Параметр8);
ШаблонСтроки = СтрЗаменить(ШаблонСтроки, "%9", Параметр9);
Возврат ШаблонСтроки;
КонецФункции
Результат применения
Принцип направлен на создание кода, который легко расширять новыми функциями без необходимости переписывать уже существующие части. Модули или методы должны быть спроектированы так, чтобы добавление новой логики происходило через добавление нового, а не через изменение старого кода. Это снижает риск ошибок, упрощает поддержку и делает систему более гибкой, что особенно ценно конфигурациях 1С.
При следовании принципу код становится более устойчивым и масштабируемым. Например, новые требования (добавление скидок, проверок или отчётов) реализуются через расширения, переопределяемые модули или настройки, не затрагивая базовую функциональность. Это минимизирует конфликты при обновлении конфигурации, ускоряет внедрение изменений и позволяет разработчикам сосредоточиться на новых задачах, а не на исправлении побочных эффектов. Тестирование тоже упрощается, так как старый код остаётся неизменным и проверенным, а новые доработки изолированы.
Взгляните на эти электронные смарт-часы, они — наследники обычных часов, и вы ждёте, что они покажут время, как и их предки, но вместо этого на экране — таблица химических элементов. Неожиданно? Так же и в программном коде: вы вызываете метод, рассчитывая на привычное поведение, а он выдаёт неожиданный результат, который рушит всю логику. Откуда берётся эта ловушка? Всё дело в принципе "подстановки Барбары Лисков". Давайте разберёмся, как вернуть часам их суть, а коду — надёжность.
Описание
Третий принцип SOLID утверждает: объекты или модули, которые подменяют друг друга, должны вести себя так, чтобы система продолжала работать без сбоев. Смарт-часы сложнее обычных часов, но если вместо времени вы видите таблицу химических элементов, что-то явно пошло не так. Это значит, что любая подмена — через наследование, расширение или переопределение — обязана сохранять ожидаемое поведение базовой версии. Если модуль подменяет типовую логику, он не должен ломать её суть. В итоге система остаётся надёжной и предсказуемой, как часы, которые всегда показывают время. Разберём дальше.
Применение в 1С
На уровне архитектуры платформа 1С предоставляет инструменты для реализации требований принципа:
Расширения конфигурации: позволяют переопределять поведение типовых объектов, сохраняя их интерфейс и контракт. Например, если типовой модуль документа возвращает определённый результат, расширение должно соблюдать тот же формат вывода, чтобы не сломать вызывающий код.
Подписки на события: используются для подмены или дополнения логики без изменения исходного объекта. Подписка должна "уважать" ожидания базового события, чтобы не нарушить целостность системы.
Переопределяемые модули: общие модули с суффиксом "Переопределяемый" предназначены для подмены типовой логики. Их реализация обязана сохранять контракт базового модуля, чтобы вызывающий код работал без ошибок.
Данные принцип тесно связан с архитектурой платформы, которая позволяет работать с общими типами (например, "ДокументСсылка"), а наследование реализуется через переопределение модулей или расширений.
Функция проверяет, участвует ли объект (справочник или документ) в активных бизнес-процессах, возвращая Булево. Она принимает ДокументСсылка, СправочникСсылка и должна работать одинаково для всех подтипов.
Нарушение принципа:
Посмотрим на вариант с нарушением принципа.
// Проверяет, участвует ли объект в активных бизнес-процессах.
//
// Параметры:
// СсылкаНаОбъект - ДокументСсылка, СправочникСсылка - ссылка на объект (справочник или документ).
//
// Возвращаемое значение:
// Булево - Истина, если объект участвует в активных бизнес-процессах.
//
Функция УчаствуетВАктивныхБизнесПроцессах(СсылкаНаОбъект) Экспорт
Запрос = Новый Запрос;
Если ТипЗнч(СсылкаНаОбъект) = Тип("СправочникСсылка.Пользователи") Тогда // Особая проверка для "Пользователей"
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| ЗадачаИсполнителя.Ссылка
|ИЗ
| Задача.ЗадачаИсполнителя КАК ЗадачаИсполнителя
|ГДЕ
| ЗадачаИсполнителя.Исполнитель = &Ссылка
| И НЕ ЗадачаИсполнителя.Выполнена";
ИначеЕсли ТипЗнч(СсылкаНаОбъект) = Тип("ДокументСсылка.ЗаказКлиента") Тогда // Особая проверка для "Заказов клиента"
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| ЗаказКлиента.Ссылка
|ИЗ
| Документ.ЗаказКлиента КАК ЗаказКлиента
|ГДЕ
| ЗаказКлиента.Ссылка = &Ссылка
| И ЗаказКлиента.Статус = ЗНАЧЕНИЕ(Перечисление.СтатусыЗаказовКлиентов.НеСогласован)";
Иначе // Общая логика для остальных типов
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| БизнесПроцесс.Ссылка
|ИЗ
| БизнесПроцесс.БизнесПроцесс КАК БизнесПроцесс
|ГДЕ
| БизнесПроцесс.Предмет = &Ссылка
| И НЕ БизнесПроцесс.Завершен";
КонецЕсли;
Запрос.УстановитьПараметр("Ссылка", СсылкаНаОбъект);
Результат = Запрос.Выполнить();
Возврат НЕ Результат.Пустой();
КонецФункции
Что плохого в этом примере:
Контракт:
Вход: ДокументСсылка, СправочникСсылка.
Выход: Булево — участвует ли объект в активных бизнес-процессах.
Ожидание: единообразная проверка участия для всех подтипов ДокументСсылка, СправочникСсылка.
Проблема:
Для СправочникСсылка.Пользователи: возвращает Истина, только если есть невыполненные задачи, игнорируя другие бизнес-процессы.
Для ДокументСсылка.ЗаказКлиента: возвращает Истина, только если заказ несогласован, даже если он участвует в активном процессе в другом статусе.
Для остальных: проверяет наличие активных бизнес-процессов через поле Предмет.
Нарушение:
Поведение зависит от подтипа: "Пользователи" и "Заказы" имеют узкие условия, отличные от общей логики.
Подстановка подтипа меняет смысл результата: например, "Заказ клиента" в статусе "В работе" с активным процессом вернёт Ложь, хотя должен Истина.
Функция теряет универсальность, а контракт становится непредсказуемым для вызывающего кода.
Если ЗаказКлиентаСсылка в статусе "В работе" и участвует в процессе, функция вернёт Ложь, хотя ожидается Истина. Вызывающий код:
Если УчаствуетВАктивныхБизнесПроцессах(ЗаказКлиентаСсылка) Тогда
Сообщить("Объект занят в процессе");
КонецЕсли;
Соблюдение принципа:
Переделаем метод так, чтобы принцип был соблюден:
// Проверяет, участвует ли объект в активных бизнес-процессах.
//
// Параметры:
// СсылкаНаОбъект - ДокументСсылка, СправочникСсылка - ссылка на объект(справочник или документ).
//
// Возвращаемое значение:
// Булево - Истина, если объект участвует в активных бизнес-процессах.
//
Функция УчаствуетВАктивныхБизнесПроцессах(СсылкаНаОбъект) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| БизнесПроцесс.Ссылка
|ИЗ
| БизнесПроцесс.БизнесПроцесс КАК БизнесПроцесс
|ГДЕ
| БизнесПроцесс.Предмет = &Ссылка
| И НЕ БизнесПроцесс.Завершен";
Запрос.УстановитьПараметр("Ссылка", СсылкаНаОбъект);
Результат = Запрос.Выполнить();
Возврат НЕ Результат.Пустой();
КонецФункции
Что решилось после исправления:
Функция работает одинаково с любым подтипом ДокументСсылка, СправочникСсылка, используя поле Предмет бизнес-процесса. Запрос универсален и не зависит от специфики объекта.
СправочникСсылка.Пользователи: возвращает Истина, если пользователь — предмет активного процесса.
ДокументСсылка.ЗаказКлиента: то же самое, независимо от статуса заказа.
Любой другой подтип: результат основан на общем правиле участия в процессах.
При соблюдении принципа код становится надёжным и универсальным, гарантируя сохранение контракта. Это упрощает использование методов, и исключает сбои и непредсказуемы результаты из-за специфичных условий.
1. ОбщегоНазначения.ПровестиДокументы()
Функция работает с любым подтипом ДокументСсылка, полагаясь на их общий контракт (методы ПолучитьОбъект(), ПроверитьЗаполнение(), Записать()). . Платформа 1С гарантирует, что все объекты типа ДокументОбъект имеют эти методы, так как это часть базовой архитектуры.
Подмена одного документа другим (например, "Реализация" на "Поступление") не нарушает поведение функции — она продолжает корректно проводить документы и возвращать результат в ожидаемом формате.
Контракт, заданный платформой для объектов типа ДокументОбъект, соблюдается всеми подтипами, что делает подстановку безопасной.
Подтипы (разные документы) взаимозаменяемы в рамках функции, и их специфическая реализация (например, какие регистры затрагиваются при проведении) не влияет на ожидаемый результат функции.
// Выполняет попытку проведения документов.
//
// Параметры:
// Документы - Массив из ДокументСсылка - документы, которые необходимо провести.
//
// Возвращаемое значение:
// Массив из Структура:
// * Ссылка - ДокументСсылка - документ, который не удалось провести,
// * ОписаниеОшибки - Строка - текст описания ошибки при проведении.
//
Функция ПровестиДокументы(Документы) Экспорт
НепроведенныеДокументы = Новый Массив;
Для Каждого ДокументСсылка Из Документы Цикл
ВыполненоУспешно = Ложь;
ДокументОбъект = ДокументСсылка.ПолучитьОбъект();
Если ДокументОбъект.ПроверитьЗаполнение() Тогда
РежимПроведения = РежимПроведенияДокумента.Неоперативный;
Если ДокументОбъект.Дата >= НачалоДня(ТекущаяДатаСеанса())
И ДокументСсылка.Метаданные().ОперативноеПроведение = Метаданные.СвойстваОбъектов.ОперативноеПроведение.Разрешить Тогда
РежимПроведения = РежимПроведенияДокумента.Оперативный;
КонецЕсли;
Попытка
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение, РежимПроведения);
ВыполненоУспешно = Истина;
Исключение
ПредставлениеОшибки = ОбработкаОшибок.КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;
Иначе
ПредставлениеОшибки = НСтр("ru = 'Поля документа не заполнены.'");
КонецЕсли;
Если Не ВыполненоУспешно Тогда
НепроведенныеДокументы.Добавить(Новый Структура("Ссылка,ОписаниеОшибки", ДокументСсылка, ПредставлениеОшибки));
КонецЕсли;
КонецЦикла;
Возврат НепроведенныеДокументы;
КонецФункции
2. РасчетСебестоимостиПрикладныеАлгоритмы.ДвиженияЗаписываютсяРасчетомПартийИСебестоимости()
Функция работает с базовым типом РегистрНакопленияНаборЗаписей и не делает предположений о специфичных свойствах подтипов, кроме наличия ДополнительныеСвойства, что гарантировано платформой.
Подстановка любого подтипа (конкретного регистра) не нарушает поведения: функция всегда возвращает Булево, как ожидается, без ошибок или побочных эффектов.
Контракт (вход — набор записей, выход — булево) соблюдается для всех подтипов.
// Возвращает признак записи движений по регистру механизмом расчета партий и себестоимости.
//
// Параметры:
// НаборЗаписей - РегистрНакопленияНаборЗаписей - набор записей регистра.
//
// Возвращаемое значение:
// Булево - Истина, если движения записываются регламентной операцией закрытия месяца.
//
Функция ДвиженияЗаписываютсяРасчетомПартийИСебестоимости(НаборЗаписей) Экспорт
// Проверим наличие служебного дополнительного свойства у набора записей
Возврат НаборЗаписей.ДополнительныеСвойства.Свойство(ИмяСлужебногоДополнительногоСвойстваОбъекта());
КонецФункции
3. ОбщегоНазначения.ЕстьСсылкиНаОбъект()
Функция работает с любым подтипом ЛюбаяСсылка (или массивом таких подтипов). Контракт (возврат Булево) сохраняется для всех подтипов, обеспечивая предсказуемость и надёжность.
// Проверяет наличие ссылок на объект в базе данных.
// При вызове в неразделенном сеансе не выявляет ссылок в разделенных областях.
//
// Параметры:
// СсылкаИлиМассивСсылок - ЛюбаяСсылка
// - Массив из ЛюбаяСсылка - объект или список объектов.
// ИскатьСредиСлужебныхОбъектов - Булево - если Истина, то не будут учитываться
// исключения поиска ссылок, заданные при разработке конфигурации.
// Про исключение поиска ссылок см. подробнее
// ОбщегоНазначенияПереопределяемый.ПриДобавленииИсключенийПоискаСсылок.
//
// Возвращаемое значение:
// Булево - Истина, если есть ссылки на объект.
//
Функция ЕстьСсылкиНаОбъект(Знач СсылкаИлиМассивСсылок, Знач ИскатьСредиСлужебныхОбъектов = Ложь) Экспорт
Если ТипЗнч(СсылкаИлиМассивСсылок) = Тип("Массив") Тогда
МассивСсылок = СсылкаИлиМассивСсылок;
Иначе
МассивСсылок = ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(СсылкаИлиМассивСсылок);
КонецЕсли;
УстановитьПривилегированныйРежим(Истина);
МестаИспользования = НайтиПоСсылкам(МассивСсылок);
УстановитьПривилегированныйРежим(Ложь);
Если Не ИскатьСредиСлужебныхОбъектов Тогда
Для каждого Элемент Из СлужебныеСвязиДанных(МестаИспользования) Цикл
МестаИспользования.Удалить(Элемент.Ключ);
КонецЦикла;
КонецЕсли;
Возврат МестаИспользования.Количество() > 0;
КонецФункции
1. ДенежныеСредства.ПроверитьВалютуКонвертации()
Неявный контракт, в описании указано "Текущий документ", но не уточняется, что процедура применима только к документам с определённой структурой. Это вводит в заблуждение о её универсальности. Код не проверяет наличие реквизитов перед обращением, полагаясь на их обязательное присутствие.
// Процедура проверяет валюту конвертации, указанную в документе.
//
// Параметры:
// ДокументОбъект - ДокументОбъект - Текущий документ
// Отказ - Булево - Признак отказа от продолжения работы
// ФлагОбменСБанками - Булево - Признак использования при обмене с банками
// ОшибкиЗаполнения - Строка - Строка, накапливающая ошибки проверок.
//
Процедура ПроверитьВалютуКонвертации(ДокументОбъект, Отказ, ФлагОбменСБанками = Ложь, ОшибкиЗаполнения = "") Экспорт
Если ДокументОбъект.ХозяйственнаяОперация = Перечисления.ХозяйственныеОперации.КонвертацияВалюты
И ЗначениеЗаполнено(ДокументОбъект.Валюта)
И ЗначениеЗаполнено(ДокументОбъект.ВалютаКонвертации)
Тогда
Если ДокументОбъект.Валюта = ДокументОбъект.ВалютаКонвертации Тогда
Текст = НСтр("ru = 'Валюта конвертации должна отличаться от валюты документа'");
Если ФлагОбменСБанками Тогда
ДобавитьОшибкуЗаполнения(ОшибкиЗаполнения, Текст);
Иначе
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
Текст,
ДокументОбъект,
"ВалютаКонвертации",
,
Отказ);
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
2. ЦенообразованиеКлиентСервер.ПолучитьСуммуДокумента()
Контракт не выдержан, в описании функции указано, что она работает с типом ТабличнаяЧасть, но не уточняется, что требуются колонки Сумма и СуммаНДС. Это вводит в заблуждение: ожидается универсальность, но фактически функция работает только с табличными частями определённой структуры.
// Возвращает сумму документа с учетом НДС
//
// Параметры:
// Товары - ТабличнаяЧасть - табличная часть документа для подсчета суммы документа.
// ЦенаВключаетНДС - Булево - признак включения НДС в цену документа.
//
// Возвращаемое значение:
// Число - Сумма документа с учетом НДС/.
//
Функция ПолучитьСуммуДокумента(Товары, Знач ЦенаВключаетНДС) Экспорт
СуммаДокумента = Товары.Итог("Сумма");
Если Не ЦенаВключаетНДС Тогда
СуммаДокумента = СуммаДокумента + Товары.Итог("СуммаНДС");
КонецЕсли;
Возврат СуммаДокумента;
КонецФункции // ПолучитьСуммуДокумента()
3. ПроведениеДокументов.ЕстьЗаписиВТаблице()
Метод предполагает, что у Документ.ДополнительныеСвойства есть вложенная структура ПроведениеДокументов.ТаблицыКонтроля, где хранится свойство ЕстьЗаписиВТаблице. Однако ДокументОбъект — это базовый тип для всех документов, и не все документы обязаны иметь такую структуру в ДополнительныеСвойства. Если подставить объект документа, у которого нет этой структуры, метод вызовет ошибку обращения к несуществующему свойству. Это нарушает принцип, так как при соблюдении контракта, подстановке допустимого объекта базового типа ДокументОбъект это приведет к сбою.
// Проверяет есть ли записи по указанной таблице контроля.
//
// Параметры:
// Документ - ДокументОбъект - записываемый документ
// ИмяТаблицы - Строка - имя таблицы контроля.
//
// Возвращаемое значение:
// Булево - Истина, если есть изменения по указанной таблице контроля.
//
Функция ЕстьЗаписиВТаблице(Документ, ИмяТаблицы) Экспорт
ТаблицыКонтроля = Документ.ДополнительныеСвойства.ПроведениеДокументов.ТаблицыКонтроля;
Возврат ТаблицыКонтроля.Свойство(ИмяТаблицы) И ТаблицыКонтроля[ИмяТаблицы].ЕстьЗаписиВТаблице;
КонецФункции
Результат применения
Принцип направлен на создание кода, который позволяет безопасно использовать подтипы вместо базовых типов, сохраняя предсказуемое поведение системы. Это обеспечивает целостность контрактов, упрощает повторное использование кода и повышает надёжность системы, что особенно важно в сложных конфигурациях 1С.
При следовании принципу код становится более надёжным и универсальным. Например, функции, работающие с общими типами, такими как "ДокументОбъект" или "ТабличнаяЧасть", корректно обрабатывают любые документы или табличные части без необходимости учитывать их специфику. Это позволяет избежать ошибок при добавлении новых документов или изменении структуры существующих, так как базовый контракт остаётся неизменным. Поддержка упрощается, поскольку разработчикам не приходится переписывать код для учёта особенностей подтипов. Тестирование тоже становится эффективнее: проверка базового поведения гарантирует работу с любыми подтипами, снижая объём дополнительных тестов для каждого нового объекта.
Представьте себе MP3-плеер с одной огромной кнопкой, на ней значки для включения, паузы, переключения треков, громкости, ещё какие-то странные иконки — доллары, стрелки, непонятные символы. Вы хотите просто послушать музыку, но вместо этого плеер начинает переводить деньги или рисовать графики. Удобно? Вряд ли. Так же и в коде, при обращениие к методу, ожидая только нужные функции, он тащит за собой кучу ненужных или неподходящих возможностей. Этого помогает избежать принцип разделения интерфейсов. Давайте разберёмся, как сделать плеер и код проще и понятнее.
Описание
Четвёртый принцип SOLID утверждает: объекты не должны быть вынуждены зависеть от интерфейсов, которые им не нужны. Клиент не должен подключаться к громоздкому интерфейсу с кучей ненужных функций и тащить этот балласт, усложняя логику и создавая путаницу. Принцип разделения интерфейса учит разбивать такие интерфейсы на простые и понятные части, чтобы система использовала только то, что ей действительно нужно. Разберём дальше.
Применение в 1С
Данные принцип тесно связан с архитектурой платформы, которая поощряет модульность и минимизацию зависимостей.
На уровне архитектуры платформа 1С предоставляет инструменты для реализации этого требования:
Общие модули с чётким разделением функций: в 1С принято создавать модули с узкой специализацией, например, отдельные модули для работы с номенклатурой, ценами или проведением документов. Это позволяет вызывающему коду подключать только необходимую функциональность, а не весь набор методов общего модуля.
Параметры функциональности: использование параметров сеанса или констант для включения/отключения определённых возможностей конфигурации позволяет модулям работать только с актуальными функциями, исключая ненужные зависимости от неиспользуемых участков кода.
Передача данных через структуры: вместо передачи объектов с полным набором реквизитов (например, ДокументОбъект) в 1С часто используются структуры или выборки с минимально необходимыми полями. Это снижает зависимость кода от полной структуры объекта и упрощает его повторное использование.
Интерфейсы в расширениях: расширения конфигурации позволяют добавлять новую логику, реализуя только те методы, которые нужны для конкретной задачи, без необходимости подключать весь интерфейс типового объекта. Например, расширение может перехватывать только обработку проведения, не затрагивая другие аспекты документа.
Нам нужно работать с данными о клиентах: получать их контактную информацию и проверять статус.
Нарушение принципа:
Посмотрим на вариант с нарушением принципа:
// Общий модуль "РаботаСКлиентами"
// Предоставляет функции для работы с клиентами, их контактами и отчётами
Функция ПолучитьДанныеКлиента(Клиент, ВключитьИсториюЗаказов = Ложь, ФайлДляЭкспорта = "") Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Наименование,
| Контрагенты.Телефон,
| Контрагенты.Активен
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Ссылка = &Клиент";
Запрос.УстановитьПараметр("Клиент", Клиент);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Результат = Новый Структура;
Результат.Вставить("Наименование", Выборка.Наименование);
Результат.Вставить("Телефон", Выборка.Телефон);
Результат.Вставить("Активен", Выборка.Активен);
// Навязанная функциональность: история заказов
Если ВключитьИсториюЗаказов Тогда
ИсторияЗаказов = ПолучитьИсториюЗаказов(Клиент);
Результат.Вставить("ИсторияЗаказов", ИсторияЗаказов);
КонецЕсли;
// Навязанная функциональность: экспорт в файл
Если ЗначениеЗаполнено(ФайлДляЭкспорта) Тогда
ТабДок = СформироватьТабличныйДокументКлиента(Клиент, Результат);
ТабДок.Записать(ФайлДляЭкспорта, ТипФайлаТабличногоДокумента.PDF);
КонецЕсли;
Возврат Результат;
КонецФункции
Функция ПроверитьСтатусКлиента(Клиент) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Активен
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Ссылка = &Клиент";
Запрос.УстановитьПараметр("Клиент", Клиент);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Возврат Выборка.Активен;
КонецФункции
Функция СформироватьОтчетПоКлиентам(Период, СписокКлиентов) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Наименование,
| Контрагенты.Телефон
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Ссылка В (&СписокКлиентов)";
Запрос.УстановитьПараметр("СписокКлиентов", СписокКлиентов);
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
// Служебные методы
Функция ПолучитьИсториюЗаказов(Клиент)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказКлиента.Номер,
| ЗаказКлиента.Дата
|ИЗ
| Документ.ЗаказКлиента КАК ЗаказКлиента
|ГДЕ
| ЗаказКлиента.Контрагент = &Клиент";
Запрос.УстановитьПараметр("Клиент", Клиент);
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
Функция СформироватьТабличныйДокументКлиента(Клиент, ДанныеКлиента)
ТабДок = Новый ТабличныйДокумент;
// Логика формирования отчёта
Возврат ТабДок;
КонецФункции
Что плохого в этом примере:
Перегруженный модуль:
Модуль РаботаСКлиентами содержит три экспортируемых метода: ПолучитьДанныеКлиента, ПроверитьСтатусКлиента и СформироватьОтчетПоКлиентам. Это смешивает функции для работы с данными клиентов и отчётности, навязывая клиентам лишнюю функциональность.
Например, документ "Заказ клиента" хочет только данные клиента, но зависит от модуля, включающего отчёты.
Перегруженный метод:
Метод ПолучитьДанныеКлиента делает больше, чем нужно большинству клиентов:
Параметр ВключитьИсториюЗаказов добавляет историю заказов, которая нужна только для специфичных случаев (например, отчётов), но не для базового получения данных.
Параметр ФайлДляЭкспорта заставляет метод заниматься экспортом в PDF, что не относится к задаче получения данных и навязывает побочный эффект.
Вызывающий код, которому нужны только Наименование и Телефон, вынужден учитывать эти параметры и их последствия, даже если они не используются.
Последствия:
Документ "Заказ клиента" вызывает ПолучитьДанныеКлиента(Клиент), но получает доступ к истории заказов и экспорту, которые ему не нужны, усложняя логику.
Изменение логики экспорта (например, формата файла) может сломать работу документа, хотя он этого не использует.
Тестирование усложняется: нужно проверять все варианты параметров, даже если клиент использует метод минимально.
Соблюдение принципа:
Разделим функциональность на два модуля и упростим метод, убрав навязанные параметры:
Общий модуль "ДанныеКлиентов":
// Общий модуль "ДанныеКлиентов"
// Предоставляет минимальные функции для работы с данными клиентов
Функция ПолучитьДанныеКлиента(Клиент) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Наименование,
| Контрагенты.Телефон,
| Контрагенты.Активен
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Ссылка = &Клиент";
Запрос.УстановитьПараметр("Клиент", Клиент);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Результат = Новый Структура;
Результат.Вставить("Наименование", Выборка.Наименование);
Результат.Вставить("Телефон", Выборка.Телефон);
Результат.Вставить("Активен", Выборка.Активен);
Возврат Результат;
КонецФункции
Функция ПроверитьСтатусКлиента(Клиент) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Активен
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Ссылка = &Клиент";
Запрос.УстановитьПараметр("Клиент", Клиент);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Возврат Выборка.Активен;
КонецФункции
Общий модуль "ОтчетыПоКлиентам":
// Общий модуль "ОтчетыПоКлиентам"
// Предоставляет функции для формирования отчётов и экспорта
Функция ПолучитьИсториюЗаказовКлиента(Клиент) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗаказКлиента.Номер,
| ЗаказКлиента.Дата
|ИЗ
| Документ.ЗаказКлиента КАК ЗаказКлиента
|ГДЕ
| ЗаказКлиента.Контрагент = &Клиент";
Запрос.УстановитьПараметр("Клиент", Клиент);
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
Функция СформироватьОтчетПоКлиентам(Период, СписокКлиентов) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Наименование,
| Контрагенты.Телефон
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Ссылка В (&СписокКлиентов)";
Запрос.УстановитьПараметр("СписокКлиентов", СписокКлиентов);
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
Функция ЭкспортироватьДанныеКлиентаВФайл(Клиент, ПутьКФайлу) Экспорт
ДанныеКлиента = ДанныеКлиентов.ПолучитьДанныеКлиента(Клиент);
ТабДок = Новый ТабличныйДокумент;
// Логика формирования отчёта
ТабДок.Записать(ПутьКФайлу, ТипФайлаТабличногоДокумента.PDF);
Возврат Истина;
КонецФункции
Что решилось после исправления:
Разделённые модули:
ДанныеКлиентов предоставляет только базовые данные и проверку статуса то, что нужно документам вроде "Заказ клиента".
ОтчетыПоКлиентам содержит функции отчётности и экспорта, используемые формами отчётов или для специфичных задач.
Упрощённый метод:
ПолучитьДанныеКлиента возвращает только основные данные клиента без дополнительных параметров и побочных эффектов. История заказов и экспорт вынесены в отдельные методы модуля ОтчетыПоКлиентам.
Вызывающий код получает ровно то, что ему нужно, без навязывания ненужной функциональности.
Таким образом, документ "Заказ клиента" зависит только от ДанныеКлиентов, не подключая отчёты или экспорт в файл. Изменение логики экспорта в ОтчетыПоКлиентам не влияет на работу документов. Тестирование проще, каждый метод и модуль проверяется в рамках своей задачи.
Методы имеют минимальный интерфейс, минимальное количество параметров, строго соответствующих задаче. Не навязывают вызывающему коду лишнюю функциональность, зависимости или побочные эффекты. Клиент зависит только от того, что ему нужно и ничего сверх того.
// Удаляет двойные кавычки с начала и конца строки, если они есть.
//
// Параметры:
// Значение - Строка - входная строка.
//
// Возвращаемое значение:
// Строка - строка без двойных кавычек.
//
Функция СократитьДвойныеКавычки(Знач Значение) Экспорт
Пока СтрНачинаетсяС(Значение, """") Цикл
Значение = Сред(Значение, 2);
КонецЦикла;
Пока СтрЗаканчиваетсяНа(Значение, """") Цикл
Значение = Лев(Значение, СтрДлина(Значение) - 1);
КонецЦикла;
Возврат Значение;
КонецФункции
// Служебная функция, предназначенная для получения описания типов даты
//
// Параметры:
// ЧастиДаты - ЧастиДаты - Системное перечисление ЧастиДаты.
//
// Возвращаемое значение:
// ОписаниеТипов -
Функция ПолучитьОписаниеТиповДаты(ЧастиДаты) Экспорт
Возврат Новый ОписаниеТипов("Дата", , , Новый КвалификаторыДаты(ЧастиДаты));
КонецФункции
// Создает массив и помещает в него переданное значение.
//
// Параметры:
// Значение - Произвольный - любое значение.
//
// Возвращаемое значение:
// Массив - массив из одного элемента.
//
Функция ЗначениеВМассиве(Знач Значение) Экспорт
Результат = Новый Массив;
Результат.Добавить(Значение);
Возврат Результат;
КонецФункции
1. ОбщегоНазначенияУТ.ИзменитьПризнакСогласованностиДокумента()
Предоставляет интерфейс, который шире, чем нужно некоторым клиентам. Параметр СтатусНеСогласован с поддержкой массивов и сложная логика для разных режимов записи навязывают функциональность, которая не требуется клиентам с простыми задачами (например, только сброс Согласован при записи).
// Устанавливает или сбрасывает флаг Согласован у документа.
// Вызывается из процедуры ПередЗаписью документа.
//
// Параметры:
// ДокументОбъект - ДокументОбъект - Документ, в котором необходимо изменить флаг Согласован
// РежимЗаписи - РежимЗаписиДокумента - Режим записи документа
// СтатусНеСогласован - ПеречислениеСсылка - Статус документа, в котором флаг Согласован должен быть сброшен.
//
// Процедура ИзменитьПризнакСогласованностиДокумента(ДокументОбъект, Знач РежимЗаписи, Знач СтатусНеСогласован = Неопределено) Экспорт
Если РежимЗаписи = РежимЗаписиДокумента.Запись
ИЛИ РежимЗаписи = РежимЗаписиДокумента.ОтменаПроведения Тогда
Если ДокументОбъект.Согласован Тогда
ДокументОбъект.Согласован = Ложь;
КонецЕсли;
ИначеЕсли РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
// Документ не имеет статуса
Если СтатусНеСогласован = Неопределено Тогда
Если Не ДокументОбъект.Согласован Тогда
ДокументОбъект.Согласован = Истина;
КонецЕсли;
// Документ имеет статус из массива, в которых проведенный документ не согласован
ИначеЕсли ТипЗнч(СтатусНеСогласован) = Тип("Массив") Тогда
Если ДокументОбъект.Согласован Тогда
Для Каждого ТекСтатус Из СтатусНеСогласован Цикл
Если ДокументОбъект.Статус = ТекСтатус Тогда
ДокументОбъект.Согласован = Ложь;
Прервать;
КонецЕсли;
КонецЦикла;
Иначе
ДокументСогласован = Истина;
Для Каждого ТекСтатус Из СтатусНеСогласован Цикл
Если ДокументОбъект.Статус = ТекСтатус Тогда
ДокументСогласован = Ложь;
КонецЕсли;
КонецЦикла;
Если ДокументСогласован Тогда
ДокументОбъект.Согласован = Истина;
КонецЕсли;
КонецЕсли;
// Документ имеет статус, в котором проведенный документ не согласован
Иначе
Если ДокументОбъект.Статус = СтатусНеСогласован И ДокументОбъект.Согласован Тогда
ДокументОбъект.Согласован = Ложь;
ИначеЕсли ДокументОбъект.Статус <> СтатусНеСогласован И Не ДокументОбъект.Согласован Тогда
ДокументОбъект.Согласован = Истина;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
2. ОбработкаТабличнойЧастиСервер.ПроверитьКорректностьЗаполнитьХарактеристикиИУпаковки()
Объединяет несколько независимых задач (проверка характеристик, упаковок, заполнение данных для некачественного товара) в одном методе с избыточным интерфейсом через СтруктураДействий. Клиенту, которому нужна только проверка характеристики, навязывается логика работы с упаковками и некачественным товаром, включая ненужные зависимости от дополнительных свойств структуры и сложных условий. Это делает интерфейс максимально перегруженным для клиентов с узкими задачами, что противоречит принципу.
Процедура ПроверитьКорректностьЗаполнитьХарактеристикиИУпаковки(ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения) Экспорт
Перем Характеристика;
Перем Упаковка;
ПроверитьХарактеристикуПоВладельцу = СтруктураДействий.Свойство("ПроверитьХарактеристикуПоВладельцу", Характеристика);
ПроверитьЗаполнитьУпаковкуПоВладельцу = СтруктураДействий.Свойство("ПроверитьЗаполнитьУпаковкуПоВладельцу", Упаковка);
Если ПроверитьХарактеристикуПоВладельцу
Или ПроверитьЗаполнитьУпаковкуПоВладельцу Тогда
РезультатПроверки = Справочники.Номенклатура.ХарактеристикаИУпаковкаПринадлежатВладельцу(ТекущаяСтрока.Номенклатура, Характеристика, Упаковка);
Если ПроверитьХарактеристикуПоВладельцу Тогда
ТекущаяСтрока.Характеристика = РезультатПроверки.Характеристика;
ТекущаяСтрока.ХарактеристикиИспользуются = РезультатПроверки.ХарактеристикиИспользуются;
КонецЕсли;
Если ПроверитьЗаполнитьУпаковкуПоВладельцу Тогда
ТекущаяСтрока.Упаковка = РезультатПроверки.Упаковка;
КонецЕсли;
Если СтруктураДействий.Свойство("ЗаполнитьУпаковкуНекачественногоТовара")
И ПроверитьЗаполнитьУпаковкуПоВладельцу
И ЗначениеЗаполнено(ТекущаяСтрока.Номенклатура)
И НЕ ЗначениеЗаполнено(ТекущаяСтрока.Упаковка) Тогда
ТекущаяСтрока.Упаковка = Справочники.УпаковкиЕдиницыИзмерения.ИдентичнаяУпаковка(ТекущаяСтрока.НоменклатураИсходногоКачества,
ТекущаяСтрока.Номенклатура,
Упаковка);
КонецЕсли;
Если СтруктураДействий.Свойство("ЗаполнитьХарактеристикуНекачественногоТовара")
И ЗначениеЗаполнено(Характеристика)
И Не ЗначениеЗаполнено(ТекущаяСтрока.Характеристика)
И ТекущаяСтрока.Номенклатура.ИспользованиеХарактеристик = Перечисления.ВариантыИспользованияХарактеристикНоменклатуры.ИндивидуальныеДляНоменклатуры Тогда
ТекущаяСтрока.Характеристика = Справочники.ХарактеристикиНоменклатуры.ИдентичнаяХарактеристика(
ТекущаяСтрока.НоменклатураИсходногоКачества,
ТекущаяСтрока.Номенклатура,
Характеристика);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
3. ОбработкаТабличнойЧастиСервер.СкорректироватьСтавкуНДСВСтрокеТЧ()
Объединяет корректировку ставки НДС с избыточной логикой (проверка многооборотной тары, работа с кэшем, обработка на основании копирования, динамическое имя поля номенклатуры) в одном методе, зависящем от сложной структуры СтруктураДействий. Клиенту, которому нужно просто скорректировать ставку НДС, навязывается весь функционал, включая ненужные проверки и зависимости от кэшированных значений, что делает интерфейс перегруженным.
Процедура СкорректироватьСтавкуНДСВСтрокеТЧ(ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения) Экспорт
Перем СтруктураПараметровДействия;
Перем ВернутьМногооборотнуюТару;
Перем ТипНоменклатуры;
Если СтруктураДействий.Свойство("СкорректироватьСтавкуНДС", СтруктураПараметровДействия) Тогда
Если СтруктураПараметровДействия.ИнициализацияВходящегоДокумента И ЗначениеЗаполнено(ТекущаяСтрока.СтавкаНДС) Тогда
Возврат;
КонецЕсли;
СтруктураПараметровДействия.Свойство("ВернутьМногооборотнуюТару", ВернутьМногооборотнуюТару);
НалогообложениеНДС = СтруктураПараметровДействия.НалогообложениеНДС;
Дата = СтруктураПараметровДействия.Дата;
Организация = СтруктураПараметровДействия.Организация;
ИмяПоляНоменклатура = "Номенклатура";
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(СтруктураПараметровДействия, "ИмяПоляНоменклатура")
И ЗначениеЗаполнено(СтруктураПараметровДействия.ИмяПоляНоменклатура) Тогда
ИмяПоляНоменклатура = СтруктураПараметровДействия.ИмяПоляНоменклатура;
КонецЕсли;
Номенклатура = Неопределено;
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ТекущаяСтрока, ИмяПоляНоменклатура) Тогда
Номенклатура = ТекущаяСтрока[ИмяПоляНоменклатура];
КонецЕсли;
Если ВернутьМногооборотнуюТару Тогда
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ТекущаяСтрока, "ТипНоменклатуры") Тогда
ТипНоменклатуры = ТекущаяСтрока.ТипНоменклатуры;
ИначеЕсли ЗначениеЗаполнено(Номенклатура) Тогда
ТипыНоменклатуры = КэшированныеЗначения.ТипыНоменклатуры; //Соответствие
ТипНоменклатуры = ТипыНоменклатуры.Получить(Номенклатура);
КонецЕсли;
КонецЕсли;
Если ТипНоменклатуры = ПредопределенноеЗначение("Перечисление.ТипыНоменклатуры.МногооборотнаяТара") Тогда
СтавкаНДС = ПредопределенноеЗначение("Справочник.СтавкиНДС.БезНДС");
Иначе
ПроверятьАктуальность = Истина;
Если ТекущаяСтрока.СтавкаНДС = ПредопределенноеЗначение("Справочник.СтавкиНДС.БезНДС")
И НалогообложениеНДС = ПредопределенноеЗначение("Перечисление.ТипыНалогообложенияНДС.ПродажаОблагаетсяНДС")
И СтруктураПараметровДействия.ЗаполнениеНаОснованииКопирование = Ложь Тогда
ПроверятьАктуальность = Ложь;
КонецЕсли;
Если ПроверятьАктуальность Тогда
Если КэшированныеЗначения.АктуальныеСтавкиНДС = Неопределено Тогда
ЗаполнитьАктуальныеСтавкиНДСКэшированныеЗначения(СтруктураПараметровДействия, КэшированныеЗначения);
КонецЕсли;
АктуальныеСтавкиНДС = КэшированныеЗначения.АктуальныеСтавкиНДС; //Массив
Если АктуальныеСтавкиНДС.Найти(ТекущаяСтрока.СтавкаНДС) <> Неопределено Тогда
Возврат;
КонецЕсли;
КонецЕсли;
СтавкаНДС = УчетНДСУП.СтавкаНДСПоНоменклатуреИНалогообложению(Номенклатура, НалогообложениеНДС, Организация, Дата);
КонецЕсли;
Если ТекущаяСтрока.СтавкаНДС <> СтавкаНДС тогда
ТекущаяСтрока.СтавкаНДС = СтавкаНДС;
ОбработанныеСтроки = КэшированныеЗначения.ОбработанныеСтроки; //Массив
ОбработанныеСтроки.Добавить(ТекущаяСтрока);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Результат применения
Принцип направлен на создание кода, в котором клиент зависит только от той функциональности, которая ему действительно необходима, избегая избыточных или ненужных зависимостей. Каждый метод или модуль должен быть спроектирован так, чтобы предоставлять минимальный интерфейс, достаточный для выполнения конкретной задачи, не заставляя клиента обрабатывать лишние методы или данные. Это повышает модульность, упрощает повторное использование и снижает сложность системы.
При следовании принципу код становится более гибким и поддерживаемым. Например, функции для работы с номенклатурой или налогами предоставляют только необходимые методы, такие как расчёт ставки НДС или получение характеристик, без навязывания дополнительных операций вроде формирования отчётов или обработки кэша. Это позволяет избежать конфликтов при доработках, так как изменения в одной части функциональности не затрагивают клиентов, использующих другую. Разработка ускоряется, поскольку новые модули или расширения подключают только нужные интерфейсы, а не громоздкие общие. Тестирование упрощается: каждый модуль проверяется изолированно, без необходимости учитывать избыточные зависимости, что снижает риск побочных эффектов и облегчает сопровождение конфигурации.
Наверное, многие сталкивались с такой ситуацией, когда нужно срочно зарядить свой iPhone или Android, а у друзей под рукой только неподходящий кабель. Вы пытаетесь подключить его, но безуспешно. Всё дело в том, что устройство слишком жёстко привязано к своему типу зарядки, и эта зависимость ломает весь процесс. Так же и в коде, когда есть прямая зависимость от конкретных реализаций, любая мелочь вроде смены "кабеля" может остановить работу системы. Принцип инверсии зависимостей учит нас, как сделать "зарядку" универсальной, чтобы код работал гибко и без сбоев. Разбираемся дальше.
Описание
Пятый принцип SOLID утверждает: модули высокого уровня не должны зависеть от конкретных реализаций низкого уровня — оба должны зависеть от абстракций. Представьте, что ваш iPhone не заряжается от кабеля для Android: он привязан к Lightning-разъёму, и любой другой кабель бесполезен. Проблема в том, что телефон зависит от конкретного "низкоуровневого" разъёма, а не от универсального стандарта. Абстракция здесь — это контракт: "Мне нужно зарядить устройство или передать данные". Пользователю не важны детали — чипы, провода или тип разъёма, — главное, чтобы зарядка работала. С разъемом Lightning это не так: телефон и кабель связаны жёстко, и смена одного ломает всё.
Инверсия зависимостей решает это через универсальный стандарт, вроде USB-C. Высокоуровневый модуль (телефон) задаёт контракт: "Я принимаю зарядку через USB-C". Низкоуровневый модуль (кабель) подстраивается, реализуя этот стандарт. В коде это работает так же: вместо прямого вызова метода из конкретного модуля (например, модуля менеджера справочника в 1С) мы создаём общий интерфейс, через который взаимодействуют оба уровня. Такой подход делает систему гибкой: модуль можно заменить, не трогая остальной код. В итоге вы получаете устойчивую архитектуру, как телефон, который заряжается от любого совместимого USB-C кабеля. Разберём дальше.
Применение в 1С
На уровне архитектуры платформа 1С предоставляет инструменты для реализации этого требования:
Общие модули как абстракции: в 1С принято создавать общие модули с чётко определёнными интерфейсами, которые скрывают детали реализации. Например, какой-то общий модуль может предоставлять функции, при этом полагаясь на более низкий уровень, например служебные модули или конкретные менеджеры, вызывающий код обращается к ним через экспортируемые методы, не зная, как именно они работают внутри.
Подписки на события: используются для передачи управления между модулями через абстрактные события, а не прямые вызовы конкретных процедур. Например, подписка на событие ПередЗаписью позволяет дополнить логику документа, не привязываясь к его внутренней реализации.
Переопределяемые модули: общие модули с суффиксом "Переопределяемый" также служат абстрактным слоем между типовой логикой и её доработками.
Расширения: позволяют внедрять новую логику через абстрактные точки расширения, такие как обработчики событий или перехват методов, вместо прямого изменения типовых объектов. Это обеспечивает независимость основной конфигурации от доработок.
Нам нужно получать данные о номенклатуре (например, цену и единицу измерения) для документа "Заказ клиента".
Нарушение принципа:
Посмотрим на вариант с нарушением принципа, модуль объекта документа "Заказ клиента":
// Модуль объекта документа "Заказ клиента"
Процедура ПриЗаписи(Отказ)
Для Каждого СтрокаТЧ Из Товары Цикл
// Прямая зависимость от справочника "Номенклатура"
ДанныеНоменклатуры = Справочники.Номенклатура.ПолучитьДанныеНоменклатуры(СтрокаТЧ.Номенклатура);
СтрокаТЧ.Цена = ДанныеНоменклатуры.Цена;
СтрокаТЧ.ЕдиницаИзмерения = ДанныеНоменклатуры.ЕдиницаИзмерения;
КонецЦикла;
КонецПроцедуры
Модуль менеджера справочника "Номенклатура":
// Справочник "Номенклатура", модуль менеджера
Функция ПолучитьДанныеНоменклатуры(Номенклатура) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатуры.Цена,
| Номенклатура.ЕдиницаИзмерения
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатуры
| ПО Номенклатура.Ссылка = ЦеныНоменклатуры.Номенклатура
|ГДЕ
| Номенклатура.Ссылка = &Номенклатура";
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Результат = Новый Структура;
Результат.Вставить("Цена", Выборка.Цена);
Результат.Вставить("ЕдиницаИзмерения", Выборка.ЕдиницаИзмерения);
Возврат Результат;
КонецФункции
Что плохого в этом примере:
Жёсткая зависимость: модуль документа "Заказ клиента" напрямую зависит от конкретной реализации справочника Номенклатура и его метода ПолучитьДанныеНоменклатуры.
Суть проблемы: Если нужно изменить источник данных (например, брать цену из другого регистра или внешнего источника), придётся переписывать код в документе. Документ привязан к низкоуровневой детали (справочнику), а не к абстракции, что делает систему хрупкой.
Последствия: Невозможно легко подменить логику получения данных без изменения модуля документа. Тестирование затруднено.
Соблюдение принципа:
Для соблюдения принципа разделим зависимость через абстрактный интерфейс в общем модуле и реализацию в отдельном модуле.
Общий модуль НоменклатураСервер:
// Общий модуль "НоменклатураСервер" (без конкретной реализации)
Функция ПолучитьДанныеНоменклатуры(Номенклатура) Экспорт
// Абстрактный интерфейс, делегирует вызов конкретной реализации
Возврат РаботаСНоменклатурой.ПолучитьДанныеНоменклатуры(Номенклатура);
КонецФункции
Общий модуль РаботаСНоменклатурой:
// Общий модуль "РаботаСНоменклатурой" (конкретная реализация)
Функция ПолучитьДанныеНоменклатуры(Номенклатура) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатуры.Цена,
| Номенклатура.ЕдиницаИзмерения
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатуры
| ПО Номенклатура.Ссылка = ЦеныНоменклатуры.Номенклатура
|ГДЕ
| Номенклатура.Ссылка = &Номенклатура";
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Результат = Новый Структура;
Результат.Вставить("Цена", Выборка.Цена);
Результат.Вставить("ЕдиницаИзмерения", Выборка.ЕдиницаИзмерения);
Возврат Результат;
КонецФункции
Модуль объекта документа "Заказ клиента":
// Модуль объекта документа "Заказ клиента"
Процедура ПриЗаписи(Отказ)
Для Каждого СтрокаТЧ Из Товары Цикл
// Зависимость от абстракции, а не от конкретной реализации
ДанныеНоменклатуры = НоменклатураСервер.ПолучитьДанныеНоменклатуры(СтрокаТЧ.Номенклатура);
СтрокаТЧ.Цена = ДанныеНоменклатуры.Цена;
СтрокаТЧ.ЕдиницаИзмерения = ДанныеНоменклатуры.ЕдиницаИзмерения;
КонецЦикла;
КонецПроцедуры
Что решилось после исправления:
Зависимость от абстракции: документ "Заказ клиента" зависит от общего модуля НоменклатураСервер (абстракция), а не от конкретного справочника или реализации.
Гибкость: логика получения данных вынесена в РаботаСНоменклатурой. Если нужно изменить, достаточно изменить этот модуль, не трогая документ.
Преимущества: легко подменить реализацию через переопределение в НоменклатураСервер или расширение. Тестирование упрощается, можно подставить mock-объект для тестов.
Все методы в общем модуле выступают абстрактным интерфейсом для высокоуровневого кода. Они скрывают детали реализации, делегируя их низкоуровневым служебным модулям. Вызывающий код зависит от абстракции (этих методов), а не от конкретной реализации, которая находится в служебном модуле, что соответствует принципу. Инверсия достигнута, высокоуровневая логика определяет контракт, а низкоуровневая подстраивается под него.
// Получение списка производителей категории.
//
// Параметры:
// ИдентификаторКатегории - Строка - идентификатор категории.
// АдресРезультата - Строка - адрес результата.
//
Процедура ПолучитьПроизводителейКатегории(Знач ИдентификаторКатегории, Знач АдресРезультата) Экспорт
Отказ = Ложь;
ПараметрыЗапроса = РаботаСНоменклатуройСлужебный.ОписаниеПараметровЗапросаПроизводители();
ПараметрыЗапроса.ИдентификаторыКатегорий = ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(ИдентификаторКатегории);
ПараметрыЗапроса.НаборПолей = РаботаСНоменклатуройСлужебный.НаборПолейМинимальный();
ПараметрыКоманды = РаботаСНоменклатуройСлужебный.ПараметрыЗапросаПроизводители(ПараметрыЗапроса);
Производители = РаботаСНоменклатуройСлужебный.ВыполнитьКомандуСервиса(ПараметрыКоманды, Отказ);
Если Отказ Тогда
Возврат;
КонецЕсли;
ПоместитьВоВременноеХранилище(Производители, АдресРезультата);
КонецПроцедуры
// Возвращает текущую настройку доступности усовершенствования подписей.
//
// Возвращаемое значение:
// Булево - если Истина, электронные подписи используются.
//
Функция ДоступнаУсовершенствованнаяПодпись() Экспорт
Возврат ЭлектроннаяПодписьСлужебныйПовтИсп.ДоступнаУсовершенствованнаяПодпись();
КонецФункции
// Возвращает текущего пользователя или текущего внешнего пользователя,
// в зависимости от того, кто выполнил вход в сеанс.
// Рекомендуется использовать в коде, который поддерживает работу в обоих случаях.
//
// Возвращаемое значение:
// СправочникСсылка.Пользователи, СправочникСсылка.ВнешниеПользователи - пользователь
// или внешний пользователь.
//
Функция АвторизованныйПользователь() Экспорт
Возврат ПользователиСлужебный.АвторизованныйПользователь();
КонецФункции
1. ЗаказНаСборку.ОформленоКомплектов()
Высокоуровневая функция ОформленоКомплектов зависит от низкоуровневых модулей (РегистрыНакопления ЗаказыНаСборку, ТоварыКПоступлению) вместо абстракций. Нет инверсии зависимостей: зависимости направлены от высокоуровневого кода к низкоуровневым деталям, а не к абстрактным интерфейсам.
// Возвращает количество собранных (разобранных) комплектов по переданному заказу
//
// Параметры:
// Объект - ДокументОбъект.ЗаказНаСборку, ДокументСсылка.ЗаказНаСборку -
//
// Возвращаемое значение:
// Число -
//
Функция ОформленоКомплектов(Объект) Экспорт
ОтборОформлено = Новый ТаблицаЗначений();
ОтборОформлено.Колонки.Добавить("КодСтроки", Новый ОписаниеТипов("Число"));
ОтборОформлено.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("ДокументСсылка.ЗаказНаСборку"));
СтрокаОтбораКомлектов = ОтборОформлено.Добавить();
СтрокаОтбораКомлектов.Ссылка = Объект.Ссылка;
СтрокаОтбораКомлектов.КодСтроки = 1;
ТаблицаШапки = Новый ТаблицаЗначений();
ТаблицаШапки.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаШапки.Колонки.Добавить("Характеристика", Новый ОписаниеТипов("СправочникСсылка.ХарактеристикиНоменклатуры"));
ТаблицаШапки.Колонки.Добавить("Склад", Новый ОписаниеТипов("СправочникСсылка.Склады"));
ТаблицаШапки.Колонки.Добавить("Назначение", Новый ОписаниеТипов("СправочникСсылка.Назначения"));
ТаблицаШапки.Колонки.Добавить("Серия", Новый ОписаниеТипов("СправочникСсылка.СерииНоменклатуры"));
ТаблицаШапки.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("ДокументСсылка.ЗаказНаСборку"));
Корректировка = ТаблицаШапки.Скопировать(); // пустая табличная часть для передачи в регистры
Корректировка.Колонки.Добавить("КПоступлению", Новый ОписаниеТипов("Число"));
Корректировка.Колонки.Добавить("КОтгрузке", Новый ОписаниеТипов("Число"));
УчитыватьНазначение = ЗначениеЗаполнено(Объект.Назначение)
И ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Объект.Назначение, "ДвиженияПоСкладскимРегистрам") = Истина;
Если Объект.СтатусУказанияСерий = 14 Тогда
СтрокаШапки = ТаблицаШапки.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаШапки, Объект);
СтрокаШапки.Назначение = ?(УчитыватьНазначение, Объект.Назначение, Справочники.Назначения.ПустаяСсылка());
Иначе
// СтатусУказанияСерий = 10
// Используется табличная часть Серии.
Для Каждого Строка Из Объект.Серии Цикл
СтрокаШапки = ТаблицаШапки.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаШапки, Строка, "Номенклатура, Характеристика, Серия, Назначение");
ЗаполнитьЗначенияСвойств(СтрокаШапки, Объект, "Склад, Ссылка");
КонецЦикла;
КонецЕсли;
Отбор = Новый Структура;
Если Объект.ХозяйственнаяОперация = Перечисления.ХозяйственныеОперации.РазборкаТоваров Тогда
Отбор.Вставить("ТипСборки", Перечисления.ТипыДвиженияЗапасов.Отгрузка);
ОформитьКомплектовПоНакладным = РегистрыНакопления.ЗаказыНаСборку.ТаблицаОформлено(ОтборОформлено, Отбор);
ОформитьКомплектовПоОрдерам = РегистрыНакопления.ТоварыКОтгрузке.ТаблицаОформлено(ТаблицаШапки, Корректировка);
Иначе
Отбор.Вставить("ТипСборки", Перечисления.ТипыДвиженияЗапасов.Поступление);
ОформитьКомплектовПоНакладным = РегистрыНакопления.ЗаказыНаСборку.ТаблицаОформлено(ОтборОформлено, Отбор);
ОформитьКомплектовПоОрдерам = РегистрыНакопления.ТоварыКПоступлению.ТаблицаОформлено(ТаблицаШапки, Корректировка);
КонецЕсли;
ОформитьКомплектовПоНакладным.Свернуть(, "Количество");
ОформитьКомплектовПоОрдерам.Свернуть(, "Количество");
ОформленоКомплектов = Макс(ОформитьКомплектовПоНакладным.Итог("Количество"), ОформитьКомплектовПоОрдерам.Итог("Количество"));
Возврат ОформленоКомплектов;
КонецФункции
2. АссортиментСервер.ОбъектПланирования()
Напрямую зависит от конкретного регистра сведений ИсторияИзмененияФорматовМагазинов и его метода ТекущийФормат(). Это низкоуровневая реализация, связанная с хранением данных в платформе 1С. Высокоуровневая логика определения объекта планирования не должна знать, откуда именно берётся текущий формат (из регистра, справочника или другого источника). Вместо этого можно было бы использовать абстракцию, например, интерфейс или функцию, реализацию которой можно подменять.
// Функция возвращает значение текущего объекта планирования ассортимента в зависимости от настроек.
//
// Параметры:
// ОбъектПроверки - СправочникСсылка.Склады, СправочникСсылка.ФорматыМагазинов - Склад или формат для которого
// определяется объект планирования
// НаДату - Дата - Дата на которую определяется текущий объект планирования.
//
// Возвращаемое значение:
// СправочникСсылка.ФорматыМагазинов, СправочникСсылка.Склады - текущий объект планирования, в зависимости от ФО -
// формат магазина или склад-магазин.
//
Функция ОбъектПланирования(ОбъектПроверки, Знач НаДату = Неопределено) Экспорт
Если Не ЗначениеЗаполнено(НаДату) Тогда
НаДату = ТекущаяДатаСеанса();
КонецЕсли;
Если ТипЗнч(ОбъектПроверки) = Тип("СправочникСсылка.Склады") И ПолучитьФункциональнуюОпцию("ИспользоватьФорматыМагазинов") Тогда
ОбъектПланирования = РегистрыСведений.ИсторияИзмененияФорматовМагазинов.ТекущийФормат(ОбъектПроверки, НаДату);
Иначе
ОбъектПланирования = ОбъектПроверки;
КонецЕсли;
Возврат ОбъектПланирования;
КонецФункции
3. РаспознаваниеДокументовУП.СтавкаНДСПоРаспознанномуТексту()
Выполняет высокоуровневую задачу, преобразование текстового представления ставки НДС в объект справочника. Однако вместо того чтобы зависеть от абстракций, она напрямую обращается к низкоуровневым объектам платформы (Справочники.СтавкиНДС, Перечисления.СтавкиНДС). Это нарушает принцип, так как Нет инверсии зависимостей, зависимости направлены от высокоуровневой логики к низкоуровневым деталям, а не к абстракциям. Жёсткая связность: Код привязан к конкретной реализации хранения ставок НДС.
Функция СтавкаНДСПоРаспознанномуТексту(Ставка, ТекущееЗначение) Экспорт
Если Ставка = "НДС20" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС20);
ИначеЕсли Ставка = "НДС18" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС18);
ИначеЕсли Ставка = "НДС10" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС10);
ИначеЕсли Ставка = "НДС18_118" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС18_118);
ИначеЕсли Ставка = "НДС10_110" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС10_110);
ИначеЕсли Ставка = "НДС20_120" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС20_120);
ИначеЕсли Ставка = "НДС0" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС0);
ИначеЕсли Ставка = "БезНДС" Тогда
Возврат Справочники.СтавкиНДС.БезНДС;
КонецЕсли;
Возврат ТекущееЗначение;
КонецФункции
Результат применения
Принцип направлен на создание кода, в котором высокоуровневые модули не зависят от конкретных реализаций низкого уровня, а опираются на абстракции, инвертируя направление зависимостей. Каждый метод или модуль следует проектировать так, чтобы детали реализации подстраивались под заданный интерфейс, а не наоборот. Это обеспечивает независимость компонентов, упрощает их замену и повышает гибкость системы, что особенно важно в динамично развивающихся конфигурациях 1С.
При следовании принципу код становится более адаптивным и устойчивым. Например, функции для работы с данными номенклатуры или складскими остатками зависят от абстрактных интерфейсов, а не от конкретных модулей или объектов метаданных. Это позволяет легко менять источник данных, например, переключаться с регистра на внешний API, без переписывания вызывающего кода. Поддержка упрощается, так как доработки и расширения подключаются через реализацию абстракций, не затрагивая основную логику. Тестирование становится эффективнее: использование mock-объектов для абстрактных интерфейсов изолирует модули, сокращая зависимости от реальных данных и ускоряя проверку поведения системы.
Как применять на практике
Весь этот материал, не просто теория, а руководство к действию, которое может изменить ваш взгляд на код. Начав мыслить категориями паттернов SOLID, вы увидите код по-новому, вместо набора процедур он превратится в систему взаимосвязанных компонентов, где каждая часть играет свою роль. Это как переключиться с хаотичного "пишу как получится" на структурированное "проектирую с целью". Используйте материал как чек-лист при проектировании:
- Анализируйте задачи с точки зрения ответственности: один модуль — одна цель.
- Проектируйте код с возможностью доработки через расширения или переопределение.
- Проверяйте, чтобы подмена типовой логики не ломала контракт с вызывающим кодом.
- Упрощайте интерфейсы, убирая из них всё лишнее для конкретного клиента.
- Внедряйте абстракции, чтобы разорвать жёсткие связи между слоями.
Примеры из статьи — это шаблоны: адаптируйте их под свои задачи. Постепенно внедряйте принципы в рабочие процессы, начиная с небольших доработок, чтобы увидеть эффект на практике.
Послесловие
В предисловии я говорил, что SOLID в 1С — это не просто модное слово, а способ укротить хаос кода, который так или иначе был знаком каждому из нас: переплетённые процедуры, бесконечные заплатки и доработки. Мы начинали с вопроса "Что это такое?" и применимы ли эти принципы к нашей платформе. Теперь, разобрав каждый из них, от разделения ответственности до инверсии зависимостей на примерах из 1С, я надеюсь, показал: SOLID не только применим в разработке 1С, но и может стать ориентиром для всех нас. Если после этой статьи у вас загорелись глаза или появился азарт применить SOLID в своих проектах, значит, мы вместе сделали шаг к более чистому и приятному коду. Спасибо, что прошли этот путь со мной, надеюсь, материал вдохновил вас!
Также хочется отметить, что статью я писал в одиночку, и наверняка она не лишена недостатков. Сейчас её просматривают тысячи новых людей, тысячи свежих взглядов и мнений, поэтому я, и, надеюсь, многие, будем благодарны за вашу вовлечённость и улучшение этого материала. Если у вас есть свои примеры или идеи, как можно было бы сделать лучше, не стесняйтесь принимать участие и делиться ими! До новых встреч!