Monday 20 May 2019

Decoration Day – Questioning The Decorator Pattern

There are design pattern. And there are design pattern. There are some I use really often because they are clear to me in use and technique, like the singleton or the factory pattern, the observer and some others. The decorator pattern for some reasons is very hard for me to understand. I learned about it in a workshop years ago. But over the years there was no need for me to use it and therefor no need to understand it. I try to change this for some weeks now…

I am not completely through with it…

The decorator pattern itself is described often and the UML diagram can mostly be found there. Up to now I have no idea what to use the pattern for in an ABAP environment. Nevertheless I tried to build up an example to understand how it works and what I maybe could do with it someday.

Characteristics Of A Decorator


The main principle of a decorator pattern is, to decorate an existing object in a way that the object does not know about the decorations. But that is exactly my problem with this pattern: In which cases would I like to have something added to a class without knowing that there was something added? In most cases I had the decorations need to know at least something about the decorated object. That is mentioned as a disadvantage of this pattern: The decorations somehow need to inform the decorated object about what they have done. And this seems to me some kind of weird: using something that is especially decoupled and than building something to connect again.

How does the decorator work?


The decorator pattern creates a hierachical structure like the one you will get when using inheritance. The predecessor object will be passed to the decorator object when being instantiated and stores it in a class attribute. This happens with each new decorator that will be applied.

SAP ABAP Certifications, SAP ABAP Study Materials, SAP ABAP Guides, SAP ABAP Learning

After all decorations are done, for example the price can be queried by asking the last decoration about it. This decoration then asks the predecessor decorator about its price, add its own price and passes back the result.

When you think about ice cream the way for getting the price might be the following:

crumble->get_price
  caramel_sauce->get_price
    cream->get_price
      vanilla->get_price
        chocolate->get_price
          cone->get_price
          cone:               0,00
        chocolate: ( 0,00 ) + 1,00
      vanilla:     ( 1,00 ) + 1,00
    cream:         ( 2,00 ) + 0,50
  caramel_sauce:   ( 2,50 ) + 1,00
crumble:           ( 3,50 ) + 0,50
Result:              4,00

There is no connection between those components. That’s good on one hand, because you simply can add new components and it will have no technical impact on the existing classes.

What are the disadvantages?


In my opinion the biggest disadvantage is, that the use case for this pattern is very limited. Another sensible question could be about the weight of the ice cream. That also can be answered very well equivalent to how to calculate the price. Querying a price and the weight looks quite reasonable, but what if I want to know more about the configuration? For example is there a component that has artificial sweeter? How many scoops of ice cream do I have? Do I need to get a bigger waffle cone?

Fictional Examples


There are many many examples for how to use the decorator. Most of them are like:

◈ making a coffee
◈ decorating a pizza
◈ selling ice cream (see above)

in order to have a total price afterwards.

Things I would always do with some kind of a structured table because creating classes for these options is not common. There was one example

Configuring A Car


Because of the lack of a better SAP-world example, I decided to use cars, because this is something most people do know about. Cars are quite common and everyone knows some special options cars have.So I decided to decorate a car with some options. Having at least one well known thing in a world of new makes it easier to understand. Hopefully.

Classes


In my example program there is a basic model class (“basic”)and there are some option classes:

◈ option_multimedia
◈ option_automatic
◈ option_metallic
◈ option_rallye

Of course there is the main decorator class “option” which is defined abstract, because this class itself will not be used.

The class “basic” will be derived from “option”. “Basic” is the main class for holding the basic car model.

The decorator class “option_decorator” will also be derived from the abstract “option” class but has the main feature of the decorator pattern: a private attribute for holding the predecessor option.

Class “option” (abstract)


"=== abstract option class providing needed methods
CLASS option DEFINITION ABSTRACT.
  PUBLIC SECTION.
    TYPES: BEGIN OF ts_option,
             name  TYPE string,
             price TYPE i,
           END OF ts_option,
           tt_options TYPE STANDARD TABLE OF ts_option WITH DEFAULT KEY.

    METHODS get_configuration ABSTRACT
      RETURNING
        VALUE(options) TYPE tt_options.

    METHODS get_price  ABSTRACT
      RETURNING
        VALUE(price) TYPE i.

    DATA price TYPE i.
    DATA name  TYPE string.

ENDCLASS.


Class “basic”


"=== main class of "option" - this is the "basic model" which will be decorated
CLASS basic DEFINITION INHERITING FROM option.
  PUBLIC SECTION.
    METHODS get_configuration REDEFINITION.
    METHODS get_price REDEFINITION.
    METHODS constructor.
  PROTECTED SECTION.
ENDCLASS.

CLASS basic IMPLEMENTATION.
  METHOD get_configuration.
    APPEND VALUE #( name = name price = price ) TO options.
  ENDMETHOD.
  METHOD constructor.
    super->constructor( ).
    price = 15000.
    name  = 'Basic Model '.
  ENDMETHOD.
  METHOD get_price.
    price = me->price.
  ENDMETHOD.
ENDCLASS.


Class “option_decorator”


"=== Option decorator - this class will handle the option pattern
CLASS option_decorator DEFINITION INHERITING FROM option.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        im_decorator TYPE REF TO option.
    METHODS get_configuration REDEFINITION.
    METHODS get_price REDEFINITION.
  PRIVATE SECTION.
    DATA lo_decorator TYPE REF TO option.
ENDCLASS.

CLASS option_decorator IMPLEMENTATION.
  METHOD constructor.
    super->constructor( ).
    me->lo_decorator = im_decorator.
  ENDMETHOD.
  METHOD get_configuration.
    CHECK lo_decorator IS BOUND.
    options = lo_decorator->get_configuration( ).
  ENDMETHOD.
  METHOD get_price.
    price = lo_decorator->get_price( ) + me->price.
  ENDMETHOD.
ENDCLASS.


Classes “option_…”


"=== OPTION metallic paint
CLASS option_metallic DEFINITION INHERITING FROM option_decorator.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        im_decorator TYPE REF TO option.
    METHODS get_configuration REDEFINITION.
    METHODS get_price REDEFINITION.
ENDCLASS.

CLASS option_metallic IMPLEMENTATION.
  METHOD constructor.
    super->constructor( im_decorator ).
    price = 500.
    name  = 'metallic paint'.
  ENDMETHOD.
  METHOD get_configuration.
    options = super->get_configuration( ).
    APPEND VALUE #( name = name price = price ) TO options.
  ENDMETHOD.
  METHOD get_price.
    price = super->get_price( ).
  ENDMETHOD.
ENDCLASS.

"=== OPTION multimedia system
CLASS option_multimedia DEFINITION INHERITING FROM option_decorator.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        im_decorator TYPE REF TO option.
    METHODS get_configuration REDEFINITION.
    METHODS get_price REDEFINITION.
ENDCLASS.

CLASS option_multimedia IMPLEMENTATION.
  METHOD constructor.
    super->constructor( im_decorator ).
    price = 1000.
    name  = 'multimedia entertainment system'.
  ENDMETHOD.
  METHOD get_configuration.
    options = super->get_configuration( ).
    APPEND VALUE #( name = name price = price ) TO options.
  ENDMETHOD.
  METHOD get_price.
    price = super->get_price( ).
  ENDMETHOD.
ENDCLASS.

"=== OPTION automatic gear
CLASS option_automatic DEFINITION INHERITING FROM option_decorator.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        im_decorator TYPE REF TO option.
    METHODS get_configuration REDEFINITION.
    METHODS get_price REDEFINITION.
ENDCLASS.

CLASS option_automatic  IMPLEMENTATION.
  METHOD constructor.
    super->constructor( im_decorator ).
    price = 2000.
    name  = 'automatic gear'.
  ENDMETHOD.
  METHOD get_configuration.
    options = super->get_configuration( ).
    APPEND VALUE #( name = name price = price ) TO options.
  ENDMETHOD.
  METHOD get_price.
    price = super->get_price( ).
  ENDMETHOD.
ENDCLASS.

"=== OPTION rallye stripes
CLASS option_rallye DEFINITION INHERITING FROM option_decorator.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        im_decorator TYPE REF TO option.
    METHODS get_configuration REDEFINITION.
    METHODS get_price REDEFINITION.
ENDCLASS.

CLASS option_rallye IMPLEMENTATION.
  METHOD constructor.
    super->constructor( im_decorator ).
    price = 100.
    name  = 'rallye stripes'.
  ENDMETHOD.
  METHOD get_configuration.
    options = super->get_configuration( ).
    APPEND VALUE #( name = name price = price ) TO options.
  ENDMETHOD.
  METHOD get_price.
    price = super->get_price( ).
  ENDMETHOD.
ENDCLASS.


Methods


There are two methods in the option class:

◈ get_price
◈ get_configuration

Get_price will ask the predecessor for its price and adds its own price. The options own price is defined in the CONSTRUCTOR.

The method get_configuration asks the predecessor for its configuration and appends it’s own configuration (name) to the returning table.

Program


The report has four checkboxes respecting one of the four options. When beeing executed, the main object will be created: the “basic” class. For each selected option, the corresponding class will be created. The predecessor object (in case its the first option, the predecessor is the basic model), will be passed as importing parameter of the constructor.

START-OF-SELECTION.

  "options selection
  PARAMETERS p_mmdia AS CHECKBOX DEFAULT 'X'.
  PARAMETERS p_autom AS CHECKBOX DEFAULT 'X'.
  PARAMETERS p_metlc AS CHECKBOX DEFAULT 'X'.
  PARAMETERS p_rally AS CHECKBOX DEFAULT 'X'.

  DATA go_decorator   TYPE REF TO option.
  DATA go_predecessor TYPE REF TO option.


  CREATE OBJECT go_decorator TYPE basic.
  go_predecessor = go_decorator.

  IF p_mmdia IS NOT INITIAL.
    CREATE OBJECT go_decorator
      TYPE option_multimedia
      EXPORTING
        im_decorator = go_predecessor.
    go_predecessor = go_decorator.

  ENDIF.
  IF p_autom IS NOT INITIAL.
    CREATE OBJECT go_decorator
      TYPE option_automatic
      EXPORTING
        im_decorator = go_predecessor.
    go_predecessor = go_decorator.
  ENDIF.
  IF p_metlc IS NOT INITIAL.
    CREATE OBJECT go_decorator
      TYPE option_metallic
      EXPORTING
        im_decorator = go_predecessor.
    go_predecessor = go_decorator.
  ENDIF.
  IF p_rally IS NOT INITIAL.
    CREATE OBJECT go_decorator
      TYPE option_rallye
      EXPORTING
        im_decorator = go_predecessor.
    go_predecessor = go_decorator.

  ENDIF.


  LOOP AT go_predecessor->get_configuration( ) INTO DATA(option).
    WRITE: / option-name, 40 option-price.
  ENDLOOP.
  .
  WRITE: / 'TOTAL', AT 40 go_predecessor->get_price(  ).


Improvements


I made some improvements which I just want to mention but will not post due to too much complexity.

First upgrade: The decorator pattern itself has one quirk I really do not like: Creating an object with passing the current object and then overwriting the current object seems quite weird to me. So I tried to hide this in a helper class.

The second improvement is that in this version each option class has the same coding in get_price and get_configuration: Calling the previous object and adding own data to the result. I thought that this should be done in another way. So I derived another class “option_decorator_easy” from “option_decorator” to implement the two methods. All other option classes inherit from this new “easy” class. It makes the pattern even more complex but reduces code implementations.

Cancelling decoration


I also tried to cancel the decoration if a price limit has been reached. Maybe I didn’t implement this the right way, but the way I did has the follwing issue: Each option just adds itself to the configuration. The configuration itself is called at the end, after all decorators have done their job. So I only had the chance to cancel the output of the options in get_price.

No comments:

Post a Comment