SOLID в ABAP

При проектировании объектно-ориентированных систем (классов), важно соблюдать основополагающие принципы проектирования. К ним можно отнести список правил, составленных Робертом Мартином, которые известны под именем SOLID. SOLID это аббревиатура, где каждая из букв обозначает отдельное правило:

  • S — (Single responsibility principle — SRP), принцип единственной обязанности. На каждый класс должна быть возложена единственная обязанность.
  • O – (Open/closed principle — OCP), принцип открытости-закрытости. Программные сущности должны быть открыты для расширения, но закрыты для изменения.
  • L – (Liskov substitution principle — LSP), принцип подстановки Барбары Лисков. Объекты в программе могут быть заменены их наследниками без изменения свойств программы.
  • I – (Interface segregation principle — ISP), принцип разделения интерфейса. Много специализированных интерфейсов лучше, чем один универсальный.
  • D – (Dependency inversion principle — DIP), принцип инверсии зависимостей. Зависимости внутри системы строятся на основе абстракций. Модули верхнего уровня не зависят от модулей нижнего уровня. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Далее мы рассмотрим каждый из принципов подробнее.

Принцип единственной обязанности

godmode

Формулировка: не должно быть более одной причины для изменения класса.

Приведенная картинка очень хорошо помогает представить суть данного принципа и проблему, которую он решает. Имея некий класс, решающий одну поставленную на него задачу, мы вкладываем в него еще и действия для него не предназначенные, тем самым усложняя наш инструмент – класс, как для сопровождения, так и для понимания. Кроме того, велика вероятность того, что меняя одну из обязанностей такого класса, мы затронем и другие. Рассмотрим пример класса, отправляющего почтовое сообщение:

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

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

Для предотвращения подобных проблем нужно вынести определение класса для ведения журнала на уровень выше, чтобы наш класс не решал подобную задачу. Исправленный код будет выглядеть следующим образом:

При создании экземпляра класса для отправки сообщений, мы будем передавать ему ссылку на класс поддерживающий интерфейс zif_logger, который содержит публичный метод write. Таким образом, класс отправки сообщений не будет знать каким способом ему нужно сделать запись в журнал, он просто её сделает.

Еще одним классическим примером нарушения данного принципа является проектирования класса, который внутри себя делает все что угодно, так называемого GOD OBJECT который может все, загружать файл, отправлять почту, проводить валидацию данных и еще кучу всевозможной бизнес-логики.

Принцип открытости/закрытости

Формулировка: классы должны быть открытыми для расширения и закрытыми для изменения.

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

Рассмотренный ранее пример класса для отправки сообщения и решение проблемы единственной обязанности так же является хорошим примером для демонстрации принципа открытости/закрытости. В первом варианте класс определял конкретный способ ведения журнала внутри себя, во втором варианте мы сделали класс открытым для расширения в части ведения журнала, после чего мы сможем вести запись в журнал каким-либо угодно способом, не влияя на уже проверенную логику отправки сообщений.

Рассмотрим другой пример:

В примере 4 класса: класс закупочного и сбытового заказа, унаследованные от базового абстрактного класса заказ и класс, сохраняющий все типы заказов в базу данных. Как видно из примера, при добавлении нового типа заказа нам потребуется вносить изменения в класс записи в БД, что является нарушением рассматриваемого принципа. Избежать подобной проблемы можно путем выноса логики сохранения в БД для каждого типа заказа:

Принцип замещения Лисков

liskov

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

Впервые этот принцип был упомянут Барбарой Лисков в 1987 году на научной конференции, посвященной объектно-ориентированному программированию.

Рассмотрим пример:

В результате расчёта площади прямоугольника мы увидим значение 100, а не 30.  В примере видно, что был создан квадрат, а не прямоугольник и площадь рассчитана верно, но представьте себе ситуацию, когда полученная ссылка на объект прямоугольника будет использована где-нибудь далеко от места создания квадрата, в лучшем случае нам потребуется отладка чтобы выяснить что работает не так.

Решение проблемы:

  • Классы прямоугольник и квадрат должны быть разных типов, квадрат не должен быть унаследован от прямоугольника
  • Класс квадрат должен агрегировать класс прямоугольник.

Отличный пример данного принципа рассмотрен здесь.

Принцип разделения интерфейса

Формулировка: много специализированных интерфейсов лучше, чем один универсальный.

Принцип разделения интерфейсов говорит о том, что слишком «толстые» интерфейсы необходимо разделять на более маленькие и специфические, чтобы клиенты маленьких интерфейсов знали только о методах, которые необходимы им в работе. В итоге, при изменении метода интерфейса не должны меняться клиенты, которые этот метод не используют.

Пример:

Как видно из примера базовый абстрактный класс «заказ» имеет два метода: установить поставщика, установить клиента. Первый метод будет использоваться в интерфейсе класса закупочного заказа, а второй в сбытовом заказе. При этом оба класса будут наследовать совершенно не нужные для их работы методы. Выходом из этой ситуации будет создание в иерархии наследования еще двух базовых абстрактных классов, один для заказов по закупке, другой для заказов по продаже:

В рассматриваемом примере, под интерфейсом понималось определение интерфейса класса (методов), но это так же справедливо и для отдельных интерфейсов.

Принцип инверсии зависимостей

Формулировка: модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Робертом Мартином были определены показатели качества дизайна, которые устраняются с применением Принципа инверсии зависимости:

  • Жесткость. Изменение одного модуля ведет к изменению других модулей.
  • Хрупкость. Изменения в одном модуле приводят к неконтролируемым ошибкам в других частях программы.
  • Неподвижность. Модуль сложно отделить от остальной части приложения для повторного использования.

Рассмотрим пример:

Обратите внимание на класс lcl_product_service. Структура класса такова:

INVER1

Пунктирными линиями указаны вызовы. lcl_product_service зависит от реализации lcl_warehouse и lcl_discount_scheme.

Такой дизайн не является гибким, т.к. по факту мы не можем без изменения lcl_product_service рассчитать скидку на товары, которые могут быть не только на складе. Так же нет возможности подсчитать скидку по другой карте скидок (с другим lcl_discount_scheme).

Согласно OCP и LSP нужно выделить использование реализаций lcl_warehouse и lcl_discount_scheme из lcl_product_service при помощи абстракций:

Представление кода в UML:

inver2

Сплошные стрелки означают наследование. Обратите внимание на стрелки от lcl_warehouse и lcl_discount_scheme — они поменяли направление. Теперь от lcl_warehouse и lcl_discount_scheme ничего не зависит. Наоборот — они зависят от абстракций. Поэтому принцип так и назван — инверсии зависимости. Теперь мы можем передавать любую схему расчёта скидок, как и любой склад.

Список источников: