При проектировании объектно-ориентированных систем (классов), важно соблюдать основополагающие принципы проектирования. К ним можно отнести список правил, составленных Робертом Мартином, которые известны под именем 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), принцип инверсии зависимостей. Зависимости внутри системы строятся на основе абстракций. Модули верхнего уровня не зависят от модулей нижнего уровня. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Далее мы рассмотрим каждый из принципов подробнее.
Принцип единственной обязанности
Формулировка: не должно быть более одной причины для изменения класса.
Приведенная картинка очень хорошо помогает представить суть данного принципа и проблему, которую он решает. Имея некий класс, решающий одну поставленную на него задачу, мы вкладываем в него еще и действия для него не предназначенные, тем самым усложняя наш инструмент – класс, как для сопровождения, так и для понимания. Кроме того, велика вероятность того, что меняя одну из обязанностей такого класса, мы затронем и другие. Рассмотрим пример класса, отправляющего почтовое сообщение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
CLASS lcl_mail_sender DEFINITION. PUBLIC SECTION. METHODS: mail_send IMPORTING iv_receiver TYPE STRING iv_theme TYPE STRING iv_attachment TYPE XSTRING. ENDCLASS. CLASS lcl_mail_sender IMPLEMENTATION. METHOD mail_send. DATA: lo_logger TYPE REF TO zcl_db_logger. " Отправка e-mail сообщения... " Формирование журнала CREATE OBJECT lo_logger EXPORTING iv_tabname = 'ZLOG' iv_logname = 'MYLOG'. lo_logger->write( 'Сообщение успешно отправлено' ). ENDMETHOD. ENDCLASS. |
Как видно внутри метода по отправке сообщения используется некий класс для логирования и записи лога по отправке в таблицу, т.е. метод в итоге выполняет два действия, первое он отправляет сообщение, а вторым он решает каким способом будет вестись журнал.
Если завтра нам потребуется изменить способ записи в журнал, нам будет нужно исправить класс отправки сообщений, хотя эти изменения не касаются исправления логики отправки сообщений.
Для предотвращения подобных проблем нужно вынести определение класса для ведения журнала на уровень выше, чтобы наш класс не решал подобную задачу. Исправленный код будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
CLASS lcl_mail_sender DEFINITION. PUBLIC SECTION. METHODS: constructor IMPORTING iv_logger TYPE REF TO zif_logger. mail_send IMPORTING iv_receiver TYPE STRING iv_theme TYPE STRING iv_attachment TYPE XSTRING. PRIVATE SECTION. DATA: mv_logger TYPE REF TO zif_logger. ENDCLASS. CLASS lcl_mail_sender IMPLEMENTATION. METHOD constructor. mv_logger = iv_logger. ENDMETHOD. METHOD mail_send. " Отправка e-mail сообщения... " Формирование журнала mv_logger->write( 'Сообщение успешно отправлено' ). ENDMETHOD. ENDCLASS. |
При создании экземпляра класса для отправки сообщений, мы будем передавать ему ссылку на класс поддерживающий интерфейс zif_logger, который содержит публичный метод write. Таким образом, класс отправки сообщений не будет знать каким способом ему нужно сделать запись в журнал, он просто её сделает.
Еще одним классическим примером нарушения данного принципа является проектирования класса, который внутри себя делает все что угодно, так называемого GOD OBJECT который может все, загружать файл, отправлять почту, проводить валидацию данных и еще кучу всевозможной бизнес-логики.
Принцип открытости/закрытости
Формулировка: классы должны быть открытыми для расширения и закрытыми для изменения.
При предъявлении новых требований к классам следует избегать их модификации, стараясь реализовывать возможность их расширения, код должен быть более гибким для расширения. Выгода в данном случае такова, что не потребуется пересматривать уже существующий код и тесты, написанные для него. Соответственно не потребуется тестировать заново всю функциональность.
Рассмотренный ранее пример класса для отправки сообщения и решение проблемы единственной обязанности так же является хорошим примером для демонстрации принципа открытости/закрытости. В первом варианте класс определял конкретный способ ведения журнала внутри себя, во втором варианте мы сделали класс открытым для расширения в части ведения журнала, после чего мы сможем вести запись в журнал каким-либо угодно способом, не влияя на уже проверенную логику отправки сообщений.
Рассмотрим другой пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
CLASS lcl_order DEFINITION ABSTRACT. ENDCLASS. CLASS lcl_sales_order DEFINITION INHERITING FROM lcl_order. ENDCLASS. CLASS lcl_purchase_order DEFINITION INHERITING FROM lcl_order. ENDCLASS. CLASS lcl_db_writer DEFINITION. PUBLIC SECTION. METHODS: save_order IMPORTING io_order TYPE REF TO lcl_order. ENDCLASS. CLASS lcl_sales_order IMPLEMENTATION. ENDCLASS. CLASS lcl_purchase_order IMPLEMENTATION. ENDCLASS. CLASS lcl_db_writer IMPLEMENTATION. METHOD save_order. DATA: lo_sales_order TYPE REF TO lcl_sales_order, lo_purchase_order TYPE REF TO lcl_purchase_order. TRY. lo_sales_order ?= io_order. " Логика сохранения сбытового заказа CATCH cx_sy_move_cast_error. ENDTRY. TRY. lo_purchase_order ?= io_order. " Логика сохранения закупочного заказа CATCH cx_sy_move_cast_error. ENDTRY. ENDMETHOD. ENDCLASS. START-OF-SELECTION. DATA: go_db_writer TYPE REF TO lcl_db_writer, go_sales_order TYPE REF TO lcl_sales_order. CREATE OBJECT go_db_writer. CREATE OBJECT go_sales_order. go_db_writer->save_order( go_sales_order ). |
В примере 4 класса: класс закупочного и сбытового заказа, унаследованные от базового абстрактного класса заказ и класс, сохраняющий все типы заказов в базу данных. Как видно из примера, при добавлении нового типа заказа нам потребуется вносить изменения в класс записи в БД, что является нарушением рассматриваемого принципа. Избежать подобной проблемы можно путем выноса логики сохранения в БД для каждого типа заказа:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
INTERFACE lif_db_save. METHODS: save. ENDINTERFACE. CLASS lcl_order DEFINITION ABSTRACT. PUBLIC SECTION. INTERFACES: lif_db_save. ALIASES save FOR lif_db_save~save. ENDCLASS. CLASS lcl_sales_order DEFINITION INHERITING FROM lcl_order. PUBLIC SECTION. METHODS: save REDEFINITION. ENDCLASS. CLASS lcl_purchase_order DEFINITION INHERITING FROM lcl_order. PUBLIC SECTION. METHODS: save REDEFINITION. ENDCLASS. CLASS lcl_sales_order IMPLEMENTATION. METHOD save. " Логика сохранения сбытового заказа ENDMETHOD. ENDCLASS. CLASS lcl_purchase_order IMPLEMENTATION. METHOD save. " Логика сохранения закупочного заказа ENDMETHOD. ENDCLASS. CLASS lcl_order IMPLEMENTATION. METHOD lif_db_save~save. ENDMETHOD. ENDCLASS. START-OF-SELECTION. DATA: go_sales_order TYPE REF TO lcl_sales_order. CREATE OBJECT go_sales_order. go_sales_order->save( ). |
Принцип замещения Лисков
Формулировка: наследуемый класс должен дополнять, а не замещать поведение базового класса.
Впервые этот принцип был упомянут Барбарой Лисков в 1987 году на научной конференции, посвященной объектно-ориентированному программированию.
Рассмотрим пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
CLASS lcl_rectangle DEFINITION. PUBLIC SECTION. METHODS: constructor IMPORTING iv_height TYPE i iv_width TYPE i, get_area RETURNING VALUE(rv_area) TYPE i, get_height RETURNING VALUE(rv_height) TYPE i, get_width RETURNING VALUE(rv_width) TYPE i. PROTECTED SECTION. DATA: mv_height TYPE i, mv_width TYPE i. ENDCLASS. CLASS lcl_square DEFINITION INHERITING FROM lcl_rectangle. PUBLIC SECTION. METHODS: get_width REDEFINITION. ENDCLASS. CLASS lcl_rectangle IMPLEMENTATION. METHOD constructor. mv_height = iv_height. mv_width = iv_width. ENDMETHOD. METHOD get_area. rv_area = get_height( ) * get_width( ). ENDMETHOD. METHOD get_height. rv_height = mv_height. ENDMETHOD. METHOD get_width. rv_width = mv_width. ENDMETHOD. ENDCLASS. CLASS lcl_square IMPLEMENTATION. METHOD get_width. rv_width = mv_height. ENDMETHOD. ENDCLASS. START-OF-SELECTION. DATA: go_rectangle TYPE REF TO lcl_rectangle, gv_area TYPE i. PERFORM get_shape CHANGING go_rectangle. gv_area = go_rectangle->get_area( ). WRITE gv_area. FORM get_shape CHANGING co_shape TYPE REF TO lcl_rectangle. DATA: lo_square TYPE REF TO lcl_square. CREATE OBJECT lo_square EXPORTING iv_height = 10 iv_width = 3. co_shape ?= lo_square. ENDFORM. |
В результате расчёта площади прямоугольника мы увидим значение 100, а не 30. В примере видно, что был создан квадрат, а не прямоугольник и площадь рассчитана верно, но представьте себе ситуацию, когда полученная ссылка на объект прямоугольника будет использована где-нибудь далеко от места создания квадрата, в лучшем случае нам потребуется отладка чтобы выяснить что работает не так.
Решение проблемы:
- Классы прямоугольник и квадрат должны быть разных типов, квадрат не должен быть унаследован от прямоугольника
- Класс квадрат должен агрегировать класс прямоугольник.
Отличный пример данного принципа рассмотрен здесь.
Принцип разделения интерфейса
Формулировка: много специализированных интерфейсов лучше, чем один универсальный.
Принцип разделения интерфейсов говорит о том, что слишком «толстые» интерфейсы необходимо разделять на более маленькие и специфические, чтобы клиенты маленьких интерфейсов знали только о методах, которые необходимы им в работе. В итоге, при изменении метода интерфейса не должны меняться клиенты, которые этот метод не используют.
Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
CLASS lcl_order DEFINITION ABSTRACT. PUBLIC SECTION. METHODS: set_supplier, " Установить поставщика set_customer. " Установить клиента. ENDCLASS. CLASS lcl_sales_order DEFINITION INHERITING FROM lcl_order. PUBLIC SECTION. METHODS: set_supplier, " Установить поставщика set_customer. " Установить клиента. ENDCLASS. CLASS lcl_purchase_order DEFINITION INHERITING FROM lcl_order. PUBLIC SECTION. METHODS: set_supplier, " Установить поставщика set_customer. " Установить клиента. ENDCLASS. CLASS lcl_sales_order IMPLEMENTATION. METHOD set_customer. ENDMETHOD. ENDCLASS. CLASS lcl_purchase_order IMPLEMENTATION. METHOD set_supplier. ENDMETHOD. ENDCLASS. CLASS lcl_order IMPLEMENTATION. METHOD set_supplier. ENDMETHOD. METHOD set_customer. ENDMETHOD. ENDCLASS. |
Как видно из примера базовый абстрактный класс «заказ» имеет два метода: установить поставщика, установить клиента. Первый метод будет использоваться в интерфейсе класса закупочного заказа, а второй в сбытовом заказе. При этом оба класса будут наследовать совершенно не нужные для их работы методы. Выходом из этой ситуации будет создание в иерархии наследования еще двух базовых абстрактных классов, один для заказов по закупке, другой для заказов по продаже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
CLASS lcl_order DEFINITION ABSTRACT. ENDCLASS. CLASS lcl_sales_abstract DEFINITION ABSTRACT INHERITING FROM lcl_order. PUBLIC SECTION. METHODS: set_customer ABSTRACT. " Установить клиента. ENDCLASS. CLASS lcl_purchase_abstract DEFINITION ABSTRACT INHERITING FROM lcl_order. PUBLIC SECTION. METHODS: set_supplier ABSTRACT. " Установить поставщика ENDCLASS. CLASS lcl_sales_order DEFINITION INHERITING FROM lcl_sales_abstract. PUBLIC SECTION. METHODS: set_customer REDEFINITION. " Установить клиента. ENDCLASS. CLASS lcl_purchase_order DEFINITION INHERITING FROM lcl_purchase_abstract. PUBLIC SECTION. METHODS: set_supplier REDEFINITION. " Установить поставщика ENDCLASS. CLASS lcl_sales_order IMPLEMENTATION. METHOD set_customer. ENDMETHOD. ENDCLASS. CLASS lcl_purchase_order IMPLEMENTATION. METHOD set_supplier. ENDMETHOD. ENDCLASS. CLASS lcl_order IMPLEMENTATION. ENDCLASS. |
В рассматриваемом примере, под интерфейсом понималось определение интерфейса класса (методов), но это так же справедливо и для отдельных интерфейсов.
Принцип инверсии зависимостей
Формулировка: модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Робертом Мартином были определены показатели качества дизайна, которые устраняются с применением Принципа инверсии зависимости:
- Жесткость. Изменение одного модуля ведет к изменению других модулей.
- Хрупкость. Изменения в одном модуле приводят к неконтролируемым ошибкам в других частях программы.
- Неподвижность. Модуль сложно отделить от остальной части приложения для повторного использования.
Рассмотрим пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
CLASS lcl_product DEFINITION. PUBLIC SECTION. DATA: mv_cost TYPE i, mv_name TYPE string, mv_count TYPE i. METHODS: constructor IMPORTING iv_cost TYPE i iv_name TYPE string iv_count TYPE i. ENDCLASS. "lcl_product DEFINITION CLASS lcl_warehouse DEFINITION. PUBLIC SECTION. TYPES: ty_t_products TYPE STANDARD TABLE OF REF TO lcl_product WITH DEFAULT KEY. METHODS: constructor, get_products RETURNING value(rt_products) TYPE ty_t_products. PRIVATE SECTION. DATA: mt_products TYPE ty_t_products. ENDCLASS. "lcl_warehouse DEFINITION CLASS lcl_discount_scheme DEFINITION. PUBLIC SECTION. METHODS: get_discount IMPORTING io_product TYPE REF TO lcl_product RETURNING VALUE(rv_discount) TYPE i. ENDCLASS. CLASS lcl_product_service DEFINITION. PUBLIC SECTION. METHODS: get_all_discounts RETURNING VALUE(rv_all_discounts) TYPE i. ENDCLASS. CLASS lcl_product_service IMPLEMENTATION. METHOD get_all_discounts. DATA: lo_warehouse TYPE REF TO lcl_warehouse, lo_discount_scheme TYPE REF TO lcl_discount_scheme, lt_products TYPE lcl_warehouse=>ty_t_products, lo_product TYPE REF TO lcl_product. CREATE OBJECT lo_warehouse. CREATE OBJECT lo_discount_scheme. lt_products = lo_warehouse->get_products( ). LOOP AT lt_products INTO lo_product. rv_all_discounts = rv_all_discounts + lo_discount_scheme->get_discount( lo_product ). ENDLOOP. ENDMETHOD. ENDCLASS. CLASS lcl_discount_scheme IMPLEMENTATION. METHOD get_discount. CASE io_product->mv_name. WHEN 'Мясо'. rv_discount = 1. WHEN 'Молоко'. rv_discount = 2. WHEN 'Творог'. rv_discount = 3. ENDCASE. ENDMETHOD. ENDCLASS. CLASS lcl_warehouse IMPLEMENTATION. METHOD get_products. rt_products = mt_products. ENDMETHOD. "get_products METHOD constructor. DATA: lo_product TYPE REF TO lcl_product. DEFINE add_product. create object lo_product exporting iv_cost = &1 iv_name = &2 iv_count = &3. append lo_product to mt_products. END-OF-DEFINITION. add_product 10 'Мясо' 10. add_product 20 'Молоко' 20. add_product 30 'Творог' 30. ENDMETHOD. "constructor ENDCLASS. "lcl_warehouse IMPLEMENTATION CLASS lcl_product IMPLEMENTATION. METHOD constructor. mv_cost = iv_cost. mv_name = iv_name. mv_count = iv_count. ENDMETHOD. "constructor ENDCLASS. "lcl_product IMPLEMENTATION START-OF-SELECTION. DATA: lo_product_service TYPE REF TO lcl_product_service, lv_all_discounts TYPE i. CREATE OBJECT lo_product_service. lv_all_discounts = lo_product_service->get_all_discounts( ). WRITE lv_all_discounts. |
Обратите внимание на класс lcl_product_service. Структура класса такова:
Пунктирными линиями указаны вызовы. lcl_product_service зависит от реализации lcl_warehouse и lcl_discount_scheme.
Такой дизайн не является гибким, т.к. по факту мы не можем без изменения lcl_product_service рассчитать скидку на товары, которые могут быть не только на складе. Так же нет возможности подсчитать скидку по другой карте скидок (с другим lcl_discount_scheme).
Согласно OCP и LSP нужно выделить использование реализаций lcl_warehouse и lcl_discount_scheme из lcl_product_service при помощи абстракций:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
CLASS lcl_product DEFINITION. PUBLIC SECTION. DATA: mv_cost TYPE i, mv_name TYPE string, mv_count TYPE i. METHODS: constructor IMPORTING iv_cost TYPE i iv_name TYPE string iv_count TYPE i. ENDCLASS. "lcl_product DEFINITION INTERFACE lif_product_storage. TYPES: ty_t_products TYPE STANDARD TABLE OF REF TO lcl_product WITH DEFAULT KEY. METHODS: get_products RETURNING VALUE(rt_products) TYPE ty_t_products. ENDINTERFACE. CLASS lcl_warehouse DEFINITION. PUBLIC SECTION. INTERFACES: lif_product_storage. ALIASES: get_products FOR lif_product_storage~get_products. METHODS: constructor. PRIVATE SECTION. DATA: mt_products TYPE lif_product_storage~ty_t_products. ENDCLASS. "lcl_warehouse DEFINITION INTERFACE lif_discount_scheme. METHODS: get_discount IMPORTING io_product TYPE REF TO lcl_product RETURNING VALUE(rv_discount) TYPE i. ENDINTERFACE. CLASS lcl_discount_scheme DEFINITION. PUBLIC SECTION. INTERFACES: lif_discount_scheme. ALIASES: get_discount FOR lif_discount_scheme~get_discount. ENDCLASS. CLASS lcl_product_service DEFINITION. PUBLIC SECTION. METHODS: get_all_discounts IMPORTING io_warehouse TYPE REF TO lif_product_storage io_discount_scheme TYPE REF TO lif_discount_scheme RETURNING VALUE(rv_all_discounts) TYPE i. ENDCLASS. CLASS lcl_product_service IMPLEMENTATION. METHOD get_all_discounts. DATA: lt_products TYPE lif_product_storage=>ty_t_products, lo_product TYPE REF TO lcl_product. lt_products = io_warehouse->get_products( ). LOOP AT lt_products INTO lo_product. rv_all_discounts = rv_all_discounts + io_discount_scheme->get_discount( lo_product ). ENDLOOP. ENDMETHOD. ENDCLASS. CLASS lcl_discount_scheme IMPLEMENTATION. METHOD lif_discount_scheme~get_discount. CASE io_product->mv_name. WHEN 'Мясо'. rv_discount = 1. WHEN 'Молоко'. rv_discount = 2. WHEN 'Творог'. rv_discount = 3. ENDCASE. ENDMETHOD. ENDCLASS. CLASS lcl_warehouse IMPLEMENTATION. METHOD lif_product_storage~get_products. rt_products = mt_products. ENDMETHOD. "get_products METHOD constructor. DATA: lo_product TYPE REF TO lcl_product. DEFINE add_product. create object lo_product exporting iv_cost = &1 iv_name = &2 iv_count = &3. append lo_product to mt_products. END-OF-DEFINITION. add_product 10 'Мясо' 10. add_product 20 'Молоко' 20. add_product 30 'Творог' 30. ENDMETHOD. "constructor ENDCLASS. "lcl_warehouse IMPLEMENTATION CLASS lcl_product IMPLEMENTATION. METHOD constructor. mv_cost = iv_cost. mv_name = iv_name. mv_count = iv_count. ENDMETHOD. "constructor ENDCLASS. "lcl_product IMPLEMENTATION START-OF-SELECTION. DATA: lo_product_service TYPE REF TO lcl_product_service, lo_warehouse TYPE REF TO lcl_warehouse, lo_discount_scheme TYPE REF TO lcl_discount_scheme, lv_all_discounts TYPE i. CREATE OBJECT lo_product_service. CREATE OBJECT lo_warehouse. CREATE OBJECT lo_discount_scheme. lv_all_discounts = lo_product_service->get_all_discounts( io_warehouse = lo_warehouse io_discount_scheme = lo_discount_scheme ). WRITE lv_all_discounts. |
Представление кода в UML:
Сплошные стрелки означают наследование. Обратите внимание на стрелки от lcl_warehouse и lcl_discount_scheme — они поменяли направление. Теперь от lcl_warehouse и lcl_discount_scheme ничего не зависит. Наоборот — они зависят от абстракций. Поэтому принцип так и назван — инверсии зависимости. Теперь мы можем передавать любую схему расчёта скидок, как и любой склад.
Список источников:
Круто, хорошая статья, спасибо =)
Пожалуйста)
Спасибо, оч.полезно!