Принципы ООП¶
Базовые идеи объектного подхода. Они дают язык, на котором потом проще обсуждать архитектурные решения.
Для 1С это особенно важно: часть архитектурных понятий закреплена не отдельными конструкциями языка, а стандартами разработки. Например:
- #std551: Разработка конфигураций с повторным использованием общего кода и объектов метаданных и #std455: Структура модуля формализуют понятия программного интерфейса, служебного программного интерфейса и внутренней реализации;
- #std453: Описание процедур и функций требует документировать методы программного интерфейса;
- #std644: Обеспечение совместимости библиотек рассматривает API как контракт совместимости;
- #std678: Безопасность прикладного программного интерфейса сервера показывает, что серверный интерфейс — это еще и граница безопасности.
Интерфейсы¶
Интерфейс — это контракт, который описывает, какие операции доступны снаружи, но не раскрывает, как именно они реализованы внутри.
Практический смысл:
- вызывающий код зависит от набора возможностей, а не от устройства реализации;
- разные реализации можно подменять без переписывания всего клиентского кода;
- границы между подсистемами становятся более явными и проверяемыми.
Во многих ООП-языках интерфейс — отдельная языковая конструкция. В повседневном коде 1С такого механизма в явном виде обычно нет, поэтому роль интерфейса часто выполняют:
- документированный набор экспортных методов;
- согласованный контракт между модулями;
- общий набор методов у разных объектов платформы.
В 1С понятие интерфейса дополнительно формализуют стандарты:
- #std551 разделяет код на
Программный интерфейс,Служебный программный интерфейси служебную реализацию; - #std455 закрепляет это разделение в структуре модуля и шаблонах областей;
- #std453 требует документировать методы программного интерфейса;
- #std630: Правила создания модулей форм и #std544: Ограничения на использование экспортных процедур и функций объясняют, где интерфейс не должен жить: в формах и командах;
- #std644 требует не ломать API библиотек и не смешивать контракт с деталями реализации.
Процедура СохранитьОбъект(Объект)
// Для этой процедуры интерфейс объекта - обладание методом Записать().
Объект.Записать();
КонецПроцедуры
Для вызывающего кода интерфейс здесь очень простой: переданный объект должен поддерживать метод Записать().
Это важно для 1С по двум причинам:
- контракт часто не зафиксирован синтаксисом языка и держится на договоренности;
- поэтому интерфейс нужно явно документировать, проверять и не размывать случайными зависимостями.
На что смотреть в 1С:
- понятен ли публичный API общего модуля или объекта;
- не зависит ли вызывающий код от лишних внутренних деталей;
- можно ли заменить реализацию, не переписывая всех потребителей.
Клиент как роль в паттернах¶
В описаниях ООП и паттернов слово Client обычно означает не сетевого клиента и не место выполнения кода, а код-потребитель интерфейса.
То есть Client в паттернах:
- создает объект сам или получает его через фабрику, параметр или внедрение зависимости;
- вызывает методы через контракт, а не через внутренние детали реализации;
- связывает участников паттерна в рабочий сценарий.
Именно поэтому в книгах по паттернам часто пишут client code: это просто код, который пользуется абстракцией.
Например:
- в
Strategyклиент выбирает конкретную стратегию и передает ее контексту; - в
Factory Methodклиент просит фабрику создать объект, не зная конкретный класс; - в
Facadeклиент работает с фасадом вместо прямой координации нескольких подсистем.
Это не то же самое, что клиент в клиент-серверной архитектуре.
В клиент-серверной архитектуре клиент — это сторона взаимодействия, которая обращается к серверу по сети или выполняется в клиентском контексте. В 1С сюда относятся, например, клиентское приложение, форма или код с &НаКлиенте.
В паттернах Client может быть любым потребителем API:
- другим объектом;
- общим модулем;
- подсистемой;
- отдельной процедурой в том же процессе.
То есть один и тот же модуль в 1С может быть серверным по месту выполнения, но при этом оставаться Client по роли относительно другого объекта или интерфейса.
Полиморфизм¶
Полиморфизм позволяет работать с разными реализациями через общий контракт.
Обычно полезно различать два вида полиморфизма:
ad-hocполиморфизм;- классический полиморфизм по подтипу или контракту.
Практический смысл:
- вызывающий код знает, что нужно сделать, но не зависит от того, как именно это сделано;
- разные варианты поведения можно подменять без переписывания всего сценария;
- длинные ветки
Если ... ИначеЕсли ...часто можно заменить на набор согласованных реализаций.
Когда это полезно:
- есть несколько способов выполнить одну и ту же операцию;
- алгоритм зависит от типа объекта, режима работы или бизнес-сценария;
- нужно уменьшить связанность между вызывающим кодом и конкретной реализацией.
Ad-hoc полиморфизм¶
Ad-hoc полиморфизм означает, что одна и та же операция ведет себя по-разному в зависимости от типов аргументов.
В 1С это особенно заметно на встроенных операциях и универсальных функциях языка.
Здесь один и тот же оператор +:
- складывает числа;
- конкатенирует строки;
- сдвигает дату на число секунд.
Это и есть ad-hoc полиморфизм: синтаксис один, а конкретное поведение выбирается по типам значений.
Похожий прием можно реализовать и в собственной функции, когда поведение выбирается через разбор типа параметра:
Функция ПолучитьКраткоеПредставление(Значение) Экспорт
ТипЗначения = ТипЗнч(Значение);
Если ТипЗначения = Тип("Строка") Тогда
Возврат СокрЛП(Значение);
ИначеЕсли ТипЗначения = Тип("Число") Тогда
Возврат Формат(Значение, "ЧГ=0");
ИначеЕсли ТипЗначения = Тип("Дата") Тогда
Возврат Формат(Значение, "ДФ=dd.MM.yyyy");
Иначе
Возврат Строка(Значение);
КонецЕсли;
КонецФункции
Здесь интерфейс вызова один и тот же, но результат зависит от того, что именно передали в параметр Значение. Это тоже ad-hoc полиморфизм, только реализованный вручную в коде прикладного решения.
Классический полиморфизм¶
Классический полиморфизм означает, что код работает с разными объектами через один и тот же ожидаемый набор операций.
В прикладной разработке на 1С это чаще проявляется не через собственные пользовательские классы, а через:
- общий набор методов у разных объектов платформы;
- договоренность о публичном API;
- единый контракт, который соблюдают разные реализации.
Пример на 1С:
Процедура СохранитьОбъект(Объект)
Объект.Записать();
КонецПроцедуры
Заказ = Документы.ЗаказКлиента.СоздатьДокумент();
Реализация = Документы.РеализацияТоваровУслуг.СоздатьДокумент();
СохранитьОбъект(Заказ);
СохранитьОбъект(Реализация);
Для процедуры СохранитьОбъект не так важно, какой именно это документ. Ей важен контракт: объект должен уметь выполнять Записать().
Именно это и есть классический полиморфизм: вызывающий код опирается на общее поведение, а не на конкретный тип реализации.
В 1С такой контракт часто делают явным организационно: методы, на которые должны опираться другие подсистемы, выносят в раздел Программный интерфейс по #std455 и #std551, а их назначение документируют по #std453.
На что смотреть в 1С:
- не разрастается ли код условиями по типу объекта или режиму выполнения;
- можно ли вынести различающееся поведение за стабильный интерфейс;
- не превращается ли общий модуль в диспетчер из десятков веток.
Инкапсуляция¶
Инкапсуляция означает, что объект или модуль скрывает внутреннее устройство и наружу отдает только нужный контракт.
Практический смысл:
- внешний код меньше знает о внутреннем состоянии и шагах обработки;
- изменение внутренней реализации меньше затрагивает соседние части системы;
- проще держать инварианты и проверять корректность данных в одном месте.
Когда это полезно:
- у сущности есть важные правила изменения состояния;
- внутренние детали легко испортить прямым доступом из разных мест;
- хочется сократить число неявных зависимостей между модулями.
В прикладной архитектуре 1С эту идею поддерживают и стандарты: #std630 выносит программный интерфейс из модулей форм, а #std678 требует не держать бизнес-логику в форме и не раскрывать на клиент лишние серверные детали.
Хороший прикладной признак инкапсуляции в 1С — самодостаточность данных внутри того объекта, который за них отвечает.
Для регистров это закрепляет #std477: Самодостаточность регистров: логика и отчеты по регистру не должны зависеть от полей регистратора, а должны работать на данных самого регистра.
Для документов ту же идею фиксирует #std603: Требования к проведению документов: движения нужно формировать максимально на данных самого документа, а изменяемые внешние данные, влияющие на движения, следует сохранять в документе. Это и есть практическое “замыкание данных в документе”, чтобы результат проведения не зависел от того, как позже изменились НСИ или другие внешние объекты.
Дополнительно #std649: Реквизиты требует при создании на основании заполнять все наследуемые реквизиты, что тоже помогает не терять нужный контекст внутри самого документа.
На что смотреть в 1С:
- не читает ли внешний код слишком много внутренних полей напрямую;
- не дублируются ли проверки и подготовка данных в нескольких местах;
- можно ли собрать связанную логику в один понятный API вместо россыпи процедур.
Наследование¶
Наследование позволяет строить новую реализацию на основе существующей, переиспользуя общее поведение и уточняя различия.
Обычно полезно различать несколько близких, но не одинаковых сценариев:
- наследование от базового класса;
- многоуровневое наследование;
- множественное наследование;
- реализацию интерфейса.
Практический смысл:
- можно вынести общий каркас поведения в базовый уровень;
- дочерние реализации получают общий контракт и общую часть логики;
- различия концентрируются в переопределяемых частях, а не копируются целиком.
Когда это полезно:
- есть устойчивое отношение
общее -> частное; - несколько реализаций действительно разделяют существенную общую логику;
- модель становится проще, а не сложнее, если ввести базовый уровень абстракции.
Наследование от базового класса¶
Это самый привычный вариант: есть базовый тип с общим поведением, а конкретные типы уточняют его под свою предметную область.
В 1С такой механизм редко оформляется как пользовательское наследование классов, но архитектурно он хорошо читается в платформенных типах, которые порождаются метаданными.
Например, платформа строит конкретные типы объектов метаданных:
СправочникОбъект.Номенклатура;СправочникОбъект.Контрагенты;ДокументОбъект.ЗаказКлиента.
У них есть конкретная предметная семантика, но при этом они получают общий набор возможностей своего семейства: запись, удаление, работа с реквизитами, стандартные операции менеджера и объекта.
В этом смысле конкретный справочник можно рассматривать как порожденный из более общего “базового” конструктора платформы для объектов метаданных.
Пример на 1С:
Номенклатура = Справочники.Номенклатура.СоздатьЭлемент();
Контрагент = Справочники.Контрагенты.СоздатьЭлемент();
Номенклатура.Наименование = "Товар";
Контрагент.Наименование = "Покупатель";
Номенклатура.Записать();
Контрагент.Записать();
Для прикладного кода это выглядит как работа с разными конкретными сущностями, которые унаследовали общий набор действий своего семейства.
Многоуровневое наследование¶
Многоуровневое наследование означает, что тип наследуется не напрямую только от одного базового класса, а через цепочку уровней: например, БазовыйТип -> ПромежуточныйТип -> КонкретныйТип.
Такой прием позволяет накапливать общее поведение по слоям, но в других языках от него тоже часто стараются уходить, особенно когда цепочка становится глубокой:
- поведение размазывается по нескольким уровням и его трудно собирать в голове;
- изменение базового уровня неожиданно ломает дальних потомков;
- переопределения и
super-вызовы быстро делают код хрупким; - иерархия начинает жить своей жизнью и хуже отражает предметную модель.
Поэтому даже там, где многоуровневое наследование технически доступно, обычно предпочитают держать иерархии короткими или заменять их:
- композицией;
- стратегиями;
- явными интерфейсами и делегированием.
В прикладной 1С такой прием применять нельзя: платформа не дает строить собственные пользовательские цепочки наследования классов уровня базовый -> промежуточный -> конечный.
То есть разработчик 1С не может осознанно спроектировать многоуровневую объектную иерархию так, как это делают в Java, C# или C++.
Если в 1С возникает желание выстроить несколько уровней “общего поведения”, это обычно признак того, что задачу лучше решить через:
- общий модуль с понятным API;
- композицию нескольких объектов или сервисов;
- полиморфизм по согласованному контракту.
Множественное наследование¶
Множественное наследование означает, что один тип наследует поведение сразу от нескольких базовых типов.
В мире в целом от него часто стараются уходить, потому что оно быстро приводит к конфликтам поведения и неочевидным зависимостям:
- становится трудно понять, откуда пришел конкретный метод;
- возникают конфликты одинаковых имен и правил переопределения;
- иерархия становится хрупкой и плохо читаемой.
В 1С такого пользовательского механизма архитектурно нет, и это скорее плюс: платформа не подталкивает к сложным иерархиям, которые потом тяжело сопровождать.
Поэтому в прикладной архитектуре 1С обычно выбирают не множественное наследование, а:
- композицию;
- явный программный интерфейс модуля;
- разделение ответственности по подсистемам и областям API.
Реализация интерфейса¶
Во многих языках это отдельный механизм: тип явно пишет, какой интерфейс он реализует.
В 1С такого синтаксиса обычно нет, но архитектурно похожий прием используется постоянно: реализация не объявляется ключевым словом, а организуется через согласованный контракт.
Обычно это выражается так:
- модуль или объект предоставляет ожидаемый набор экспортных методов;
- эти методы выносятся в область
ПрограммныйИнтерфейс; - при необходимости методы для внутренних потребителей выносятся в
СлужебныйПрограммныйИнтерфейс; - назначение интерфейса документируется по #std453, а сама структура закрепляется по #std455 и #std551.
То есть в 1С реализация интерфейса чаще всего не объявляется явно, а описывается организационно: через специальную область и соблюдение контрактов между модулями и подсистемами.
На что смотреть в 1С:
- не пытаемся ли мы смоделировать сложную иерархию там, где достаточно композиции;
- не подменяем ли отсутствие явных интерфейсов хаотичным набором экспортных процедур;
- оформлен ли контракт в
ПрограммномИнтерфейсетак, чтобы им реально могли безопасно пользоваться другие подсистемы; - не используется ли наследование там, где достаточно композиции или стратегии;
- не появляется ли хрупкая иерархия, где изменение базового поведения ломает все частные случаи;
- не маскирует ли наследование обычное копирование кода под “архитектуру”.
Как читать этот раздел дальше¶
- Сначала понять базовый смысл принципа.
- Потом проверить, какую реальную проблему он решает в прикладной конфигурации.
- Только после этого переходить к более конкретным паттернам
GOFи принципамGRASP.