Патерни проектування ООП
Патерн проектування (design pattern) — перевірене рішення типової проблеми дизайну класів. Цей довідник містить стислі ABAP-приклади для найуживаніших патернів Gang of Four, адаптованих під особливості ABAP Objects. Використовуй як шпаргалку-нагадування: “який патерн розвʼязує саме цю задачу”.
Правило номер один: не тягни патерн заради патерну. Якщо просте рішення достатнє — не ускладнюй.
Injection-патерни (для тестування)
Section titled “Injection-патерни (для тестування)”Якщо клас має залежність (інший клас/інтерфейс), є кілька способів підсунути у нього test double у тесті.
Constructor injection
Section titled “Constructor injection”Найкращий стандартний варіант — передавай залежність у конструктор:
CLASS zcl_service DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. METHODS constructor IMPORTING repo TYPE REF TO lif_repository. PRIVATE SECTION. DATA repo TYPE REF TO lif_repository.ENDCLASS.У тесті — передаєш мок:
DATA(fake_repo) = NEW ltc_fake_repo( ).DATA(sut) = NEW zcl_service( fake_repo ).Setter injection
Section titled “Setter injection”METHODS set_repo IMPORTING repo TYPE REF TO lif_repository.Корисно, коли залежність опційна або змінюється в runtime.
Backdoor injection — через LOCAL FRIENDS
Section titled “Backdoor injection — через LOCAL FRIENDS”Коли поле private і setter-а нема — тестовий клас робиться friend:
CLASS zcl_service DEFINITION PUBLIC FINAL CREATE PUBLIC GLOBAL FRIENDS zcl_service_test. " або LOCAL FRIENDS для локальних
CLASS ltc_test DEFINITION FOR TESTING ... . PRIVATE SECTION. METHODS setup.ENDCLASS.CLASS ltc_test IMPLEMENTATION. METHOD setup. sut = NEW zcl_service( ... ). sut->repo = NEW fake_repo( ). " прямий запис у private ENDMETHOD.ENDCLASS.Test double через спадкування
Section titled “Test double через спадкування”Якщо замокати потрібно public-метод — підкласс з REDEFINITION:
CLASS ltc_fake_service DEFINITION INHERITING FROM zcl_service FOR TESTING. PUBLIC SECTION. METHODS send REDEFINITION.ENDCLASS.CLASS ltc_fake_service IMPLEMENTATION. METHOD send. last_call = message. " записуємо виклик замість справжньої дії ENDMETHOD.ENDCLASS.Singleton
Section titled “Singleton”Єдиний екземпляр на весь runtime. CREATE PRIVATE + static factory + static holder:
CLASS zcl_config DEFINITION PUBLIC FINAL CREATE PRIVATE. PUBLIC SECTION. CLASS-METHODS get_instance RETURNING VALUE(r) TYPE REF TO zcl_config. METHODS get_value IMPORTING key TYPE string RETURNING VALUE(val) TYPE string. PRIVATE SECTION. CLASS-DATA instance TYPE REF TO zcl_config. METHODS constructor.ENDCLASS.
CLASS zcl_config IMPLEMENTATION. METHOD get_instance. IF instance IS INITIAL. instance = NEW #( ). ENDIF. r = instance. ENDMETHOD.
METHOD constructor. " завантаження конфігурації ENDMETHOD.
METHOD get_value. ENDMETHOD.ENDCLASS.Multiton
Section titled “Multiton”Як singleton, але кілька іменованих екземплярів:
CLASS-DATA instances TYPE HASHED TABLE OF REF TO zcl_cache WITH UNIQUE KEY table_line.
CLASS-METHODS get_instance IMPORTING name TYPE string RETURNING VALUE(r) TYPE REF TO zcl_cache." Повертає кешований інстанс або створює новийFactory Method
Section titled “Factory Method”Клієнт не знає про конкретний клас — створення делегується фабриці:
INTERFACE lif_parser. METHODS parse IMPORTING text TYPE string RETURNING VALUE(r) TYPE REF TO data.ENDINTERFACE.
CLASS lcl_json_parser DEFINITION. PUBLIC SECTION. INTERFACES lif_parser.ENDCLASS.CLASS lcl_xml_parser DEFINITION. PUBLIC SECTION. INTERFACES lif_parser.ENDCLASS.
CLASS lcl_parser_factory DEFINITION. PUBLIC SECTION. CLASS-METHODS create IMPORTING format TYPE string RETURNING VALUE(r) TYPE REF TO lif_parser.ENDCLASS.
CLASS lcl_parser_factory IMPLEMENTATION. METHOD create. CASE format. WHEN `json`. r = NEW lcl_json_parser( ). WHEN `xml`. r = NEW lcl_xml_parser( ). WHEN OTHERS. RAISE EXCEPTION TYPE zcx_unknown_format. ENDCASE. ENDMETHOD.ENDCLASS.Abstract Factory
Section titled “Abstract Factory”Фабрика, що створює сімейство повʼязаних обʼєктів:
INTERFACE lif_ui_factory. METHODS: create_button RETURNING VALUE(b) TYPE REF TO lif_button, create_input RETURNING VALUE(i) TYPE REF TO lif_input.ENDINTERFACE.
" Конкретні фабрики — lcl_dark_theme_factory, lcl_light_theme_factoryКлієнт отримує фабрику і через неї будує узгоджений набір обʼєктів (одна тема, один стиль).
Builder
Section titled “Builder”Покрокова побудова складного обʼєкта:
CLASS lcl_query_builder DEFINITION. PUBLIC SECTION. METHODS: from IMPORTING tab TYPE string RETURNING VALUE(r) TYPE REF TO lcl_query_builder, where IMPORTING cond TYPE string RETURNING VALUE(r) TYPE REF TO lcl_query_builder, order_by IMPORTING cols TYPE string RETURNING VALUE(r) TYPE REF TO lcl_query_builder, build RETURNING VALUE(sql) TYPE string.ENDCLASS.
" Використання з method chaining (fluent interface)DATA(sql) = NEW lcl_query_builder( )->from( `mara` ) ->where( `matnr LIKE ''A%''` ) ->order_by( `matnr` ) ->build( ).Fluent Interface
Section titled “Fluent Interface”Методи повертають me — виклики чейняться:
METHODS set_x IMPORTING x TYPE i RETURNING VALUE(r) TYPE REF TO lcl_config.METHOD set_x. me->x = x. r = me.ENDMETHOD.Prototype
Section titled “Prototype”Клонування існуючого обʼєкта замість створення з нуля. У ABAP — часто через CL_ABAP_TYPEDESCR + CORRESPONDING або серіалізацію:
METHODS clone RETURNING VALUE(r) TYPE REF TO lcl_doc.METHOD clone. r = NEW #( ). r->data = CORRESPONDING #( me->data ).ENDMETHOD.Strategy
Section titled “Strategy”Алгоритм як обʼєкт — клієнт вибирає, яку стратегію використати:
INTERFACE lif_discount. METHODS calc IMPORTING amount TYPE p RETURNING VALUE(r) TYPE p.ENDINTERFACE.
CLASS lcl_no_discount DEFINITION. PUBLIC SECTION. INTERFACES lif_discount.ENDCLASS.CLASS lcl_ten_percent DEFINITION. PUBLIC SECTION. INTERFACES lif_discount.ENDCLASS.
CLASS lcl_cart DEFINITION. PUBLIC SECTION. METHODS: constructor IMPORTING discount TYPE REF TO lif_discount, total RETURNING VALUE(r) TYPE p. PRIVATE SECTION. DATA discount TYPE REF TO lif_discount.ENDCLASS.Зміна стратегії — підсунути інший екземпляр без правки lcl_cart.
Template Method
Section titled “Template Method”Базовий клас задає скелет алгоритму, підкласи переозначають окремі кроки:
CLASS lcl_report DEFINITION ABSTRACT. PUBLIC SECTION. METHODS run FINAL. " скелет — не змінюваний PROTECTED SECTION. METHODS: load ABSTRACT, compute ABSTRACT, render ABSTRACT.ENDCLASS.
CLASS lcl_report IMPLEMENTATION. METHOD run. load( ). compute( ). render( ). ENDMETHOD.ENDCLASS.Підклас реалізує load, compute, render — каркас фіксований.
Observer
Section titled “Observer”Publisher-subscriber через вбудовані ABAP events:
CLASS lcl_subject DEFINITION. PUBLIC SECTION. EVENTS changed EXPORTING VALUE(new_value) TYPE string. METHODS set_value IMPORTING v TYPE string. PRIVATE SECTION. DATA value TYPE string.ENDCLASS.
CLASS lcl_subject IMPLEMENTATION. METHOD set_value. value = v. RAISE EVENT changed EXPORTING new_value = v. ENDMETHOD.ENDCLASS.
CLASS lcl_observer DEFINITION. PUBLIC SECTION. METHODS on_change FOR EVENT changed OF lcl_subject IMPORTING new_value.ENDCLASS.
" ПідпискаDATA(s) = NEW lcl_subject( ).DATA(o) = NEW lcl_observer( ).SET HANDLER o->on_change FOR s.Decorator
Section titled “Decorator”Обгортка, що додає поведінку без зміни базового класу. Обгортка реалізує той самий інтерфейс і має посилання на оригінал:
INTERFACE lif_writer. METHODS write IMPORTING text TYPE string.ENDINTERFACE.
CLASS lcl_file_writer DEFINITION. PUBLIC SECTION. INTERFACES lif_writer.ENDCLASS.
CLASS lcl_timestamp_writer DEFINITION. PUBLIC SECTION. INTERFACES lif_writer. METHODS constructor IMPORTING inner TYPE REF TO lif_writer. PRIVATE SECTION. DATA inner TYPE REF TO lif_writer.ENDCLASS.
CLASS lcl_timestamp_writer IMPLEMENTATION. METHOD constructor. me->inner = inner. ENDMETHOD. METHOD lif_writer~write. inner->write( |{ sy-datum } { sy-uzeit } - { text }| ). ENDMETHOD.ENDCLASS.
" ОбгортаємоDATA(w) = CAST lif_writer( NEW lcl_timestamp_writer( inner = NEW lcl_file_writer( ) ) ).Adapter
Section titled “Adapter”Переводить один інтерфейс у інший — коли треба підʼєднати чужий клас до свого контракту:
INTERFACE lif_logger. METHODS log IMPORTING msg TYPE string.ENDINTERFACE.
" Чужий клас із неконтрольованим APICLASS lcl_legacy_logger DEFINITION. PUBLIC SECTION. METHODS write_log IMPORTING text TYPE string severity TYPE c.ENDCLASS.
" АдаптерCLASS lcl_legacy_adapter DEFINITION. PUBLIC SECTION. INTERFACES lif_logger. METHODS constructor IMPORTING legacy TYPE REF TO lcl_legacy_logger. PRIVATE SECTION. DATA legacy TYPE REF TO lcl_legacy_logger.ENDCLASS.
CLASS lcl_legacy_adapter IMPLEMENTATION. METHOD constructor. me->legacy = legacy. ENDMETHOD. METHOD lif_logger~log. legacy->write_log( text = msg severity = 'I' ). ENDMETHOD.ENDCLASS.Facade
Section titled “Facade”Спрощений фасад над складною підсистемою:
CLASS lcl_order_facade DEFINITION. PUBLIC SECTION. METHODS place_order IMPORTING customer TYPE i items TYPE ty_items. PRIVATE SECTION. DATA: inventory TYPE REF TO lif_inventory, payment TYPE REF TO lif_payment, shipping TYPE REF TO lif_shipping.ENDCLASS.
CLASS lcl_order_facade IMPLEMENTATION. METHOD place_order. inventory->reserve( items ). payment->charge( customer ). shipping->schedule( customer ). ENDMETHOD.ENDCLASS.Клієнт викликає place_order( ) замість трьох сервісів.
Обʼєкт-заступник з тим самим інтерфейсом — для ліньки, кешу, перевірок авторизації:
CLASS lcl_cached_repo DEFINITION. PUBLIC SECTION. INTERFACES lif_repository. METHODS constructor IMPORTING real TYPE REF TO lif_repository. PRIVATE SECTION. DATA: real TYPE REF TO lif_repository, cache TYPE HASHED TABLE OF ... .ENDCLASS.
METHOD lif_repository~get. READ TABLE cache WITH KEY id = id ASSIGNING FIELD-SYMBOL(<c>). IF sy-subrc = 0. r = <c>-val. ELSE. r = real->get( id ). INSERT VALUE #( id = id val = r ) INTO TABLE cache. ENDIF.ENDMETHOD.Chain of Responsibility
Section titled “Chain of Responsibility”Ланцюг обробників — кожен або обробляє запит, або передає далі:
CLASS lcl_handler DEFINITION ABSTRACT. PUBLIC SECTION. METHODS: set_next IMPORTING h TYPE REF TO lcl_handler, handle IMPORTING req TYPE string. PROTECTED SECTION. DATA next TYPE REF TO lcl_handler.ENDCLASS.
" Конкретний handler: якщо не свій — передає next->handle( req )Command
Section titled “Command”Інкапсулює виклик у обʼєкт — можна ставити у чергу, undo, логувати:
INTERFACE lif_command. METHODS: execute, undo.ENDINTERFACE.
CLASS lcl_add_item DEFINITION. PUBLIC SECTION. INTERFACES lif_command. METHODS constructor IMPORTING cart TYPE REF TO lcl_cart item TYPE ty_item. PRIVATE SECTION. DATA: cart TYPE REF TO lcl_cart, item TYPE ty_item.ENDCLASS.Поведінка залежить від поточного стану; стан — окремий обʼєкт, що може змінюватись:
INTERFACE lif_order_state. METHODS: pay IMPORTING order TYPE REF TO lcl_order, ship IMPORTING order TYPE REF TO lcl_order, cancel IMPORTING order TYPE REF TO lcl_order.ENDINTERFACE.
" Конкретні стани: lcl_new_state, lcl_paid_state, lcl_shipped_state" Кожен знає, у який стан перейти після дії, і що забороненоIterator
Section titled “Iterator”Обхід колекції без оголення внутрішньої структури:
INTERFACE lif_iterator. METHODS: has_next RETURNING VALUE(r) TYPE abap_bool, next RETURNING VALUE(r) TYPE REF TO data.ENDINTERFACE.У ABAP зазвичай — просто LOOP AT, але коли колекція складна (дерево, віддалене джерело) — iterator корисний.
Composite
Section titled “Composite”Однаково працювати з листом і контейнером дерева:
INTERFACE lif_component. METHODS render RETURNING VALUE(html) TYPE string.ENDINTERFACE.
CLASS lcl_leaf DEFINITION. PUBLIC SECTION. INTERFACES lif_component. ENDCLASS.CLASS lcl_box DEFINITION. PUBLIC SECTION. INTERFACES lif_component. METHODS add IMPORTING c TYPE REF TO lif_component. PRIVATE SECTION. DATA children TYPE STANDARD TABLE OF REF TO lif_component.ENDCLASS.
METHOD lif_component~render. LOOP AT children INTO DATA(c). html &&= c->render( ). ENDLOOP.ENDMETHOD.Flyweight
Section titled “Flyweight”Розшарений незмінний стан — один екземпляр на ключ, багато посилань:
" Cache: пул спільних обʼєктів (наприклад, шрифти, деталі стилю)CLASS-DATA pool TYPE HASHED TABLE OF ... WITH UNIQUE KEY key.Mediator
Section titled “Mediator”Обʼєкти спілкуються через центрального посередника, а не напряму — зменшує звʼязність:
CLASS lcl_form_mediator DEFINITION. PUBLIC SECTION. METHODS notify IMPORTING sender TYPE REF TO object event TYPE string.ENDCLASS.Форма-медіатор реагує на події своїх полів і координує їх (зміна одного поля → вмикання іншого).
Memento
Section titled “Memento”Збереження/відновлення стану обʼєкта без порушення інкапсуляції:
CLASS lcl_editor_memento DEFINITION FRIENDS lcl_editor. PRIVATE SECTION. DATA snapshot TYPE ty_state.ENDCLASS.
CLASS lcl_editor DEFINITION. PUBLIC SECTION. METHODS: save RETURNING VALUE(m) TYPE REF TO lcl_editor_memento, restore IMPORTING m TYPE REF TO lcl_editor_memento.ENDCLASS.Visitor
Section titled “Visitor”Операція над ієрархією без зміни класів ієрархії. Подвійний диспетчер:
INTERFACE lif_visitor. METHODS: visit_circle IMPORTING s TYPE REF TO lcl_circle, visit_rectangle IMPORTING s TYPE REF TO lcl_rectangle.ENDINTERFACE.
INTERFACE lif_shape. METHODS accept IMPORTING v TYPE REF TO lif_visitor.ENDINTERFACE.
METHOD accept. " у lcl_circle v->visit_circle( me ).ENDMETHOD.Новий алгоритм — новий lif_visitor-імплементатор, без правки lcl_circle/lcl_rectangle.
Data Access Object (DAO)
Section titled “Data Access Object (DAO)”Шар доступу до БД за інтерфейсом — відокремлює бізнес-логіку від SQL:
INTERFACE lif_user_dao. METHODS: get_by_id IMPORTING id TYPE i RETURNING VALUE(r) TYPE ty_user, save IMPORTING user TYPE ty_user, delete IMPORTING id TYPE i.ENDINTERFACE.Конкретні реалізації — для SQL, для тест-пам’яті, для REST-API. Бізнес-логіка не знає, звідки дані.
Коли який патерн
Section titled “Коли який патерн”| Задача | Патерн |
|---|---|
| Кілька реалізацій одного контракту | Strategy |
| Одне місце створення — багато залежать | Factory Method / Abstract Factory |
| Скелет з переозначуваними кроками | Template Method |
| Додати поведінку без зміни класу | Decorator |
| Підʼєднати чужий API до свого контракту | Adapter |
| Спростити взаємодію зі складною підсистемою | Facade |
| Сповістити множину обʼєктів про подію | Observer |
| Дерево з листами і контейнерами | Composite |
| Дії як обʼєкти (undo, queue, log) | Command |
| Кеш / ліньки / контроль доступу | Proxy |
| Складна побудова обʼєкта по кроках | Builder |
| Глобальна точка доступу | Singleton (обережно) |
| Відокремити БД від логіки | DAO |
Адаптовано з 34_OO_Design_Patterns.md (Apache 2.0). Повний перелік нюансів — в оригіналі.