Design Patterns pour RAP : modeles d architecture eprouves

Catégorie
RAP
Publié
Auteur
Johannes

Les Design Patterns sont des modeles de solution eprouves pour des problemes recurrents dans le developpement logiciel. Dans le contexte de RAP (RESTful ABAP Programming), ils aident a developper des Business Objects flexibles, maintenables et extensibles.

Pourquoi les Design Patterns dans RAP ?

DefiDesign PatternAvantage
Creation d’objets differenteFactoryDecouplage de l’implementation
Algorithmes changeantsStrategyLogique metier interchangeable
Structure de processus communeTemplate MethodCadre reutilisable
Sous-systemes complexesFacadeInterface simplifiee

Pattern Factory

Le Pattern Factory encapsule la creation d’objets et permet de determiner l’implementation concrete a l’execution. Dans RAP, cela est particulierement utile pour :

  • Differentes logiques de calcul selon le contexte metier
  • Implementations specifiques au mandant ou au pays
  • Testabilite par remplacement d’implementations de production par des mocks

Scenario : Calcul de prix par region

" Interface pour le calcul de prix
INTERFACE zif_price_calculator.
METHODS calculate_price
IMPORTING
iv_base_price TYPE decfloat34
iv_quantity TYPE i
RETURNING
VALUE(rv_result) TYPE decfloat34.
ENDINTERFACE.
" Implementation pour la France
CLASS zcl_price_calc_fr DEFINITION.
PUBLIC SECTION.
INTERFACES zif_price_calculator.
ENDCLASS.
CLASS zcl_price_calc_fr IMPLEMENTATION.
METHOD zif_price_calculator~calculate_price.
" TVA francaise : 20%
rv_result = iv_base_price * iv_quantity * '1.20'.
ENDMETHOD.
ENDCLASS.
" Implementation pour la Suisse
CLASS zcl_price_calc_ch DEFINITION.
PUBLIC SECTION.
INTERFACES zif_price_calculator.
ENDCLASS.
CLASS zcl_price_calc_ch IMPLEMENTATION.
METHOD zif_price_calculator~calculate_price.
" TVA suisse : 8.1%
rv_result = iv_base_price * iv_quantity * '1.081'.
ENDMETHOD.
ENDCLASS.

Classe Factory

CLASS zcl_price_calculator_factory DEFINITION
PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS get_instance
IMPORTING
iv_country_code TYPE land1
RETURNING
VALUE(ro_calculator) TYPE REF TO zif_price_calculator.
PRIVATE SECTION.
CLASS-DATA gt_instances TYPE HASHED TABLE OF REF TO zif_price_calculator
WITH UNIQUE KEY table_line.
ENDCLASS.
CLASS zcl_price_calculator_factory IMPLEMENTATION.
METHOD get_instance.
CASE iv_country_code.
WHEN 'FR'.
ro_calculator = NEW zcl_price_calc_fr( ).
WHEN 'CH'.
ro_calculator = NEW zcl_price_calc_ch( ).
WHEN 'BE'.
ro_calculator = NEW zcl_price_calc_be( ).
WHEN OTHERS.
ro_calculator = NEW zcl_price_calc_default( ).
ENDCASE.
ENDMETHOD.
ENDCLASS.

Utilisation dans l’implementation du Behavior RAP

CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS calculate_total FOR DETERMINE ON SAVE
IMPORTING keys FOR Order~calculateTotal.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD calculate_total.
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
FIELDS ( CountryCode BasePrice Quantity )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
" La Factory fournit la bonne implementation
DATA(lo_calculator) = zcl_price_calculator_factory=>get_instance(
iv_country_code = ls_order-CountryCode ).
DATA(lv_total) = lo_calculator->calculate_price(
iv_base_price = ls_order-BasePrice
iv_quantity = ls_order-Quantity ).
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( TotalPrice )
WITH VALUE #( ( %tky = ls_order-%tky
TotalPrice = lv_total ) ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Pattern Strategy

Le Pattern Strategy permet d’echanger des algorithmes a l’execution. Contrairement au Pattern Factory, qui encapsule la creation, Strategy encapsule le comportement.

Scenario : Differentes strategies de remise

" Interface Strategy
INTERFACE zif_discount_strategy.
METHODS apply_discount
IMPORTING
iv_price TYPE decfloat34
is_customer TYPE zcustomer
RETURNING
VALUE(rv_result) TYPE decfloat34.
ENDINTERFACE.
" Strategie sans remise
CLASS zcl_no_discount DEFINITION.
PUBLIC SECTION.
INTERFACES zif_discount_strategy.
ENDCLASS.
CLASS zcl_no_discount IMPLEMENTATION.
METHOD zif_discount_strategy~apply_discount.
rv_result = iv_price.
ENDMETHOD.
ENDCLASS.
" Strategie de remise en pourcentage
CLASS zcl_percentage_discount DEFINITION.
PUBLIC SECTION.
INTERFACES zif_discount_strategy.
METHODS constructor
IMPORTING iv_percentage TYPE decfloat34.
PRIVATE SECTION.
DATA mv_percentage TYPE decfloat34.
ENDCLASS.
CLASS zcl_percentage_discount IMPLEMENTATION.
METHOD constructor.
mv_percentage = iv_percentage.
ENDMETHOD.
METHOD zif_discount_strategy~apply_discount.
rv_result = iv_price * ( 1 - mv_percentage / 100 ).
ENDMETHOD.
ENDCLASS.
" Remise client VIP avec echelonnement
CLASS zcl_vip_discount DEFINITION.
PUBLIC SECTION.
INTERFACES zif_discount_strategy.
ENDCLASS.
CLASS zcl_vip_discount IMPLEMENTATION.
METHOD zif_discount_strategy~apply_discount.
" Remise echelonnee basee sur le CA client
CASE is_customer-customer_tier.
WHEN 'GOLD'.
rv_result = iv_price * '0.85'. " 15% de remise
WHEN 'SILVER'.
rv_result = iv_price * '0.90'. " 10% de remise
WHEN 'BRONZE'.
rv_result = iv_price * '0.95'. " 5% de remise
WHEN OTHERS.
rv_result = iv_price.
ENDCASE.
ENDMETHOD.
ENDCLASS.

Classe Context avec Strategy

CLASS zcl_order_processor DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor
IMPORTING
io_discount_strategy TYPE REF TO zif_discount_strategy.
METHODS set_discount_strategy
IMPORTING
io_discount_strategy TYPE REF TO zif_discount_strategy.
METHODS calculate_final_price
IMPORTING
iv_price TYPE decfloat34
is_customer TYPE zcustomer
RETURNING
VALUE(rv_result) TYPE decfloat34.
PRIVATE SECTION.
DATA mo_discount_strategy TYPE REF TO zif_discount_strategy.
ENDCLASS.
CLASS zcl_order_processor IMPLEMENTATION.
METHOD constructor.
mo_discount_strategy = io_discount_strategy.
ENDMETHOD.
METHOD set_discount_strategy.
mo_discount_strategy = io_discount_strategy.
ENDMETHOD.
METHOD calculate_final_price.
rv_result = mo_discount_strategy->apply_discount(
iv_price = iv_price
is_customer = is_customer ).
ENDMETHOD.
ENDCLASS.

Utilisation dans une RAP Action

CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS apply_campaign_discount FOR MODIFY
IMPORTING keys FOR ACTION SalesOrder~applyCampaignDiscount
RESULT result.
ENDCLASS.
CLASS lhc_salesorder IMPLEMENTATION.
METHOD apply_campaign_discount.
READ ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
" Choisir la Strategy basee sur la campagne active
DATA lo_strategy TYPE REF TO zif_discount_strategy.
IF ls_order-CampaignCode = 'SUMMER25'.
lo_strategy = NEW zcl_percentage_discount( 25 ).
ELSEIF ls_order-CustomerTier IS NOT INITIAL.
lo_strategy = NEW zcl_vip_discount( ).
ELSE.
lo_strategy = NEW zcl_no_discount( ).
ENDIF.
DATA(lo_processor) = NEW zcl_order_processor( lo_strategy ).
DATA(lv_final_price) = lo_processor->calculate_final_price(
iv_price = ls_order-GrossPrice
is_customer = CORRESPONDING #( ls_order ) ).
MODIFY ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( FinalPrice )
WITH VALUE #( ( %tky = ls_order-%tky
FinalPrice = lv_final_price ) ).
ENDLOOP.
result = VALUE #( FOR order IN lt_orders
( %tky = order-%tky
%param = CORRESPONDING #( order ) ) ).
ENDMETHOD.
ENDCLASS.

Pattern Template Method

Le Pattern Template Method definit la structure de base d’un algorithme dans une classe de base, tandis que les sous-classes redefinissent certaines etapes. Le flux reste le meme, les details varient.

Scenario : Generation de documents avec differents formats

" Classe de base abstraite avec Template Method
CLASS zcl_document_generator DEFINITION
PUBLIC ABSTRACT CREATE PUBLIC.
PUBLIC SECTION.
" Template Method - definit le flux
METHODS generate_document FINAL
IMPORTING
is_order TYPE zorder
RETURNING
VALUE(rv_document) TYPE string.
PROTECTED SECTION.
" Methodes hook - implementees par les sous-classes
METHODS get_header ABSTRACT
IMPORTING is_order TYPE zorder
RETURNING VALUE(rv_header) TYPE string.
METHODS get_line_items ABSTRACT
IMPORTING is_order TYPE zorder
RETURNING VALUE(rv_items) TYPE string.
METHODS get_footer ABSTRACT
IMPORTING is_order TYPE zorder
RETURNING VALUE(rv_footer) TYPE string.
METHODS format_output ABSTRACT
IMPORTING iv_content TYPE string
RETURNING VALUE(rv_formatted) TYPE string.
ENDCLASS.
CLASS zcl_document_generator IMPLEMENTATION.
METHOD generate_document.
" La structure de l'algorithme est fixe
DATA(lv_header) = get_header( is_order ).
DATA(lv_items) = get_line_items( is_order ).
DATA(lv_footer) = get_footer( is_order ).
DATA(lv_content) = |{ lv_header }{ lv_items }{ lv_footer }|.
rv_document = format_output( lv_content ).
ENDMETHOD.
ENDCLASS.

Implementations concretes

" Generateur de document HTML
CLASS zcl_html_document_generator DEFINITION
INHERITING FROM zcl_document_generator
PUBLIC FINAL CREATE PUBLIC.
PROTECTED SECTION.
METHODS get_header REDEFINITION.
METHODS get_line_items REDEFINITION.
METHODS get_footer REDEFINITION.
METHODS format_output REDEFINITION.
ENDCLASS.
CLASS zcl_html_document_generator IMPLEMENTATION.
METHOD get_header.
rv_header = |<html><head><title>Commande { is_order-order_id }</title></head>| &&
|<body><h1>Commande { is_order-order_id }</h1>| &&
|<p>Client : { is_order-customer_name }</p>|.
ENDMETHOD.
METHOD get_line_items.
rv_items = |<table><tr><th>Position</th><th>Article</th><th>Quantite</th></tr>|.
LOOP AT is_order-items INTO DATA(ls_item).
rv_items = |{ rv_items }<tr><td>{ ls_item-pos }</td>| &&
|<td>{ ls_item-description }</td>| &&
|<td>{ ls_item-quantity }</td></tr>|.
ENDLOOP.
rv_items = |{ rv_items }</table>|.
ENDMETHOD.
METHOD get_footer.
rv_footer = |<p>Montant total : { is_order-total_amount } EUR</p></body></html>|.
ENDMETHOD.
METHOD format_output.
rv_formatted = iv_content.
ENDMETHOD.
ENDCLASS.
" Generateur texte brut
CLASS zcl_text_document_generator DEFINITION
INHERITING FROM zcl_document_generator
PUBLIC FINAL CREATE PUBLIC.
PROTECTED SECTION.
METHODS get_header REDEFINITION.
METHODS get_line_items REDEFINITION.
METHODS get_footer REDEFINITION.
METHODS format_output REDEFINITION.
ENDCLASS.
CLASS zcl_text_document_generator IMPLEMENTATION.
METHOD get_header.
rv_header = |========================================\n| &&
|COMMANDE { is_order-order_id }\n| &&
|Client : { is_order-customer_name }\n| &&
|========================================\n|.
ENDMETHOD.
METHOD get_line_items.
rv_items = |Positions :\n|.
LOOP AT is_order-items INTO DATA(ls_item).
rv_items = |{ rv_items } { ls_item-pos }. { ls_item-description } - Quantite : { ls_item-quantity }\n|.
ENDLOOP.
ENDMETHOD.
METHOD get_footer.
rv_footer = |----------------------------------------\n| &&
|TOTAL : { is_order-total_amount } EUR\n| &&
|========================================|.
ENDMETHOD.
METHOD format_output.
rv_formatted = iv_content.
ENDMETHOD.
ENDCLASS.

Utilisation dans RAP pour les Actions d’export

CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS export_document FOR MODIFY
IMPORTING keys FOR ACTION Order~exportDocument
RESULT result.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD export_document.
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
DATA lo_generator TYPE REF TO zcl_document_generator.
" Choisir le generateur base sur le parametre
CASE ls_order-%param-format.
WHEN 'HTML'.
lo_generator = NEW zcl_html_document_generator( ).
WHEN 'TEXT'.
lo_generator = NEW zcl_text_document_generator( ).
WHEN 'PDF'.
lo_generator = NEW zcl_pdf_document_generator( ).
ENDCASE.
DATA(ls_order_data) = CORRESPONDING zorder( ls_order ).
DATA(lv_document) = lo_generator->generate_document( ls_order_data ).
" Definir le resultat
APPEND VALUE #( %tky = ls_order-%tky
%param = VALUE #( content = lv_document
format = ls_order-%param-format ) )
TO result.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Pattern Facade

Le Pattern Facade offre une interface simplifiee vers un sous-systeme complexe. Il reduit les dependances et rend l’utilisation de fonctionnalites complexes plus simple.

Scenario : Order Processing Facade

CLASS zcl_order_facade DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS constructor.
" Interface simplifiee - un appel pour un processus complexe
METHODS process_complete_order
IMPORTING
is_order TYPE zorder_create
RETURNING
VALUE(rs_result) TYPE zorder_result
RAISING
cx_order_processing.
METHODS cancel_order
IMPORTING
iv_order_id TYPE zorder_id
RETURNING
VALUE(rv_success) TYPE abap_bool
RAISING
cx_order_processing.
PRIVATE SECTION.
DATA mo_validator TYPE REF TO zcl_order_validator.
DATA mo_inventory TYPE REF TO zcl_inventory_service.
DATA mo_payment TYPE REF TO zcl_payment_service.
DATA mo_notification TYPE REF TO zcl_notification_service.
DATA mo_logger TYPE REF TO zcl_order_logger.
ENDCLASS.
CLASS zcl_order_facade IMPLEMENTATION.
METHOD constructor.
" Initialiser les services internes
mo_validator = NEW zcl_order_validator( ).
mo_inventory = NEW zcl_inventory_service( ).
mo_payment = NEW zcl_payment_service( ).
mo_notification = NEW zcl_notification_service( ).
mo_logger = NEW zcl_order_logger( ).
ENDMETHOD.
METHOD process_complete_order.
" Processus complexe derriere une interface simple
mo_logger->log_start( is_order ).
" 1. Validation
DATA(lv_valid) = mo_validator->validate_order( is_order ).
IF lv_valid = abap_false.
RAISE EXCEPTION TYPE cx_order_processing
MESSAGE e001(zorder) WITH is_order-order_id.
ENDIF.
" 2. Verification et reservation du stock
DATA(lv_available) = mo_inventory->check_availability( is_order-items ).
IF lv_available = abap_false.
RAISE EXCEPTION TYPE cx_order_processing
MESSAGE e002(zorder).
ENDIF.
mo_inventory->reserve_items( is_order-items ).
" 3. Traitement du paiement
TRY.
DATA(ls_payment) = mo_payment->process_payment(
iv_amount = is_order-total_amount
is_payment_method = is_order-payment_method ).
CATCH cx_payment_failed INTO DATA(lx_payment).
" Annuler la reservation
mo_inventory->release_reservation( is_order-items ).
RAISE EXCEPTION TYPE cx_order_processing
MESSAGE e003(zorder)
PREVIOUS lx_payment.
ENDTRY.
" 4. Finaliser la commande
rs_result-order_id = zcl_number_range=>get_next( 'ZORDER' ).
rs_result-status = 'CONFIRMED'.
rs_result-payment_ref = ls_payment-reference.
rs_result-created_at = utclong_current( ).
" 5. Notifications
mo_notification->send_confirmation(
iv_email = is_order-customer_email
is_order = rs_result ).
mo_logger->log_success( rs_result ).
ENDMETHOD.
METHOD cancel_order.
mo_logger->log_cancellation_start( iv_order_id ).
" Processus d'annulation
mo_inventory->release_reservation( iv_order_id ).
mo_payment->refund( iv_order_id ).
mo_notification->send_cancellation( iv_order_id ).
rv_success = abap_true.
mo_logger->log_cancellation_success( iv_order_id ).
ENDMETHOD.
ENDCLASS.

Utilisation dans une RAP Action

CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS create_and_process FOR MODIFY
IMPORTING keys FOR ACTION Order~createAndProcess
RESULT result.
METHODS cancel FOR MODIFY
IMPORTING keys FOR ACTION Order~cancel
RESULT result.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD create_and_process.
DATA(lo_facade) = NEW zcl_order_facade( ).
LOOP AT keys INTO DATA(ls_key).
TRY.
" Un appel - processus complet
DATA(ls_result) = lo_facade->process_complete_order(
is_order = ls_key-%param ).
" Retourner le succes
APPEND VALUE #( %cid = ls_key-%cid
%param = ls_result )
TO result.
CATCH cx_order_processing INTO DATA(lx_error).
" Afficher l'erreur dans Reported
APPEND VALUE #( %cid = ls_key-%cid )
TO failed-order.
APPEND VALUE #( %cid = ls_key-%cid
%msg = lx_error )
TO reported-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD cancel.
DATA(lo_facade) = NEW zcl_order_facade( ).
LOOP AT keys INTO DATA(ls_key).
TRY.
DATA(lv_success) = lo_facade->cancel_order( ls_key-OrderId ).
IF lv_success = abap_true.
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( Status )
WITH VALUE #( ( %tky = ls_key-%tky
Status = 'CANCELLED' ) ).
APPEND VALUE #( %tky = ls_key-%tky
%param = VALUE #( success = abap_true ) )
TO result.
ENDIF.
CATCH cx_order_processing INTO DATA(lx_error).
APPEND VALUE #( %tky = ls_key-%tky )
TO failed-order.
APPEND VALUE #( %tky = ls_key-%tky
%msg = lx_error )
TO reported-order.
ENDTRY.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Quand utiliser quel Pattern ?

SituationPatternExemple
Creation d’objets dependant de parametresFactoryCalcul de prix par pays
Algorithme interchangeableStrategyStrategies de remise
Meme flux, details differentsTemplate MethodGeneration de documents
Simplifier un sous-systeme complexeFacadeTraitement de commandes

Eviter les anti-patterns

1. God Class - trop de responsabilites

" MAUVAIS : Une classe fait tout
CLASS zcl_order_manager DEFINITION.
PUBLIC SECTION.
METHODS validate_order.
METHODS calculate_price.
METHODS calculate_tax.
METHODS apply_discount.
METHODS reserve_inventory.
METHODS process_payment.
METHODS send_email.
METHODS generate_pdf.
METHODS update_statistics.
ENDCLASS.
" BON : Repartir les responsabilites
CLASS zcl_order_validator DEFINITION. " Seulement la validation
CLASS zcl_price_calculator DEFINITION. " Seulement le calcul de prix
CLASS zcl_inventory_service DEFINITION. " Seulement le stock
CLASS zcl_order_facade DEFINITION. " Orchestration

2. Dependances codees en dur

" MAUVAIS : Instanciation directe
METHOD process.
DATA(lo_service) = NEW zcl_payment_service( ). " Non testable
lo_service->process( ).
ENDMETHOD.
" BON : Injection de dependances
METHOD constructor
IMPORTING
io_payment_service TYPE REF TO zif_payment_service.
mo_payment_service = io_payment_service.
ENDMETHOD.

3. Instructions Switch au lieu du polymorphisme

" MAUVAIS : Switch pour les types
METHOD get_discount.
CASE iv_customer_type.
WHEN 'VIP'.
rv_discount = 15.
WHEN 'REGULAR'.
rv_discount = 5.
WHEN 'NEW'.
rv_discount = 10.
ENDCASE.
ENDMETHOD.
" BON : Pattern Strategy
METHOD get_discount.
rv_discount = mo_discount_strategy->calculate( is_customer ).
ENDMETHOD.

Bonnes pratiques pour RAP

  1. Interfaces pour la flexibilite : Definir des interfaces pour tous les composants interchangeables
  2. Classes petites et ciblees : Chaque classe a une responsabilite
  3. Injection de dependances : Injecter les dependances via le constructeur ou une Factory
  4. Testabilite : Les patterns permettent d’utiliser des objets mock
  5. Documentation : Mentionner les noms de patterns dans le code (" Strategy Pattern)

Combinaison de Patterns

En pratique, les patterns sont souvent combines :

" Combinaison Factory + Strategy
CLASS zcl_discount_factory DEFINITION.
PUBLIC SECTION.
CLASS-METHODS create_strategy
IMPORTING iv_campaign TYPE string
RETURNING VALUE(ro_strategy) TYPE REF TO zif_discount_strategy.
ENDCLASS.
CLASS zcl_discount_factory IMPLEMENTATION.
METHOD create_strategy.
CASE iv_campaign.
WHEN 'SUMMER'.
ro_strategy = NEW zcl_percentage_discount( 25 ).
WHEN 'VIP'.
ro_strategy = NEW zcl_vip_discount( ).
WHEN OTHERS.
ro_strategy = NEW zcl_no_discount( ).
ENDCASE.
ENDMETHOD.
ENDCLASS.

Sujets connexes

Conclusion

Les Design Patterns ne sont pas une fin en soi, mais des outils pour un meilleur code. Dans RAP, ils aident particulierement a :

  • Flexibilite : Implementations interchangeables sans modifier les appelants
  • Testabilite : Objets mock au lieu de vrais services dans les tests unitaires
  • Maintenabilite : Classes petites et comprehensibles au lieu de code monolithique
  • Extensibilite : Nouvelles variantes sans modifier le code existant

Commencez par le pattern le plus simple qui resout votre probleme. Toutes les situations ne necessitent pas des patterns complexes - parfois une simple instruction IF est la meilleure solution.