Перейти к содержанию

Принципы ООП

Базовые идеи объектного подхода. Они дают язык, на котором потом проще обсуждать архитектурные решения.

Для 1С это особенно важно: часть архитектурных понятий закреплена не отдельными конструкциями языка, а стандартами разработки. Например:

Интерфейсы

Интерфейс — это контракт, который описывает, какие операции доступны снаружи, но не раскрывает, как именно они реализованы внутри.

Практический смысл:

  • вызывающий код зависит от набора возможностей, а не от устройства реализации;
  • разные реализации можно подменять без переписывания всего клиентского кода;
  • границы между подсистемами становятся более явными и проверяемыми.

Во многих ООП-языках интерфейс — отдельная языковая конструкция. В повседневном коде 1С такого механизма в явном виде обычно нет, поэтому роль интерфейса часто выполняют:

  • документированный набор экспортных методов;
  • согласованный контракт между модулями;
  • общий набор методов у разных объектов платформы.

В 1С понятие интерфейса дополнительно формализуют стандарты:

Процедура СохранитьОбъект(Объект)
    // Для этой процедуры интерфейс объекта - обладание методом Записать().
    Объект.Записать();
КонецПроцедуры

Для вызывающего кода интерфейс здесь очень простой: переданный объект должен поддерживать метод Записать().

Это важно для 1С по двум причинам:

  • контракт часто не зафиксирован синтаксисом языка и держится на договоренности;
  • поэтому интерфейс нужно явно документировать, проверять и не размывать случайными зависимостями.

На что смотреть в 1С:

  • понятен ли публичный API общего модуля или объекта;
  • не зависит ли вызывающий код от лишних внутренних деталей;
  • можно ли заменить реализацию, не переписывая всех потребителей.

Клиент как роль в паттернах

В описаниях ООП и паттернов слово Client обычно означает не сетевого клиента и не место выполнения кода, а код-потребитель интерфейса.

То есть Client в паттернах:

  • создает объект сам или получает его через фабрику, параметр или внедрение зависимости;
  • вызывает методы через контракт, а не через внутренние детали реализации;
  • связывает участников паттерна в рабочий сценарий.

Именно поэтому в книгах по паттернам часто пишут client code: это просто код, который пользуется абстракцией.

Например:

  • в Strategy клиент выбирает конкретную стратегию и передает ее контексту;
  • в Factory Method клиент просит фабрику создать объект, не зная конкретный класс;
  • в Facade клиент работает с фасадом вместо прямой координации нескольких подсистем.

Это не то же самое, что клиент в клиент-серверной архитектуре.

В клиент-серверной архитектуре клиент — это сторона взаимодействия, которая обращается к серверу по сети или выполняется в клиентском контексте. В 1С сюда относятся, например, клиентское приложение, форма или код с &НаКлиенте.

В паттернах Client может быть любым потребителем API:

  • другим объектом;
  • общим модулем;
  • подсистемой;
  • отдельной процедурой в том же процессе.

То есть один и тот же модуль в 1С может быть серверным по месту выполнения, но при этом оставаться Client по роли относительно другого объекта или интерфейса.

Полиморфизм

Полиморфизм позволяет работать с разными реализациями через общий контракт.

Обычно полезно различать два вида полиморфизма:

  • ad-hoc полиморфизм;
  • классический полиморфизм по подтипу или контракту.

Практический смысл:

  • вызывающий код знает, что нужно сделать, но не зависит от того, как именно это сделано;
  • разные варианты поведения можно подменять без переписывания всего сценария;
  • длинные ветки Если ... ИначеЕсли ... часто можно заменить на набор согласованных реализаций.

Когда это полезно:

  • есть несколько способов выполнить одну и ту же операцию;
  • алгоритм зависит от типа объекта, режима работы или бизнес-сценария;
  • нужно уменьшить связанность между вызывающим кодом и конкретной реализацией.

Ad-hoc полиморфизм

Ad-hoc полиморфизм означает, что одна и та же операция ведет себя по-разному в зависимости от типов аргументов.

В 1С это особенно заметно на встроенных операциях и универсальных функциях языка.

ЧисловойРезультат = 2 + 3;
ТекстовыйРезультат = "Заказ №" + "15";
НоваяДата = '20240101' + 60;

Здесь один и тот же оператор +:

  • складывает числа;
  • конкатенирует строки;
  • сдвигает дату на число секунд.

Это и есть 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С:

  • не пытаемся ли мы смоделировать сложную иерархию там, где достаточно композиции;
  • не подменяем ли отсутствие явных интерфейсов хаотичным набором экспортных процедур;
  • оформлен ли контракт в ПрограммномИнтерфейсе так, чтобы им реально могли безопасно пользоваться другие подсистемы;
  • не используется ли наследование там, где достаточно композиции или стратегии;
  • не появляется ли хрупкая иерархия, где изменение базового поведения ломает все частные случаи;
  • не маскирует ли наследование обычное копирование кода под “архитектуру”.

Как читать этот раздел дальше

  1. Сначала понять базовый смысл принципа.
  2. Потом проверить, какую реальную проблему он решает в прикладной конфигурации.
  3. Только после этого переходить к более конкретным паттернам GOF и принципам GRASP.