Перейти до вмісту

Патерни проектування ООП

Патерн проектування (design pattern) — перевірене рішення типової проблеми дизайну класів. Цей довідник містить стислі ABAP-приклади для найуживаніших патернів Gang of Four, адаптованих під особливості ABAP Objects. Використовуй як шпаргалку-нагадування: “який патерн розвʼязує саме цю задачу”.

Правило номер один: не тягни патерн заради патерну. Якщо просте рішення достатнє — не ускладнюй.

Injection-патерни (для тестування)

Section titled “Injection-патерни (для тестування)”

Якщо клас має залежність (інший клас/інтерфейс), є кілька способів підсунути у нього test double у тесті.

Найкращий стандартний варіант — передавай залежність у конструктор:

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 ).
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.

Єдиний екземпляр на весь 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.

Як 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.
" Повертає кешований інстанс або створює новий

Клієнт не знає про конкретний клас — створення делегується фабриці:

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.

Фабрика, що створює сімейство повʼязаних обʼєктів:

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

Клієнт отримує фабрику і через неї будує узгоджений набір обʼєктів (одна тема, один стиль).

Покрокова побудова складного обʼєкта:

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( ).

Методи повертають 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.

Клонування існуючого обʼєкта замість створення з нуля. У 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.

Алгоритм як обʼєкт — клієнт вибирає, яку стратегію використати:

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.

Базовий клас задає скелет алгоритму, підкласи переозначають окремі кроки:

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 — каркас фіксований.

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.

Обгортка, що додає поведінку без зміни базового класу. Обгортка реалізує той самий інтерфейс і має посилання на оригінал:

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( ) ) ).

Переводить один інтерфейс у інший — коли треба підʼєднати чужий клас до свого контракту:

INTERFACE lif_logger.
METHODS log IMPORTING msg TYPE string.
ENDINTERFACE.
" Чужий клас із неконтрольованим API
CLASS 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.

Спрощений фасад над складною підсистемою:

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.

Ланцюг обробників — кожен або обробляє запит, або передає далі:

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 )

Інкапсулює виклик у обʼєкт — можна ставити у чергу, 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
" Кожен знає, у який стан перейти після дії, і що заборонено

Обхід колекції без оголення внутрішньої структури:

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 корисний.

Однаково працювати з листом і контейнером дерева:

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.

Розшарений незмінний стан — один екземпляр на ключ, багато посилань:

" Cache: пул спільних обʼєктів (наприклад, шрифти, деталі стилю)
CLASS-DATA pool TYPE HASHED TABLE OF ... WITH UNIQUE KEY key.

Обʼєкти спілкуються через центрального посередника, а не напряму — зменшує звʼязність:

CLASS lcl_form_mediator DEFINITION.
PUBLIC SECTION.
METHODS notify IMPORTING sender TYPE REF TO object event TYPE string.
ENDCLASS.

Форма-медіатор реагує на події своїх полів і координує їх (зміна одного поля → вмикання іншого).

Збереження/відновлення стану обʼєкта без порушення інкапсуляції:

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.

Операція над ієрархією без зміни класів ієрархії. Подвійний диспетчер:

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.

Шар доступу до БД за інтерфейсом — відокремлює бізнес-логіку від 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. Бізнес-логіка не знає, звідки дані.

ЗадачаПатерн
Кілька реалізацій одного контракту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). Повний перелік нюансів — в оригіналі.