Предисловие
Стоит завести разговор о SOLID среди 1С разработчиков, как в ответ, либо недоуменные взгляды, либо попытки найти хоть что-то в интернете, после которых вопросов становится только больше. Сегодня эти слова всe чаще звучат на собеседованиях, мелькают в требованиях компаний и в поисковых строках разработчиков. Материалов, которые бы ясно объясняли, как эти принципы работают в нашей платформе, почти нет. В лучшем случае попадаются общие материалы про объектно-ориентированное программирование, не учитывающие специфику платформы. В худшем, разрозненные публикации, оставляющие больше вопросов, чем ответов.
Интерес к этим принципам растeт, и не зря. Наверное, каждый сталкивался с кодом, который превратился в хаос. Методы, переплетeнные, как клубок, где правка одной строки, вызывает ошибку в другом модуль, влечет за собой часы отладки, и исправлений. Был похожий опыт? Или хоть раз приходили мысли: "Всe, пора искать новую работу, этот код меня доконает"?
Начинающие разработчики смотрят на такой наследственный беспорядок и теряются, как писать правильно, если вокруг нет ориентиров, а во всех классических пособиях для начинающих ни слова о чистоте кода. При этом те, кто заглядывал в типовые конфигурации, наверняка, тоже задавались вопросом, почему они такие "большие", но при этом так надeжно работают и быстро масштабируются, что им в этом помогает?
Эта статья для опытных разработчиков, которые ищут способ укротить сложные проекты и сделать их гибкими. Для новичков, стремящихся с самого начала заложить крепкую основу. Для тех, кто хочет углубиться и понять, какие принципы помогают сложным конфигурациям оставаться устойчивыми и масштабируемыми. Для всех нас, кто не любит заплатки в коде в духе "и так сойдет", и хочет понять, как принципы SOLID могут в этом помочь. Мы разберем каждый принцип SOLID на примерах 1С разработки, которые вы сможете взять за основу для своих задач.
Это про наше общее желание сделать код чище, а работу эффективнее и приятнее. И если после этих страниц у вас появится ощущение или даже легкий азарт, что SOLID не просто модное слово, а реально полезный инструмент на пути к порядку, значит, всe было не зря.
Возможно, целиком и сразу, объем информации покажется большим, для тех, кто впервые знакомится с ней. Для удобства я разделил каждый принцип на независимые блоки, которые можно читать отдельно друг от друга. Начнём.
Что такое SOLID
SOLID - это пять принципов объектно-ориентированного программирования, которые помогают создавать код, способный расти, меняться и оставаться понятным.
Вместе эти правила формируют подход, где структура программы становится не просто набором строк, а продуманной системой.
Зачем это нужно? Программирование, это не только написание кода, но и его развитие. Без чeтких принципов даже простая задача может превратиться в головоломку, когда требования меняются, а сроки поджимают. SOLID помогает решить эту проблему, давая инструкции для написания кода, который легко поддерживать, расширять и передавать другим. Это не абстрактная теория, а практическая основа, которая помогает избежать путаницы и сделать разработку предсказуемой.
Немного о контрактах
Далее я буду использовать слово "контракт", которое может оказаться непривычным для многих. Но это очень важная часть, которая лежит в основе качественного кода и эффективной командной работы.
Контракт - это формальное описание ожидаемого поведения метода, задающее его интерфейс и определяющее, как он взаимодействует с вызывающим кодом. Это позволяет использовать метод, опираясь на гарантированный результат, не зная его внутренней реализации.
Например, в приведенном примере метод ПодготовитьТекстЖурналаРегистрации находится в области ПрограммныйИнтерфейс и имеет четко описанную сигнатуру. Это задает контракт.
Вызывающий код ничего не знает о внутренней реализации метода, но, полагаясь на контракт, понимает, какие действия будут выполнены. В секции параметры описано какие параметры и в каком формате нужно передать, пояснения для каждого из них. В свою очередь, при соблюдении входящих условий контракта метод гарантирует его исполнение, вернув значение определенного типа в определенном формате, описанное в секции возвращаемое значение.
Методы в таких областях, как ПрограммныйИнтерфейс, СлужебныйПрограммныйИнтерфейс задают публичные контракты для других подсистем. Размещение методов в этих областях обязывает разработчиков не вносить в них изменения, которые приведут к изменению условий контракта, например добавление нового обязательного параметра. Лучше добавлять необязательные параметры, либо создавать новые методы, перенося старые в область УстаревшиеПроцедурыИФункции. При этом, если вы используете SonarQube или EDT, использование устаревших методов будет анализироваться.
Пример заданного контракта:
#Область ПрограммныйИнтерфейс
// Получает события журнала регистрации по отбору, выполняет проверку получения и передает результат
// в виде текста.
//
// Параметры:
// ПараметрыОтбора - Структура - структура с ключами:
// * ДатаНачала - Дата - начало периода журнала;
// * ДатаОкончания - Дата - конец периода журнала;
// * Событие - Массив - массив событий;
// * Метаданные - Массив, Неопределено - массив метаданных для отбора;
// * Уровень - УровеньЖурналаРегистрации - уровень важности событий журнала регистрации;
// ДанныеВложений - Массив Из Структура - подготовленные данные вложений:
// *Представление - Строка - представление вложения;
// *Текст - Строка - текст файла вложения;
// *Размер - Число - размер файла вложения в байтах.
//
// Возвращаемое значение:
// Структура - результат подготовки:
// *КодОшибки - Строка - идентификатор ошибки при отправки:
// *СообщениеОбОшибке - Строка, ФорматированнаяСтрока - сообщение об ошибке для пользователя.
//
Функция ПодготовитьТекстЖурналаРегистрации(ПараметрыОтбора, ДанныеВложений) Экспорт
// Логика метода, обеспечивающая исполнение контракта.
КонецФункции
#КонецОбласти
Теперь, вернемся к SOLID, начнeм с первого принципа и посмотрим, как одна простая мысль меняет всe.
Посмотрите на этот чайник, он стоит на столе, в нeм букет цветов, плавают рыбки. Звучит, как бред? А теперь представьте, что это ваш код, где один модуль или метод, рассчитывает зарплату, экспортирует файлы, настраивает пользовательский интерфейс. Примерно так выглядят многие проекты на 1С. Принцип единственной ответственности обещает порядок. Узнаем, как выгнать рыбок из чайника.
Описание
Первый принцип SOLID утверждает - программные сущности должны иметь только одну ответственность, чтобы их правка требовалась только по одной причине. Проще говоря, одна четко определенная задача. Это значит, что чайник должен только кипятить воду, а не быть также сразу в роли аквариума для рыб или вазы для цветов. В коде это работает так же, если модуль или метод отвечает за расчeт зарплаты, он не должен работать с файлами или настраивать пользовательский интерфейс.
Поскольку изменения в коде чаще всего инициируются пользователями, принцип единственной ответственности можно интерпретировать так, что каждая функциональность должна иметь одного владельца. Если модуль или метод приходится изменять одновременно по задачам, например, отдела продаж и отдела кадров, это признак нарушения принципа, так как у функциональности несколько причин для изменения.
Важно понимать, что одна ответственность, это не про одну строку кода и не исключительно про крошечные функции. Это про логическую цель, все действия должны быть неотъемлемой частью одной логической задачи.
Применение в 1С
На уровне архитектуры платформа 1С предоставляет следующие механизмы для реализации требований принципа.
- Каждый объект конфигурации имеет четко определенные роли и назначение.
- Различные типы модулей, где каждый отвечает за свою задачу, минимизируя смешивание обязанностей.
* Примечание. Библиотека стандартных подсистем содержит в себе функционал нижеприведенных методов. Пример призван продемонстрировать разделение ответственности.
Нарушение принципа:
Общий модуль "РаботаСЗаказами":
#Область ПрограммныйИнтерфейс
// Заполняет заказ клиента и отключает контроль записи.
// Рассчитывает сумму документа.
// Определяет авторизованного пользователя и устанавливает его автором в документе.
// Определяет склад по умолчанию и устанавливает его в документе.
// Отключает контроль записи.
//
// Параметры:
// ЗаказОбъект - ДокументОбъект.ЗаказКлиента - заказ, который необходимо обработать.
//
Процедура ОбработатьЗаказ(ЗаказОбъект) Экспорт
// 1. Рассчет суммы документа.
СуммаДокумента = ЗаказОбъект.Товары.Итог("Сумма");
Если Не ЗаказОбъект.ЦенаВключаетНДС Тогда
СуммаНДС = ЗаказОбъект.Товары.Итог("СуммаНДС");
СуммаДокумента = СуммаДокумента + СуммаНДС;
КонецЕсли;
ЗаказОбъект.СуммаДокумента = СуммаДокумента;
// 2. Определение текущего пользователя.
Если ЗначениеЗаполнено(ПараметрыСеанса.ТекущийПользователь) Тогда
АвторизованныйПользователь = ПараметрыСеанса.ТекущийПользователь;
Иначе
АвторизованныйПользователь = ПараметрыСеанса.ТекущийВнешнийПользователь;
КонецЕсли;
ЗаказОбъект.Автор = АвторизованныйПользователь;
// 3. Определение склада по умолчанию.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 2
| Таблица.Ссылка КАК Склад
|ИЗ
| Справочник.Склады КАК Таблица
|ГДЕ
| НЕ Таблица.ПометкаУдаления
| И НЕ Таблица.ЭтоГруппа";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() И Выборка.Количество() = 1 Тогда
Склад = Выборка.Склад;
Иначе
Склад = Справочники.Склады.ПустаяСсылка();
КонецЕсли;
ЗаказОбъект.Склад = Склад;
// 4. Отключение контроля записи.
ЗаказОбъект.ОбменДанными.Загрузка = Истина;
ЗаказОбъект.ОбменДанными.Получатели.АвтоЗаполнение = Ложь;
ЗаказОбъект.ДополнительныеСвойства.Вставить("ОтключитьМеханизмРегистрацииОбъектов");
КонецПроцедуры
#КонецОбласти
Что плохого в этом примере:
Метод выполняет сразу несколько разнородных задач, рассчитывает сумму документа, определяет авторизованного пользователя, определяет склад по умолчанию, отключет контроль записи. Это нарушает принцип, т.к. у метода более одной причины для изменения. Из-за смешивания разной логики в одном месте, код становится трудно читать и тестировать, например, расчет суммы документа невозможно протестировать отдельно, без учета остальных действий.
Кроме того, логика метода содержит в себе действия, которые потенциально применимы к большинству объектов. Расчет суммы документа подходит для большинства документов, определение авторизованного пользователя, организации и отключение контроля записи тоже содержат общую логику. Это означает, что есть высокая вероятность, что эту логику кто-то повторит в другом месте, что влечет за собой множество побочных эффектов.
Следование принципу:
Следуя принципу, разделим обязанности между модулями и методами.
ОбщийМодуль "Ценообразование":
#Область ПрограммныйИнтерфейс
// Возвращает сумму документа с учетом НДС.
//
// Параметры:
// ТабличнаяЧасть - ТабличнаяЧасть - табличная часть, содержащая колонки:
// *Сумма - Число.
// *СуммаНДС = Число.
// ЦенаВключаетНДС - Булево - признак включения НДС в цену документа.
//
// Возвращаемое значение:
// Число - Сумма документа с учетом НДС.
//
Функция СуммаДокумента(ТабличнаяЧасть, ЦенаВключаетНДС) Экспорт
Результат = ТабличнаяЧасть.Итог("Сумма");
Если Не ЦенаВключаетНДС Тогда
СуммаНДС = ТабличнаяЧасть.Итог("СуммаНДС");
Результат = Результат + СуммаНДС;
КонецЕсли;
Возврат Результат;
КонецФункции
#КонецОбласти
ОбщийМодуль "Пользователи":
#Область ПрограммныйИнтерфейс
// Возвращает текущего пользователя или текущего внешнего пользователя,
// в зависимости от того, кто выполнил вход в сеанс.
// Рекомендуется использовать в коде, который поддерживает работу в обоих случаях.
//
// Возвращаемое значение:
// СправочникСсылка.Пользователи, СправочникСсылка.ВнешниеПользователи - пользователь
// или внешний пользователь.
//
Функция АвторизованныйПользователь() Экспорт
Если ЗначениеЗаполнено(ПараметрыСеанса.ТекущийПользователь) Тогда
Результат = ПараметрыСеанса.ТекущийПользователь;
Иначе
Результат = ПараметрыСеанса.ТекущийВнешнийПользователь;
КонецЕсли;
Возврат Результат;
КонецФункции
#КонецОбласти
ОбщийМодуль "ЗначенияПоУмолчаниюПовтИсп":
#Область ПрограммныйИнтерфейс
// Возвращает склад, если он один в информационной базе.
// Если склад в базе не один - возвращает пустую ссылку на склад.
//
// Возвращаемое значение:
// СправочникСсылка.Склады - склад по умолчанию.
//
Функция СкладПоУмолчанию() Экспорт
Результат = Справочники.Склады.СкладПоУмолчанию();
Возврат Результат;
КонецФункции
#КонецОбласти
Модуль менеджера справочник "Склады":
#Если Не МобильныйАвтономныйСервер Тогда
#Если Сервер Или ТолстыйКлиентОбычноеПриложение Или ВнешнееСоединение Тогда
#Область ПрограммныйИнтерфейс
// Возвращает склад, если он один в информационной базе.
// Если склад в базе не один - возвращает пустую ссылку на склад.
//
// Возвращаемое значение:
// СправочникСсылка.Склады - склад по умолчанию.
//
Функция СкладПоУмолчанию() Экспорт
Результат = Справочники.Склады.ПустаяСсылка();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 2
| Таблица.Ссылка КАК Склад
|ИЗ
| Справочник.Склады КАК Таблица
|ГДЕ
| НЕ Таблица.ПометкаУдаления
| И НЕ Таблица.ЭтоГруппа";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() И Выборка.Количество() = 1 Тогда
Результат = Выборка.Склад;
КонецЕсли;
Возврат Результат;
КонецФункции
#КонецОбласти
#КонецЕсли
#КонецЕсли
Общий модуль "ПроведениеДокументов":
#Область ПрограммныйИнтерфейс
// Включает режим загрузки данных, отключает регистрацию объекта на планах обмена.
//
// Параметры:
// ДокументОбъект - ДокументОбъект - объект документа, для которого требуется отключить контроль записи.
//
Процедура ОтключитьКонтрольЗаписи(ДокументОбъект) Экспорт
ДокументОбъект.ОбменДанными.Загрузка = Истина;
ДокументОбъект.ОбменДанными.Получатели.АвтоЗаполнение = Ложь;
ДокументОбъект.ДополнительныеСвойства.Вставить("ОтключитьМеханизмРегистрацииОбъектов");
КонецПроцедуры
#КонецОбласти
Что решилось после исправления:
Каждое действие вынесено в отдельные узкоспециализированные модули и методы с одной причиной для изменения. Уменьшена высокая связанность, т.к. методы изолированы и ничего не знают о существовании друг друга. Каждый метод можно протестировать отдельно, что упрощает unit-тесты.
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].Выбрать();
Выборка.Следующий();
Иначе
Возврат Неопределено;
КонецЕсли;
Иначе
Возврат Неопределено;
КонецЕсли;
СтруктураРеквизитов = ШаблонУсловияЗакупок();
ЗаполнитьЗначенияСвойств(СтруктураРеквизитов, Выборка);
Возврат СтруктураРеквизитов;
КонецФункции
Результат применения
При следовании принципу уменьшается связанность кода, он становится легко читаемым и понятным, его легче модифицировать, расширять и тестировать. Изменения в одной части системы не приводят к неожиданным сбоям в других. Тестирование также упрощается за счeт изоляции функциональности.
Представьте себе утро в большом городе. Вы садитесь в машину, чтобы добраться до работы, но вместо привычного ритма главной магистрали вас встречает хаос, дорога перекопана, развязки превратились в лабиринт, машины сигналят, а пробка тянется до горизонта. Что пошло не так? Казалось бы, хотели улучшить движение, добавить новые пути, но вместо этого старые маршруты стали непроходимыми, а новые бесполезными. Почему простое изменение привело к такому коллапсу, и главное, можно ли было этого избежать?
А теперь представьте другую картину, та же магистраль, но уже с новыми развязками, которые не ломают привычный поток. Машины едут плавно, пробок нет, а дорога остаeтся такой же надeжной, как раньше. В чeм секрет? Ответ кроется в одном простом принципе, который мы, часто упускаем из виду. Разберем дальше, как принцип открытости/закрытости может спасти разработки от "дорожного хаоса" и сделать код гибким, как идеальная магистраль.
Описание
Второй принцип SOLID утверждает - программные компоненты, должны позволять добавлять новую функциональность без изменения исходого кода, т.е. быть открыты для расширения, но закрыты для модификации. Представьте, что ваш код, это главная магистраль, вы можете строить новые развязки, не перекапывая саму дорогу. Это значит, что новая функциональность добавляется без изменения исходного кода, и старый, проверенный код, остаeтся нетронутым. В итоге система становится гибкой, как сеть дорог, где новые маршруты не ломают старые пути.
Применение в 1С
На уровне архитектуры платформа 1С предоставляет следующие механизмы для реализации требований принципа.
- Переопределяемые модули. Дают возможность расширить логику базового метода, без изменения его исходного кода.
- Расширения конфигурации. Выносят новую функциональность в "отдельный слой", базовые код и объекты конфигурации остаются нетронутыми.
- Подписки на события. Механизм для подключения дополнительной логики, без изменений в базовом объекте.
- Общие модули с настройками в пользовательском режиме. Вместо правки кода под новые требования логика выносится в настройки. Скажем, правила расчeта скидок задаются в справочнике или регистре сведений, а общий модуль просто использует эти данные, код остаeтся неизменным.
- Внешние отчeты и обработки. Для нового отчета или обработки создаeтся внешний файл и подключается "дополнительно" к конфигурации. Добавляется новая функциональность, основная конфигурация не меняется.
- Область "УстаревшиеПроцедурыИФункции". Вместо изменения старых методов, добавляются новые. Старые помещаются в специальную область, с пометкой, что они устарели и следует использовать новые.
- Свойство "ЗаполнятьИзДанныхЗаполнения" на уровне метаданных, позволяет расширить логику обработчика события "ОбработкаЗаполнения" без его непосредственной модификации.
Нарушение принципа:
Общий модуль "РасчетСкидок":
#Область ПрограммныйИнтерфейс
// Возвращает размер скидки в процентах для указанного вида клиента.
//
// Параметры:
// ВидКлиента - СправочникСсылка.ВидыКлиентов - вид клиента, для которого требуется получить размер скидки.
//
// Возвращаемое значение:
// Число - размер скидки в процентах.
//
Функция РазмерСкидкиПоВидуКлиента(ВидКлиента) Экспорт
Результат = 0;
МенеджерВидовКлиентов = МенеджерВидовКлиентов();
СкидкиПоВидамКлиентов = Новый Соответствие;
СкидкиПоВидамКлиентов.Вставить(МенеджерВидовКлиентов.Обычный, 5);
СкидкиПоВидамКлиентов.Вставить(МенеджерВидовКлиентов.VIP, 15);
СкидкиПоВидамКлиентов.Вставить(МенеджерВидовКлиентов.Оптовый, 20); // новое требование
СкидкиПоВидамКлиентов.Вставить(МенеджерВидовКлиентов.Партнер, 10); // новое требование
РазмерСкидки = СкидкиПоВидамКлиентов[ВидКлиента];
Если РазмерСкидки <> Неопределено Тогда
Результат = РазмерСкидки;
КонецЕсли;
Возврат Результат;
КонецФункции
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция МенеджерВидовКлиентов()
Результат = Справочники.ВидыКлиентов;
Возврат Результат;
КонецФункции
#КонецОбласти
Что плохого в этом примере:
В данном случае, каждый раз, когда появляется новый вид клиента или скидки, разработчик вынужден модифицировать этот метод и добавлять новые значения в соответствие СкидкиПоВидамКлиентов. Это ломает "закрытость" метода.
Соблюдение принципа:
Перепишем этот код с учетом требований принципа двумя способами, используя подходы характерные для 1С.
Вариант 1
Добавим регистр сведений СкидкиПоВидамКлиентов с измерением ВидКлиента и ресурсом РазмерСкидки, для хранения настроек скидок. Реализуем в нем метод получения процента скидки и задействуем его в общем модуле РасчетСкидок.
Общий модуль "РасчетСкидок":
#Область ПрограммныйИнтерфейс
// Возвращает размер скидки в процентах для указанного вида клиента.
//
// Параметры:
// ВидКлиента - СправочникСсылка.ВидыКлиентов - вид клиента, для которого требуется получить размер скидки.
//
// Возвращаемое значение:
// Число - размер скидки в процентах.
//
Функция ПроцентСкидкиПоВидуКлиента(ВидКлиента) Экспорт
Результат = РегистрыСведений.СкидкиПоВидамКлиентов.ПроцентСкидкиПоВидуКлиента(ВидКлиента);
Возврат Результат;
КонецФункции
#КонецОбласти
Модуль менеджера регистра "СкидкиПоВидамКлиентов":
#Если Сервер Или ТолстыйКлиентОбычноеПриложение Или ВнешнееСоединение Тогда
#Область ПрограммныйИнтерфейс
// Возвращает размер скидки в процентах для указанного вида клиента.
//
// Параметры:
// ВидКлиента - СправочникСсылка.ВидыКлиентов - вид клиента, для которого требуется получить размер скидки.
//
// Возвращаемое значение:
// Число - размер скидки в процентах.
//
Функция ПроцентСкидкиПоВидуКлиента(ВидКлиента) Экспорт
Результат = 0;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СкидкиПоВидамКлиентов.ПроцентСкидки КАК ПроцентСкидки
|ИЗ
| РегистрСведений.СкидкиПоВидамКлиентов КАК СкидкиПоВидамКлиентов
|ГДЕ
| СкидкиПоВидамКлиентов.ВидКлиента = &ВидКлиента";
Запрос.УстановитьПараметр("ВидКлиента", ВидКлиента);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() Тогда
Результат = Выборка.ПроцентСкидки;
КонецЕсли;
Возврат Результат;
КонецФункции
#КонецОбласти
#КонецЕсли
Что решилось после исправления:
Теперь, что добавить новую скидку, допустим, появился вид клиента "Сотрудник" с 25% скидкой, добавляем запись в регистр сведений СкидкиПоВидамКлиентов в пользовательском режиме. Код метода ПроцентСкидкиПоВидуКлиента общего модуля и модуля менеджера регистра сведений, остаeтся нетронутым. Таким образом, функционал методов расширяется без модификации исходного кода.
Вариант 2
Реализуем требования принципа при помощи переопределяемого модуля.
Общий модуль "РасчетСкидок":
#Область ПрограммныйИнтерфейс
// Возвращает размер скидки в процентах для указанного вида клиента.
//
// Параметры:
// ВидКлиента - СправочникСсылка.ВидыКлиентов - вид клиента, для которого требуется получить размер скидки.
//
// Возвращаемое значение:
// Число - размер скидки в процентах.
//
Функция ПроцентСкидкиПоВидуКлиента(ВидКлиента) Экспорт
Результат = 0;
СкидкиПоВидамКлиентов = Новый Соответствие;
// Точка расширения функциональности.
РасчетСкидокПереопределяемый.ЗаполнитьСкидкиПоВидамКлиентов(СкидкиПоВидамКлиентов);
РазмерСкидки = СкидкиПоВидамКлиентов[ВидКлиента];
Если РазмерСкидки <> Неопределено Тогда
Результат = РазмерСкидки;
КонецЕсли;
Возврат Результат;
КонецФункции
#КонецОбласти
Общий модуль "РасчетСкидокПереопределяемый":
#Область ПрограммныйИнтерфейс
// Заполняет скидки по видам клиентов.
//
// Параметры:
// СкидкиПоВидамКлиентов - Соответствие - соответствие для сопоставления процентов скидок видам клиентов.
//
Процедура ЗаполнитьСкидкиПоВидамКлиентов(СкидкиПоВидамКлиентов) Экспорт
МенеджерВидовКлиентов = МенеджерВидовКлиентов();
СкидкиПоВидамКлиентов.Вставить(МенеджерВидовКлиентов.Обычный, 5);
СкидкиПоВидамКлиентов.Вставить(МенеджерВидовКлиентов.VIP, 15);
СкидкиПоВидамКлиентов.Вставить(МенеджерВидовКлиентов.Оптовый, 20); // новое требование
СкидкиПоВидамКлиентов.Вставить(МенеджерВидовКлиентов.Партнер, 10); // новое требование
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция МенеджерВидовКлиентов()
Результат = Справочники.ВидыКлиентов;
Возврат Результат;
КонецФункции
#КонецОбласти
Что решилось после исправления:
Для добавления новой скидки, мы не модифицируем базовый метод ПроцентСкидкиПоВидуКлиента в модуле РасчетСкидок, задающий контракт. Вместо этого мы дополняем значениям в методе ЗаполнитьСкидкиПоВидамКлиентов в переопределяемом методе. Таким образом метод закрыт от изменений, но открыт для расширения.
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);
Возврат ШаблонСтроки;
КонецФункции
Результат применения
При следовании принципу код становится более устойчивым и масштабируемым. Новые требования реализуются, не затрагивая базовую функциональность. Это минимизирует конфликты, ускоряет внедрение изменений и позволяет разработчикам сосредоточиться на новых задачах, а не на исправлении побочных эффектов. Тестирование тоже упрощается, так как старый код остаeтся неизменным и проверенным, а новые доработки изолированы.
Взгляните на эти электронные смарт-часы, вы ждeте, что они покажут время, но вместо этого на экране таблица химических элементов. Неожиданно? Такое же часто происходит и в программном коде, вы вызываете метод, рассчитывая на привычное поведение, а он выдаeт неожиданный результат, который рушит всю логику. Откуда берeтся эта ловушка? Всe дело в принципе подстановки Барбары Лисков. Давайте разберeмся, как вернуть часам их суть, а коду надeжность.
Описание
Третий принцип SOLID утверждает - объекты или модули, заменяющие друг друга, должны работать так, чтобы их подмена не нарушала поведение системы. Это значит, что любая подмена, через наследование, расширение или переопределение, обязана сохранять ожидаемое поведение базовой версии. Если модуль подменяет типовую логику, он не должен изменять еe суть. В иллюстрации со смарт-часами, они - наследники обычных часов, и должны "наследовать" поведение их предков, т.е. показывать время.
Применение в 1С
На уровне архитектуры платформа 1С предоставляет следующие механизмы для реализации требований принципа.
- Расширения конфигурации. Позволяют переопределять поведение типовых объектов, сохраняя их интерфейс и контракт. Например, если метод типового модуля возвращает результат в определeнном формате, расширение должно соблюдать тот же формат.
- Подписки на события. Используются для подмены или дополнения логики без изменения исходного объекта. Подписка должна "уважать" ожидания базового события, чтобы не нарушить целостность системы.
- Переопределяемые модули. Общие модули с суффиксом "Переопределяемый" предназначены для подмены типовой логики. Их реализация обязана сохранять контракт базового модуля.
- Работа с общими типами. Методы, которые одинаково работают со всеми подтипами какого-то общего типа, например с любым подтипом от "ДокументСсылка" или "СправочниСсылка".
В данном примере, общий модуль "ЗапасыПереопределяемый" и его метод "ПодбиратьВидыЗапасовПоИнтеркампани" являются наследниками базового модуля "Запасы" и его метода "ПодбиратьВидыЗапасовПоИнтеркампани". Т.е. наследуют его контракт, согласно которому, базовый метод возвращает признак необходимости подбора вида запасов по интеркампани, на вход принимая любой подтип от "ДокументОбъект" и возвращая значение типа "булево". Вызывающий код ожидает одинаковое поведение для любого подтипа, т.е. возврат значения типа "булево".
Нарушение принципа:
Общий модуль "Запасы":
#Область ПрограммныйИнтерфейс
// Возвращает признак необходимости подбора вида запасов по интеркампани.
//
// Параметры:
// Документ - ДокументОбъект - записываемый документ.
//
// Возвращаемое значение:
// Булево - Истина, если необходимо подбирать виды запасов по интеркампани.
//
Функция ПодбиратьВидыЗапасовПоИнтеркампани(Документ) Экспорт
Результат = ЗапасыПереопределяемый.ПодбиратьВидыЗапасовПоИнтеркампани(Документ);
Возврат Результат;
КонецФункции
#КонецОбласти
Общий модуль "ЗапасыПереопределяемый":
#Область ПрограммныйИнтерфейс
// Возвращает признак необходимости подбора вида запасов по интеркампани.
//
// Параметры:
// Документ - ДокументОбъект - записываемый документ.
//
// Возвращаемое значение:
// Булево - Истина, если необходимо подбирать виды запасов по интеркампани.
//
Функция ПодбиратьВидыЗапасовПоИнтеркампани(Документ) Экспорт
Если ТипЗнч(Документ) = Тип("ДокументОбъект.РеализацияТоваровУслуг") Тогда
Результат = Документ.ПодбиратьВидыЗапасовПоИнтеркампани;
Иначе
Результат = Документ.ДополнительныеСвойства.ПодбиратьВидыЗапасовПоИнтеркампани;
КонецЕсли;
Возврат Результат;
КонецФункции
#КонецОбласти
Что плохого в этом примере:
Ожидая сохранение контракта базового метода, и одинаковое поведения для любого подтипа от "ДокументОбъект", по факту, переопределяемый метод выполняет разную логику, опираясь на конкретный тип. Для подтипа "ДокументОбъект.РеализацияТоваровУслуг", значение берется напрямую из реквизита, для всех остальных подтипов, из дополнительных свойств. В таком случае полученное значение для вызывающего кода может быть оказаться неожиданным, например, в реквизите "ПодбиратьВидыЗапасовПоИнтеркампани" документа "РеализацияТоваровУслуг" будет установлено значение "ложь", но при этом в дополнительных свойствах будет значение "истина", а вызывающий код может ожидать значение именно из дополнительных свойств, как происходит для большинства документов.
Далее, в ветке иначе, метод обращается напряму к свойству "ДополнительныеСвойства" объекта документа, что допустимо, т.к. это свойство действительно есть у любого подтипа от "ДокументОбъект", этот контракт выполняется на уровне платформы.
Но дальше, происходит обращение к ключу "ПодбиратьВидыЗапасовПоИнтеркампани" структуры "ДополнительныеСвойства", и вот здесь уже возникает проблема, т.к. в контракте нет явного указания, что входящий параметр с типом "ДокументОбъект" должен содержать в дополнительных свойствах такой ключ. Вместо "обещанного" возвращаемого значения с типом "булево" возникнет ошибка.
Принцип нарушен, т.к. метод не работает одинаково для любого подтипа от "ДокументОбъект", и не сохраняет контракт, наследуемый от базового метода, вместо ожидаемого значения "булево" возникает ошибка.
Соблюдение принципа:
Общий модуль "ЗапасыПереопределяемый":
#Область ПрограммныйИнтерфейс
// Возвращает признак необходимости подбора вида запасов по интеркампани.
//
// Параметры:
// Документ - ДокументОбъект - записываемый документ.
//
// Возвращаемое значение:
// Булево - Истина, если необходимо подбирать виды запасов по интеркампани.
//
Функция ПодбиратьВидыЗапасовПоИнтеркампани(Документ) Экспорт
Результат = ОбщегоНазначенияКлиентСервер.СвойствоСтруктуры(Документ.ДополнительныеСвойства,
"ПодбиратьВидыЗапасовПоИнтеркампани", Ложь);
Возврат Результат;
КонецФункции
#КонецОбласти
Что решилось после исправления:
Теперь метод работает одинаково с любым подтипом от "ДокументОбъект", логика не зависит от конкретного типа. Сохранение контракта базового метода гарантируется за счет использования метода БСП "ОбщегоНазначенияКлиентСервер.СвойствоСтруктуры", который позволяет "безопасно" получить значение ключа структуры и также указать возвращаемое значение по умолчанию, если ключа нет.
Таким образом, метод вернет значение типа "булево" для любого подтипа, независимо от того, есть у него в дополнительных свойствах ключ "ПодбиратьВидыЗапасовПоИнтеркампани" или нет.
1. ДенежныеСредства.ПроверитьВалютуКонвертации()
Неявный контракт, в описании указано "Текущий документ", но не уточняется, что процедура применима только к документам с определeнной структурой. Это вводит в заблуждение о еe универсальности. Код не проверяет наличие реквизитов перед обращением, полагаясь на их обязательное присутствие.
// Процедура проверяет валюту конвертации, указанную в документе.
//
// Параметры:
// ДокументОбъект - ДокументОбъект - Текущий документ
// Отказ - Булево - Признак отказа от продолжения работы
// ФлагОбменСБанками - Булево - Признак использования при обмене с банками
// ОшибкиЗаполнения - Строка - Строка, накапливающая ошибки проверок.
//
Процедура ПроверитьВалютуКонвертации(ДокументОбъект, Отказ, ФлагОбменСБанками = Ложь, ОшибкиЗаполнения = "") Экспорт
Если ДокументОбъект.ХозяйственнаяОперация = Перечисления.ХозяйственныеОперации.КонвертацияВалюты
И ЗначениеЗаполнено(ДокументОбъект.Валюта)
И ЗначениеЗаполнено(ДокументОбъект.ВалютаКонвертации)
Тогда
Если ДокументОбъект.Валюта = ДокументОбъект.ВалютаКонвертации Тогда
Текст = НСтр("ru = 'Валюта конвертации должна отличаться от валюты документа'");
Если ФлагОбменСБанками Тогда
ДобавитьОшибкуЗаполнения(ОшибкиЗаполнения, Текст);
Иначе
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
Текст,
ДокументОбъект,
"ВалютаКонвертации",
,
Отказ);
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
2. ЦенообразованиеКлиентСервер.ПолучитьСуммуДокумента()
Контракт не выдержан, в описании функции указано, что она работает с типом ТабличнаяЧасть, но не уточняется, что требуются колонки Сумма и СуммаНДС. Это вводит в заблуждение: ожидается универсальность, но фактически функция работает только с табличными частями определeнной структуры.
// Возвращает сумму документа с учетом НДС
//
// Параметры:
// Товары - ТабличнаяЧасть - табличная часть документа для подсчета суммы документа.
// ЦенаВключаетНДС - Булево - признак включения НДС в цену документа.
//
// Возвращаемое значение:
// Число - Сумма документа с учетом НДС/.
//
Функция ПолучитьСуммуДокумента(Товары, Знач ЦенаВключаетНДС) Экспорт
СуммаДокумента = Товары.Итог("Сумма");
Если Не ЦенаВключаетНДС Тогда
СуммаДокумента = СуммаДокумента + Товары.Итог("СуммаНДС");
КонецЕсли;
Возврат СуммаДокумента;
КонецФункции // ПолучитьСуммуДокумента()
3. ПроведениеДокументов.ЕстьЗаписиВТаблице()
Метод предполагает, что у Документ.ДополнительныеСвойства есть вложенная структура ПроведениеДокументов.ТаблицыКонтроля, где хранится свойство ЕстьЗаписиВТаблице. Однако ДокументОбъект — это базовый тип для всех документов, и не все документы обязаны иметь такую структуру в ДополнительныеСвойства. Если подставить объект документа, у которого нет этой структуры, метод вызовет ошибку обращения к несуществующему свойству. Это нарушает принцип, так как при соблюдении контракта, подстановке допустимого объекта базового типа ДокументОбъект это приведет к сбою.
// Проверяет есть ли записи по указанной таблице контроля.
//
// Параметры:
// Документ - ДокументОбъект - записываемый документ
// ИмяТаблицы - Строка - имя таблицы контроля.
//
// Возвращаемое значение:
// Булево - Истина, если есть изменения по указанной таблице контроля.
//
Функция ЕстьЗаписиВТаблице(Документ, ИмяТаблицы) Экспорт
ТаблицыКонтроля = Документ.ДополнительныеСвойства.ПроведениеДокументов.ТаблицыКонтроля;
Возврат ТаблицыКонтроля.Свойство(ИмяТаблицы) И ТаблицыКонтроля[ИмяТаблицы].ЕстьЗаписиВТаблице;
КонецФункции
Результат применения
При следовании принципу код становится более надeжным и универсальным, исключаются сбои и непредсказуемые результаты из-за специфичных условий. Например, методы, работающие с общими типами, такими как "ДокументСсылка", корректно обрабатывают любые документы без необходимости учитывать их специфику. Это позволяет избежать ошибок при добавлении новых документов или изменении структуры существующих, так как базовый контракт остаeтся неизменным. Не приходится переписывать код для учeта особенностей подтипов. Тестирование тоже становится эффективнее, проверка поведения базового типа гарантирует работу с любыми подтипами, снижая объeм дополнительных тестов для каждого нового подтипа.
Представьте себе MP3-плеер с монолитными кнопками, на них значки для включения, паузы, переключения треков, громкости, ещe какие-то странные иконки вроде долларов, стрелок, непонятных символов. Вы хотите просто включить музыку, но вместо этого плеер начинает сообщать погоду, курс валют. Удобно? Вряд ли. Так же и в коде, при обращениии к модулю, ожидая в нем только нужные функции, он загроможден лишним функционалом, или методы которые тащат за собой кучу ненужных или неподходящих возможностей. Этого помогает избежать принцип разделения интерфейсов. Рассмотрим, как сделать плеер и код проще и понятнее.
Описание
Четвeртый принцип SOLID утверждает - клиенты интерфейсов должны взаимодействовать только с минимально необходимыми интерфейсами. Клиент не должен подключаться к громоздкому интерфейсу с кучей ненужных методов или к методу с излишним функционалом, и тащить этот балласт, усложняя логику и создавая путаницу. Принцип разделения интерфейса учит разбивать такие интерфейсы на простые и понятные части, чтобы клиенты использовали только то, что действительно нужно.
Применение в 1С
На уровне архитектуры платформа 1С предоставляет следующие механизмы для реализации требований принципа.
- Общие модули с чeтким разделением функций, узкой специализацией, например, отдельные модули для работы с номенклатурой, ценами или проведением документов. Это позволяет вызывающему коду подключать только необходимую функциональность, а не весь набор методов конфигурации. В свою очередь, внутри модулей, методы также делятся по областям, предоставляя публичный интерфейс через экспортируемые методы и внутренние служебные неэкспортируемые методы, которые вызывающий код не видит.
- Параметры функциональности. Использование параметров сеанса или констант, или подсистем БСП, для включения/отключения определeнных возможностей конфигурации, позволяет модулям работать только с актуальными функциями, исключая ненужные зависимости от неиспользуемых участков кода.
- Интерфейсы в расширениях. Расширения конфигурации позволяют добавлять новую логику, реализуя только необходимые методы, без необходимости подключать весь интерфейс объекта конфигурации.
Нарушение принципа:
Общий модуль РаботаСКонтрагентами:
#Область ПрограммныйИнтерфейс
// Возвращает сведения о контрагенте.
//
// Параметры:
// Контрагент - СправочникСсылка.Контрагенты - контрагент, для которого требуется получить сведения.
// ВключатьЗаказыКОтгрузке - Булево - если признак установлен, добавляет в сведения заказы к отгрузке.
// ОповещатьОПросроченнойЗадолженности - Булево - если признак установлен, будет выполнена рассылка оповещений
// контрагенту о просроченной задолженности.
//
// Возвращаемое значение:
// Структура:
// * НаименованиеПолное - Строка
// * НаименованиеМеждународное - Строка
// * ИНН - Строка
// * КПП - Строка
// * ЗаказыКОтгрузке - Массив - наличие ключа определяется параметром ВключатьЗаказыКОтгрузке
//
Функция ОсновныеСведенияОКонтрагенте(Контрагент, ВключатьЗаказыКОтгрузке = Ложь,
ОповещатьОПросроченнойЗадолженности = Ложь) Экспорт
// Основная функциональность.
Результат = ОписаниеСведенийОКонтрагенте();
ДанныеКонтрагента = ДанныеКонтрагента(Контрагент);
ЗаполнитьЗначенияСвойств(Результат, ДанныеКонтрагента);
// Навязанная фунциональность.
Если ВключатьЗаказыКОтгрузке Тогда
Статус = СтатусЗаказаКОтгрузке();
ЗаказыКОтгрузке = ЗаказыПоСтатусу(Контрагент, Статус);
Результат.Вставить("ЗаказыКОтгрузке", ЗаказыКОтгрузке);
КонецЕсли;
// Навязанная фунциональность.
Если ОповещатьОПросроченнойЗадолженности Тогда
ТипСобытия = ТипСобытияОповещенияПросроченнаяЗадолженность();
ВыполнитьРассылкуОповещенийПоТипу(Контрагент, ТипСобытия);
КонецЕсли;
Возврат Результат;
КонецФункции
// Возвращает заказы контрагента в указанном статусе.
//
// Параметры:
// Контрагент - СправочникСсылка.Контрагенты - контрагент, для которого требуется получить заказы.
// Статус - ПеречислениеСсылка.СтатусыЗаказовКлиентов - статус заказов.
//
// Возвращаемое значение:
// Массив из ДокументСсылка.ЗаказКлиента
//
Функция ЗаказыПоСтатусу(Контрагент, Статус) Экспорт // Навязанная функциональность интерфейса.
Результат = РегистрыСведений.ЗаказыКлиентов.ЗаказыПоСтатусу(Контрагент, Статус);
Возврат Результат;
КонецФункции
// Выполняет рассылку оповещений контрагенту из очереди по указанному типу события.
//
// Параметры:
// Контрагент - СправочникСсылка.Контрагенты - контрагент, для которого требуется выполнить рассылку.
// ТипСобытия - ПеречислениеСсылка.ТипыСобытийОповещений - тип события.
//
Процедура ВыполнитьРассылкуОповещенийПоТипу(Контрагент, ТипСобытия) Экспорт // Навязанная функциональность интерфейса.
// Логика выполнения рассылки.
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция ДанныеКонтрагента(Контрагент)
Результат = Справочники.Контрагенты.ОсновныеСведенияОКонтрагенте(Контрагент);
Возврат Результат;
КонецФункции
Функция ОписаниеСведенийОКонтрагенте()
Результат = Новый Структура;
Результат.Вставить("НаименованиеПолное", "");
Результат.Вставить("НаименованиеМеждународное", "");
Результат.Вставить("ИНН", "");
Результат.Вставить("КПП", "");
Возврат Результат;
КонецФункции
Функция СтатусЗаказаКОтгрузке()
Результат = Перечисления.СтатусыЗаказовКлиентов.КОтгрузке;
Возврат Результат;
КонецФункции
Функция ТипСобытияОповещенияПросроченнаяЗадолженность()
Результат = Перечисления.ТипыСобытийОповещений.ПросроченнаяЗадолженность;
Возврат Результат;
КонецФункции
#КонецОбласти
Что плохого в этом примере:
Интерфейс модуля РаботаСКонтрагентами перегружен. Он содержит три экспортируемых метода, ОсновныеСведенияОКонтрагенте, ЗаказыПоСтатусу и ВыполнитьРассылкуОповещенийПоТипу. Это смешивает функции для работы с данными клиентов, заказами и рассылкой оповещений, навязывая клиентам интерфейса лишнюю функциональность.
Интерфейс метода ОсновныеСведенияОКонтрагенте также перегружен, он предоставляет избыточный функционал, чем нужно большинству клиентов. Параметр ВключатьЗаказыКОтгрузке добавляет заказы, которые нужны только для специфичных случаев, но не для базового получения сведений о котрагенте. Параметр ОповещатьОПросроченнойЗадолженности выполняет рассылку оповещения контрагенту, что не относится к задаче получения данных и навязывает побочный эффект.
Вызывающий код, которому нужны только основные сведения о контрагенте, вынужден учитывать эти параметры и их логику, даже если они не используются, что усложняет использование интерфейса. Тестирование также усложняется, нужно проверять все варианты параметров, даже если клиент использует метод минимально.
Соблюдение принципа:
Общий модуль "РаботаСКонтрагентами":
#Область ПрограммныйИнтерфейс
// Возвращает сведения о контрагенте.
//
// Параметры:
// Контрагент - СправочникСсылка.Контрагенты - контрагент, для которого требуется получить сведения.
//
// Возвращаемое значение:
// Структура - см. ОписаниеСведенийОКонтрагенте
//
Функция ОсновныеСведенияОКонтрагенте(Контрагент) Экспорт
Результат = ОписаниеСведенийОКонтрагенте();
ДанныеКонтрагента = ДанныеКонтрагента(Контрагент);
ЗаполнитьЗначенияСвойств(Результат, ДанныеКонтрагента);
Возврат Результат;
КонецФункции
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция ДанныеКонтрагента(Контрагент)
Результат = Справочники.Контрагенты.ОсновныеСведенияОКонтрагенте(Контрагент);
Возврат Результат;
КонецФункции
Функция ОписаниеСведенийОКонтрагенте()
Результат = Новый Структура;
Результат.Вставить("НаименованиеПолное", "");
Результат.Вставить("НаименованиеМеждународное", "");
Результат.Вставить("ИНН", "");
Результат.Вставить("КПП", "");
Возврат Результат;
КонецФункции
#КонецОбласти
Общий модуль "РаботаСЗаказами":
#Область ПрограммныйИнтерфейс
// Возвращает заказы контрагента в указанном статусе.
//
// Параметры:
// Контрагент - СправочникСсылка.Контрагенты - контрагент, для которого требуется получить заказы.
// Статус - ПеречислениеСсылка.СтатусыЗаказовКлиентов - статус заказов.
//
// Возвращаемое значение:
// Массив из ДокументСсылка.ЗаказКлиента
//
Функция ЗаказыПоСтатусу(Контрагент, Статус) Экспорт
Результат = РегистрыСведений.ЗаказыКлиентов.ЗаказыПоСтатусу(Контрагент, Статус);
Возврат Результат;
КонецФункции
#КонецОбласти
Общий модуль "РассылкиИОповещенияКлиентам":
#Область ПрограммныйИнтерфейс
// Выполняет рассылку оповещений контрагенту из очереди по указанному типу события.
//
// Параметры:
// Контрагент - СправочникСсылка.Контрагенты - контрагент, для которого требуется выполнить рассылку.
// ТипСобытия - ПеречислениеСсылка.ТипыСобытийОповещений - тип события.
//
Процедура ВыполнитьРассылкуОповещенийПоТипу(Контрагент, ТипСобытия) Экспорт
// Логика выполнения рассылки.
КонецПроцедуры
#КонецОбласти
Что решилось после исправления:
Интерфейс модуля упрощен, функциональность разделена на узконаправленные модули, убраны навязанные параметры. Теперь, вызывающий код, получает ровно то, что ему нужно, без навязывания ненужной функциональности. Тестирование также упрощается, каждый метод и модуль проверяется отдельно.
1. ОбщегоНазначенияУТ.ИзменитьПризнакСогласованностиДокумента()
Предоставляет интерфейс, который шире, чем нужно некоторым клиентам. Параметр СтатусНеСогласован с поддержкой массивов и сложная логика для разных режимов записи навязывают функциональность, которая не требуется клиентам с простыми задачами (например, только сброс Согласован при записи).
// Устанавливает или сбрасывает флаг Согласован у документа.
// Вызывается из процедуры ПередЗаписью документа.
//
// Параметры:
// ДокументОбъект - ДокументОбъект - Документ, в котором необходимо изменить флаг Согласован
// РежимЗаписи - РежимЗаписиДокумента - Режим записи документа
// СтатусНеСогласован - ПеречислениеСсылка - Статус документа, в котором флаг Согласован должен быть сброшен.
//
// Процедура ИзменитьПризнакСогласованностиДокумента(ДокументОбъект, Знач РежимЗаписи, Знач СтатусНеСогласован = Неопределено) Экспорт
Если РежимЗаписи = РежимЗаписиДокумента.Запись
ИЛИ РежимЗаписи = РежимЗаписиДокумента.ОтменаПроведения Тогда
Если ДокументОбъект.Согласован Тогда
ДокументОбъект.Согласован = Ложь;
КонецЕсли;
ИначеЕсли РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
// Документ не имеет статуса
Если СтатусНеСогласован = Неопределено Тогда
Если Не ДокументОбъект.Согласован Тогда
ДокументОбъект.Согласован = Истина;
КонецЕсли;
// Документ имеет статус из массива, в которых проведенный документ не согласован
ИначеЕсли ТипЗнч(СтатусНеСогласован) = Тип("Массив") Тогда
Если ДокументОбъект.Согласован Тогда
Для Каждого ТекСтатус Из СтатусНеСогласован Цикл
Если ДокументОбъект.Статус = ТекСтатус Тогда
ДокументОбъект.Согласован = Ложь;
Прервать;
КонецЕсли;
КонецЦикла;
Иначе
ДокументСогласован = Истина;
Для Каждого ТекСтатус Из СтатусНеСогласован Цикл
Если ДокументОбъект.Статус = ТекСтатус Тогда
ДокументСогласован = Ложь;
КонецЕсли;
КонецЦикла;
Если ДокументСогласован Тогда
ДокументОбъект.Согласован = Истина;
КонецЕсли;
КонецЕсли;
// Документ имеет статус, в котором проведенный документ не согласован
Иначе
Если ДокументОбъект.Статус = СтатусНеСогласован И ДокументОбъект.Согласован Тогда
ДокументОбъект.Согласован = Ложь;
ИначеЕсли ДокументОбъект.Статус <> СтатусНеСогласован И Не ДокументОбъект.Согласован Тогда
ДокументОбъект.Согласован = Истина;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
2. ОбработкаТабличнойЧастиСервер.ПроверитьКорректностьЗаполнитьХарактеристикиИУпаковки()
Объединяет несколько независимых задач (проверка характеристик, упаковок, заполнение данных для некачественного товара) в одном методе с избыточным интерфейсом через СтруктураДействий. Клиенту, которому нужна только проверка характеристики, навязывается логика работы с упаковками и некачественным товаром, включая ненужные зависимости от дополнительных свойств структуры и сложных условий. Это делает интерфейс максимально перегруженным для клиентов с узкими задачами, что противоречит принципу.
Процедура ПроверитьКорректностьЗаполнитьХарактеристикиИУпаковки(ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения) Экспорт
Перем Характеристика;
Перем Упаковка;
ПроверитьХарактеристикуПоВладельцу = СтруктураДействий.Свойство("ПроверитьХарактеристикуПоВладельцу", Характеристика);
ПроверитьЗаполнитьУпаковкуПоВладельцу = СтруктураДействий.Свойство("ПроверитьЗаполнитьУпаковкуПоВладельцу", Упаковка);
Если ПроверитьХарактеристикуПоВладельцу
Или ПроверитьЗаполнитьУпаковкуПоВладельцу Тогда
РезультатПроверки = Справочники.Номенклатура.ХарактеристикаИУпаковкаПринадлежатВладельцу(ТекущаяСтрока.Номенклатура, Характеристика, Упаковка);
Если ПроверитьХарактеристикуПоВладельцу Тогда
ТекущаяСтрока.Характеристика = РезультатПроверки.Характеристика;
ТекущаяСтрока.ХарактеристикиИспользуются = РезультатПроверки.ХарактеристикиИспользуются;
КонецЕсли;
Если ПроверитьЗаполнитьУпаковкуПоВладельцу Тогда
ТекущаяСтрока.Упаковка = РезультатПроверки.Упаковка;
КонецЕсли;
Если СтруктураДействий.Свойство("ЗаполнитьУпаковкуНекачественногоТовара")
И ПроверитьЗаполнитьУпаковкуПоВладельцу
И ЗначениеЗаполнено(ТекущаяСтрока.Номенклатура)
И НЕ ЗначениеЗаполнено(ТекущаяСтрока.Упаковка) Тогда
ТекущаяСтрока.Упаковка = Справочники.УпаковкиЕдиницыИзмерения.ИдентичнаяУпаковка(ТекущаяСтрока.НоменклатураИсходногоКачества,
ТекущаяСтрока.Номенклатура,
Упаковка);
КонецЕсли;
Если СтруктураДействий.Свойство("ЗаполнитьХарактеристикуНекачественногоТовара")
И ЗначениеЗаполнено(Характеристика)
И Не ЗначениеЗаполнено(ТекущаяСтрока.Характеристика)
И ТекущаяСтрока.Номенклатура.ИспользованиеХарактеристик = Перечисления.ВариантыИспользованияХарактеристикНоменклатуры.ИндивидуальныеДляНоменклатуры Тогда
ТекущаяСтрока.Характеристика = Справочники.ХарактеристикиНоменклатуры.ИдентичнаяХарактеристика(
ТекущаяСтрока.НоменклатураИсходногоКачества,
ТекущаяСтрока.Номенклатура,
Характеристика);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
3. ОбработкаТабличнойЧастиСервер.СкорректироватьСтавкуНДСВСтрокеТЧ()
Объединяет корректировку ставки НДС с избыточной логикой (проверка многооборотной тары, работа с кэшем, обработка на основании копирования, динамическое имя поля номенклатуры) в одном методе, зависящем от сложной структуры СтруктураДействий. Клиенту, которому нужно просто скорректировать ставку НДС, навязывается весь функционал, включая ненужные проверки и зависимости от кэшированных значений, что делает интерфейс перегруженным.
Процедура СкорректироватьСтавкуНДСВСтрокеТЧ(ТекущаяСтрока, СтруктураДействий, КэшированныеЗначения) Экспорт
Перем СтруктураПараметровДействия;
Перем ВернутьМногооборотнуюТару;
Перем ТипНоменклатуры;
Если СтруктураДействий.Свойство("СкорректироватьСтавкуНДС", СтруктураПараметровДействия) Тогда
Если СтруктураПараметровДействия.ИнициализацияВходящегоДокумента И ЗначениеЗаполнено(ТекущаяСтрока.СтавкаНДС) Тогда
Возврат;
КонецЕсли;
СтруктураПараметровДействия.Свойство("ВернутьМногооборотнуюТару", ВернутьМногооборотнуюТару);
НалогообложениеНДС = СтруктураПараметровДействия.НалогообложениеНДС;
Дата = СтруктураПараметровДействия.Дата;
Организация = СтруктураПараметровДействия.Организация;
ИмяПоляНоменклатура = "Номенклатура";
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(СтруктураПараметровДействия, "ИмяПоляНоменклатура")
И ЗначениеЗаполнено(СтруктураПараметровДействия.ИмяПоляНоменклатура) Тогда
ИмяПоляНоменклатура = СтруктураПараметровДействия.ИмяПоляНоменклатура;
КонецЕсли;
Номенклатура = Неопределено;
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ТекущаяСтрока, ИмяПоляНоменклатура) Тогда
Номенклатура = ТекущаяСтрока[ИмяПоляНоменклатура];
КонецЕсли;
Если ВернутьМногооборотнуюТару Тогда
Если ОбщегоНазначенияКлиентСервер.ЕстьРеквизитИлиСвойствоОбъекта(ТекущаяСтрока, "ТипНоменклатуры") Тогда
ТипНоменклатуры = ТекущаяСтрока.ТипНоменклатуры;
ИначеЕсли ЗначениеЗаполнено(Номенклатура) Тогда
ТипыНоменклатуры = КэшированныеЗначения.ТипыНоменклатуры; //Соответствие
ТипНоменклатуры = ТипыНоменклатуры.Получить(Номенклатура);
КонецЕсли;
КонецЕсли;
Если ТипНоменклатуры = ПредопределенноеЗначение("Перечисление.ТипыНоменклатуры.МногооборотнаяТара") Тогда
СтавкаНДС = ПредопределенноеЗначение("Справочник.СтавкиНДС.БезНДС");
Иначе
ПроверятьАктуальность = Истина;
Если ТекущаяСтрока.СтавкаНДС = ПредопределенноеЗначение("Справочник.СтавкиНДС.БезНДС")
И НалогообложениеНДС = ПредопределенноеЗначение("Перечисление.ТипыНалогообложенияНДС.ПродажаОблагаетсяНДС")
И СтруктураПараметровДействия.ЗаполнениеНаОснованииКопирование = Ложь Тогда
ПроверятьАктуальность = Ложь;
КонецЕсли;
Если ПроверятьАктуальность Тогда
Если КэшированныеЗначения.АктуальныеСтавкиНДС = Неопределено Тогда
ЗаполнитьАктуальныеСтавкиНДСКэшированныеЗначения(СтруктураПараметровДействия, КэшированныеЗначения);
КонецЕсли;
АктуальныеСтавкиНДС = КэшированныеЗначения.АктуальныеСтавкиНДС; //Массив
Если АктуальныеСтавкиНДС.Найти(ТекущаяСтрока.СтавкаНДС) <> Неопределено Тогда
Возврат;
КонецЕсли;
КонецЕсли;
СтавкаНДС = УчетНДСУП.СтавкаНДСПоНоменклатуреИНалогообложению(Номенклатура, НалогообложениеНДС, Организация, Дата);
КонецЕсли;
Если ТекущаяСтрока.СтавкаНДС <> СтавкаНДС тогда
ТекущаяСтрока.СтавкаНДС = СтавкаНДС;
ОбработанныеСтроки = КэшированныеЗначения.ОбработанныеСтроки; //Массив
ОбработанныеСтроки.Добавить(ТекущаяСтрока);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Результат применения
При следовании принципу код становится более адаптивным и поддерживаемым. Уменьшается высокая связанность кода, что снижает риск побочных эффектов. Это позволяет избежать конфликтов при доработках, так как изменения в одной части функциональности интерфейса, не затрагивают клиентов интерфейсов, использующих другую. Разработка становится более эффективной, поскольку клиентам доступны только нужные интерфейсы, а не громоздкие общие. Тестирование также упрощается, каждый модуль проверяется изолированно, без необходимости учитывать избыточные зависимости.
Наверное, многие сталкивались с такой ситуацией, когда нужно срочно зарядить свой iPhone или Android, а у друзей под рукой только неподходящий кабель. Вы пытаетесь подключить его, но безуспешно. Все дело в том, что устройство слишком жестко привязано к своему типу зарядки, и эта зависимость ломает весь процесс. Так же и в коде, когда есть прямая зависимость от конкретных реализаций, любая мелочь вроде смены кабеля может остановить работу системы. Принцип инверсии зависимостей учит нас, как сделать зарядку универсальной, чтобы код работал гибко и без сбоев. Разбираемся дальше.
Описание
Пятый принцип SOLID утверждает - модули высокого уровня не должны зависеть от конкретных реализаций низкого уровня, оба должны зависеть от абстракций. Ваш iPhone не заряжается от кабеля для Android, потому что он привязан к Lightning-разъeму, и любой другой кабель бесполезен. Проблема в том, что телефон зависит от конкретного низкоуровневого типа кабеля, а не от универсального стандарта. Пользователь телефона выражает потребность в зарядке устройства или передаче данных и ему не важны детали, чипы, провода или тип разъeма, главное, чтобы зарядка работала. С разъeмом Lightning это не так, телефон и кабель связаны жeстко, и смена одного ломает всe.
Инверсия зависимостей решает это через универсальный стандарт, вроде разъема USB-C, который задает правила взаимодействия. Разъем USB-C, это абстракция, контракт, который определяет, как энергия (данные) должна поступать в телефон. Он говорит: "Я принимаю энергию в таком формате (вольты, амперы)". Зарядный кабель, это низкоуровневая реализация, которая реализует детали, как именно передается энергия и из какого источника (зарядного устройства, power bank и т.д.) к телефону, через абстракцию (разъем USB-C), подстраиваясь под этот разъем и его формат получения энергии (контракт).
В коде это работает так же, вместо жесткой связи высокоуровневой логики и низкоуровневых деталей реализации, следует использовать асбтракцию, которая будет задавать контракт, правила взаимодействия между ними.
Применение в 1С
На уровне архитектуры платформа 1С предоставляет следующие механизмы для реализации требований принципа.
- Общие модули как абстракции. В 1С принято создавать общие модули с чeтко определeнными интерфейсами, которые скрывают детали реализации. Например, какой-то общий модуль может предоставлять функции, при этом полагаясь на более низкий уровень, например служебные модули или переопределяемые, вызывающий код обращается к ним через экспортируемые методы, не зная, как именно они работают внутри.
- Подписки на события. Используются для передачи управления между модулями через абстрактные события, а не прямые вызовы конкретных процедур. Например, подписка на событие ПередЗаписью позволяет дополнить логику документа, не привязываясь к его внутренней реализации.
- Расширения. Позволяют внедрять новую логику через абстрактные точки расширения, такие как обработчики событий или перехват методов, вместо прямого изменения типовых объектов.
Модуль объекта документа "СборкаТоваров" выполняет бизнес логику, т.е. находится на "высоком" уровне. Он диктует потребность в данных определенного вида. В данных примерах, его потребность, это данные цены в виде структуры с ключами "Цена" и "ДатаУстановки".
Нарушение принципа:
Документ "СборкаТоваров" модуль объекта:
#Если Сервер Или ТолстыйКлиентОбычноеПриложение Или ВнешнееСоединение Тогда
#Область ОбработчикиСобытий
Процедура ПередЗаписью(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
ЗаполнитьДанныеЦены();
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Процедура ЗаполнитьДанныеЦены()
// Зависимость "высокоуровнего" модуля от деталей реализации - конкретного регистра сведений.
ДанныеУстановленнойЦены = РегистрыСведений.ЦеныНоменклатуры.ДанныеУстановленнойЦены(Номенклатура);
Цена = ДанныеУстановленнойЦены.Цена;
ДатаУстановкиЦены = ДанныеУстановленнойЦены.ДатаУстановки;
КонецПроцедуры
#КонецОбласти
#КонецЕсли
Регистр сведений "ЦеныНоменклатуры" модуль менеджера:
#Если Сервер Или ТолстыйКлиентОбычноеПриложение Или ВнешнееСоединение Тогда
#Область ПрограммныйИнтерфейс
// Возвращает данные по установленной цене номенклатуры.
//
// Параметры:
// НоменклатураСсылка - СправочникСсылка.Номенклатура - номенклатура, для которой требуется получить данные.
// Дата - Дата, Неопределено - дата, на которую требуется получить данные, если не заполнена - текущая дата.
//
// Возвращаемое значение:
// Структура:
// * Цена - Число - цена.
// * ДатаУстановки - Дата - дата установки цены.
//
Функция ДанныеУстановленнойЦены(НоменклатураСсылка, Дата = Неопределено) Экспорт
Результат = Новый Структура;
Результат.Вставить("Цена", 0);
Результат.Вставить("ДатаУстановки", '00010101');
Если Не ЗначениеЗаполнено(Дата) Тогда
Дата = ТекущаяДатаСеанса();
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатурыСрезПоследних.Период КАК ДатаУстановки,
| ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата, Номенклатура = &НоменклатураСсылка) КАК ЦеныНоменклатурыСрезПоследних";
Запрос.УстановитьПараметр("Дата", Дата);
Запрос.УстановитьПараметр("НоменклатураСсылка", НоменклатураСсылка);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() Тогда
ЗаполнитьЗначенияСвойств(Результат, Выборка);
КонецЕсли;
Возврат Результат;
КонецФункции
#КонецОбласти
#КонецЕсли
Что плохого в этом примере:
Потребность модуля объекта документа "СборкаТоваров" берется реализовать метод "ДанныеУстановленнойЦены" модуля менеджера регистра сведений "ЦеныНоменклатуры", в котором находится конкретная "низкоуровневая" логика получения данных.
Таким образом, модуль "высокого" уровня, привязан к модулю "низкого уровня", т.е. зависит от конкретных деталей реализации его потребности, конкретного регистра сведений, и получения данных из него.
Если нужно будет изменить логку получения данных цены, например, на новый механизм ценообразования, регистр сведений "ЦеныНоменклатуры25" или получать данные из внешнего источника, то придется модифицировать код в модуле объекта документа.
Тестирование также затруднено, не получится протестировать получение данных цены из другого источника, без модификации модуля объекта документа.
Соблюдение принципа:
Документ "СборкаТоваров" модуль объекта:
#Если Сервер Или ТолстыйКлиентОбычноеПриложение Или ВнешнееСоединение Тогда
#Область ОбработчикиСобытий
Процедура ПередЗаписью(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
ЗаполнитьДанныеЦены();
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Процедура ЗаполнитьДанныеЦены()
// Зависимость модуля от абстракции.
ДанныеУстановленнойЦены = Ценообразование.ДанныеУстановленнойЦены(Номенклатура);
Цена = ДанныеУстановленнойЦены.Цена;
ДатаУстановкиЦены = ДанныеУстановленнойЦены.ДатаУстановки;
КонецПроцедуры
#КонецОбласти
#КонецЕсли
Общий модуль Ценообразование:
#Область ПрограммныйИнтерфейс
// Возвращает данные по установленной цене номенклатуры.
//
// Параметры:
// НоменклатураСсылка - СправочникСсылка.Номенклатура - номенклатура, для которой требуется получить данные.
// Дата - Дата, Неопределено - дата, на которую требуется получить данные, если не заполнена - текущая дата.
//
// Возвращаемое значение:
// Структура - см. ОписаниеДанныхУстановленнойЦены
//
Функция ДанныеУстановленнойЦены(НоменклатураСсылка, Дата = Неопределено) Экспорт
Результат = ОписаниеДанныхУстановленнойЦены();
Если Не ЗначениеЗаполнено(Дата) Тогда
Дата = ТекущаяДатаСеанса();
КонецЕсли;
// Абстракция передает управление конкретной реализации, которая подстраивается под заданный контракт.
ДанныеУстановленнойЦены = ЦенообразованиеПереопределяемый.ДанныеУстановленнойЦены(НоменклатураСсылка, Дата);
ЗаполнитьЗначенияСвойств(Результат, ДанныеУстановленнойЦены);
Возврат Результат;
КонецФункции
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
Функция ОписаниеДанныхУстановленнойЦены()
Результат = Новый Структура;
Результат.Вставить("Цена", 0);
Результат.Вставить("ДатаУстановки", '00010101');
Возврат Результат;
КонецФункции
#КонецОбласти
Общий модуль ЦенообразованиеПереопределяемый:
#Область ПрограммныйИнтерфейс
// Возвращает данные по установленной цене номенклатуры.
//
// Параметры:
// НоменклатураСсылка - СправочникСсылка.Номенклатура - номенклатура, для которой требуется получить данные.
// Дата - Дата - дата, на которую требуется получить данные.
//
// Возвращаемое значение:
// Структура - см. Ценообразование.ОписаниеДанныхУстановленнойЦены
//
Функция ДанныеУстановленнойЦены(НоменклатураСсылка, Дата) Экспорт
// Подстраивается и выполняет контракт, заданный абстракцией.
Результат = Новый Структура;
Результат.Вставить("Цена", 0);
Результат.Вставить("ДатаУстановки", '00010101');
ДанныеУстановленнойЦены = РегистрыСведений.ЦеныНоменклатуры.ДанныеУстановленнойЦены(Номенклатура, Дата);
ЗаполнитьЗначенияСвойств(Результат, ДанныеУстановленнойЦены );
Возврат Результат;
КонецФункции
#КонецОбласти
Что решилось после исправления:
Метод ДанныеУстановленнойЦены в модуле Ценообразование находится в области ПрограммныйИнтерфейс, в сигнатуре описаны входящие параметры и возвращаемое значение. Тем самым, задаeтся контракт для публичного интерфейса, который гарантирует, что данные будут возвращены в том виде, как "обещано", при соблюдении условий контракта (корректных входящих параметрах). И мы, как разработчики, после этого, не должны изменять логику этого метода, если при этом будут изменены условия контракта.
Теперь, модуль объекта документа "СборкаТоваров", задавая потребность в данных определенного вида, не зависит от того, откуда и как эти данные получаются. Он зависит от абстракции, метода ДанныеУстановленнойЦены в модуле Ценообразование, его устраивает контракт, заданный этим методом.
Контракт метода публичного интерфейса сам ничего не получает, он делегирует эту логику "низкоуровневой" реализации, в данном случае методу ДанныеУстановленнойЦены модуля ЦенообразованиеПереопределяемый, который, в свою очередь, подстраивается под контракт, заданный методом ДанныеУстановленнойЦены модуля Ценообразование и реализует его.
Таким образом, "высокоуровневый" код бизнес-логики, больше не зависит от "низкоуровневых" деталей реализации, он зависит от абстракции (контракта). "Низкоуровневая" логика получения также зависит от абстракции, подстраиваясь под контракт. Оба, "высокоуровневый" и "низкоуровневый" код, теперь зависят от абстракции, контракта, задающего правила взаимодействия.
Теперь, чтобы изменить источник получения цены, достаточно изменить "низкоуровневую" логику, подстроившись, под заданный контракт, не трогая модуль объекта документа "СборкаТоваров".
Тестирование также упрощается, для тестов можно подставлять mock-объекты (заглушки).
1. ЗаказНаСборку.ОформленоКомплектов()
Высокоуровневая функция ОформленоКомплектов зависит от низкоуровневых модулей (РегистрыНакопления ЗаказыНаСборку, ТоварыКПоступлению) вместо абстракций. Нет инверсии зависимостей: зависимости направлены от высокоуровневого кода к низкоуровневым деталям, а не к абстрактным интерфейсам.
// Возвращает количество собранных (разобранных) комплектов по переданному заказу
//
// Параметры:
// Объект - ДокументОбъект.ЗаказНаСборку, ДокументСсылка.ЗаказНаСборку -
//
// Возвращаемое значение:
// Число -
//
Функция ОформленоКомплектов(Объект) Экспорт
ОтборОформлено = Новый ТаблицаЗначений();
ОтборОформлено.Колонки.Добавить("КодСтроки", Новый ОписаниеТипов("Число"));
ОтборОформлено.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("ДокументСсылка.ЗаказНаСборку"));
СтрокаОтбораКомлектов = ОтборОформлено.Добавить();
СтрокаОтбораКомлектов.Ссылка = Объект.Ссылка;
СтрокаОтбораКомлектов.КодСтроки = 1;
ТаблицаШапки = Новый ТаблицаЗначений();
ТаблицаШапки.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаШапки.Колонки.Добавить("Характеристика", Новый ОписаниеТипов("СправочникСсылка.ХарактеристикиНоменклатуры"));
ТаблицаШапки.Колонки.Добавить("Склад", Новый ОписаниеТипов("СправочникСсылка.Склады"));
ТаблицаШапки.Колонки.Добавить("Назначение", Новый ОписаниеТипов("СправочникСсылка.Назначения"));
ТаблицаШапки.Колонки.Добавить("Серия", Новый ОписаниеТипов("СправочникСсылка.СерииНоменклатуры"));
ТаблицаШапки.Колонки.Добавить("Ссылка", Новый ОписаниеТипов("ДокументСсылка.ЗаказНаСборку"));
Корректировка = ТаблицаШапки.Скопировать(); // пустая табличная часть для передачи в регистры
Корректировка.Колонки.Добавить("КПоступлению", Новый ОписаниеТипов("Число"));
Корректировка.Колонки.Добавить("КОтгрузке", Новый ОписаниеТипов("Число"));
УчитыватьНазначение = ЗначениеЗаполнено(Объект.Назначение)
И ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Объект.Назначение, "ДвиженияПоСкладскимРегистрам") = Истина;
Если Объект.СтатусУказанияСерий = 14 Тогда
СтрокаШапки = ТаблицаШапки.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаШапки, Объект);
СтрокаШапки.Назначение = ?(УчитыватьНазначение, Объект.Назначение, Справочники.Назначения.ПустаяСсылка());
Иначе
// СтатусУказанияСерий = 10
// Используется табличная часть Серии.
Для Каждого Строка Из Объект.Серии Цикл
СтрокаШапки = ТаблицаШапки.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаШапки, Строка, "Номенклатура, Характеристика, Серия, Назначение");
ЗаполнитьЗначенияСвойств(СтрокаШапки, Объект, "Склад, Ссылка");
КонецЦикла;
КонецЕсли;
Отбор = Новый Структура;
Если Объект.ХозяйственнаяОперация = Перечисления.ХозяйственныеОперации.РазборкаТоваров Тогда
Отбор.Вставить("ТипСборки", Перечисления.ТипыДвиженияЗапасов.Отгрузка);
ОформитьКомплектовПоНакладным = РегистрыНакопления.ЗаказыНаСборку.ТаблицаОформлено(ОтборОформлено, Отбор);
ОформитьКомплектовПоОрдерам = РегистрыНакопления.ТоварыКОтгрузке.ТаблицаОформлено(ТаблицаШапки, Корректировка);
Иначе
Отбор.Вставить("ТипСборки", Перечисления.ТипыДвиженияЗапасов.Поступление);
ОформитьКомплектовПоНакладным = РегистрыНакопления.ЗаказыНаСборку.ТаблицаОформлено(ОтборОформлено, Отбор);
ОформитьКомплектовПоОрдерам = РегистрыНакопления.ТоварыКПоступлению.ТаблицаОформлено(ТаблицаШапки, Корректировка);
КонецЕсли;
ОформитьКомплектовПоНакладным.Свернуть(, "Количество");
ОформитьКомплектовПоОрдерам.Свернуть(, "Количество");
ОформленоКомплектов = Макс(ОформитьКомплектовПоНакладным.Итог("Количество"), ОформитьКомплектовПоОрдерам.Итог("Количество"));
Возврат ОформленоКомплектов;
КонецФункции
2. АссортиментСервер.ОбъектПланирования()
Напрямую зависит от конкретного регистра сведений ИсторияИзмененияФорматовМагазинов и его метода ТекущийФормат(). Это низкоуровневая реализация, связанная с хранением данных в платформе 1С. Высокоуровневая логика определения объекта планирования не должна знать, откуда именно берeтся текущий формат (из регистра, справочника или другого источника). Вместо этого можно было бы использовать абстракцию, например, интерфейс или функцию, реализацию которой можно подменять.
// Функция возвращает значение текущего объекта планирования ассортимента в зависимости от настроек.
//
// Параметры:
// ОбъектПроверки - СправочникСсылка.Склады, СправочникСсылка.ФорматыМагазинов - Склад или формат для которого
// определяется объект планирования
// НаДату - Дата - Дата на которую определяется текущий объект планирования.
//
// Возвращаемое значение:
// СправочникСсылка.ФорматыМагазинов, СправочникСсылка.Склады - текущий объект планирования, в зависимости от ФО -
// формат магазина или склад-магазин.
//
Функция ОбъектПланирования(ОбъектПроверки, Знач НаДату = Неопределено) Экспорт
Если Не ЗначениеЗаполнено(НаДату) Тогда
НаДату = ТекущаяДатаСеанса();
КонецЕсли;
Если ТипЗнч(ОбъектПроверки) = Тип("СправочникСсылка.Склады") И ПолучитьФункциональнуюОпцию("ИспользоватьФорматыМагазинов") Тогда
ОбъектПланирования = РегистрыСведений.ИсторияИзмененияФорматовМагазинов.ТекущийФормат(ОбъектПроверки, НаДату);
Иначе
ОбъектПланирования = ОбъектПроверки;
КонецЕсли;
Возврат ОбъектПланирования;
КонецФункции
3. РаспознаваниеДокументовУП.СтавкаНДСПоРаспознанномуТексту()
Выполняет высокоуровневую задачу, преобразование текстового представления ставки НДС в объект справочника. Однако вместо того чтобы зависеть от абстракций, она напрямую обращается к низкоуровневым объектам платформы (Справочники.СтавкиНДС, Перечисления.СтавкиНДС). Это нарушает принцип, так как Нет инверсии зависимостей, зависимости направлены от высокоуровневой логики к низкоуровневым деталям, а не к абстракциям. Жeсткая связанность: Код привязан к конкретной реализации хранения ставок НДС.
Функция СтавкаНДСПоРаспознанномуТексту(Ставка, ТекущееЗначение) Экспорт
Если Ставка = "НДС20" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС20);
ИначеЕсли Ставка = "НДС18" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС18);
ИначеЕсли Ставка = "НДС10" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС10);
ИначеЕсли Ставка = "НДС18_118" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС18_118);
ИначеЕсли Ставка = "НДС10_110" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС10_110);
ИначеЕсли Ставка = "НДС20_120" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС20_120);
ИначеЕсли Ставка = "НДС0" Тогда
Возврат Справочники.СтавкиНДС.НайтиПоРеквизиту("ПеречислениеСтавкаНДС", Перечисления.СтавкиНДС.НДС0);
ИначеЕсли Ставка = "БезНДС" Тогда
Возврат Справочники.СтавкиНДС.БезНДС;
КонецЕсли;
Возврат ТекущееЗначение;
КонецФункции
Результат применения
При следовании принципу код становится более адаптивным и устойчивым. "Высокоуровневые" методы зависят от абстрактных интерфейсов, а не от конкретных деталей реализации. Это позволяет легко менять логику, например, переключаться с регистра на внешний источник, без переписывания вызывающего кода. Поддержка также упрощается, так как доработки и расширения подключаются к абстракциям, не затрагивая основную логику. Тестирование становится эффективнее, использование mock-объектов для абстрактных интерфейсов изолирует модули, сокращая зависимости от реальных данных.
Как применять на практике
Весь этот материал, не просто теория, а руководство к действию, которое может изменить ваш взгляд на код. Начав мыслить принципами SOLID, вы увидите код по-новому, вместо набора методов, он превратится в систему взаимосвязанных компонентов, где каждая часть играет свою роль. Это, как переключиться с хаотичного "пишу как получится" на структурированное "проектирую с целью". Используйте материал как чек-лист при проектировании.
S: Анализируйте задачи с точки зрения ответственности, одна программная сущность - одна цель.
O: Проектируйте код с возможностью добавления функциональности через точки расширения.
L: Проверяйте, чтобы подмена базовой логики не нарушала контракт.
I: Упрощайте интерфейсы, убирая из них всe лишнее для конкретного клиента.
D: Внедряйте абстракции, чтобы разорвать жeсткие связи между "высокоуровневой" логикой и "низкоуровневыми" деталями реализации.
Примеры из статьи - это шаблоны, адаптируйте их под свои задачи. Постепенно внедряйте принципы в рабочие процессы, начиная с небольших доработок, чтобы увидеть эффект на практике.
Послесловие
Мы начинали с вопроса "что это такое?" и применимы ли эти принципы к нашей платформе. Теперь, разобрав каждый из них, от разделения ответственности до инверсии зависимостей на примерах из 1С, я надеюсь, показал, что SOLID не только применим в разработке 1С, но и может стать отличным ориентиром на пути к порядку в коде, для всех нас. Если после этой статьи у вас загорелись глаза применить эти принципы в своих проектах, значит, мы вместе сделали шаг к порядку, чистому коду. Спасибо, что прошли этот путь со мной, надеюсь, материал вдохновил вас!
Автор: Григорьев Руслан