Skip to Content

Hey SCN,

I was reading Bruno Esperança’s post ( The last runtime buffer you’ll ever need?) yesterday and it inspired me to think about the way I cache data in my own classes. I got to google-ing and found a nice blog about Caching with Decorator pattern and I thought I might give it a try in ABAP. I think the pattern works nicely for caching and as the author of Caching with Decorator pattern says:

I think this is a good way of applying caching, logging or any other things that you want to do before or after hitting your database.  It leaves your existing system in place and does not pollute your pure data access code (repositories) with other concerns.  In this case both classes have their own responsibilities, when it’s not in the cache the decorator class delegates the task to the repository and let it deal with the database.  Do I hear Single Responsibility Principal – 🙂


So lets jump right in to it. I just used the normal SAP example – SBOOK. For our fictitious program we just need to be able to select a single entry from SBOOK and we happen to know all the key fields.

I started with an interface:

INTERFACE ZIF_SBOOK_DB

PUBLIC.

METHODS:

     FIND_BY_KEY IMPORTING CARRID TYPE S_CARR_ID

                           CONNID TYPE S_CONN_ID

                           FLDATE TYPE S_DATE

                           BOOKID TYPE S_BOOK_ID

                 RETURNING VALUE(RS_SBOOK) TYPE SBOOK.

ENDINTERFACE.

Then I created the basic implementation – selecting from the database directly:

CLASS ZCL_SBOOK_DB_IMPL DEFINITION

PUBLIC

CREATE PUBLIC .

PUBLIC SECTION.

   INTERFACES: ZIF_SBOOK_DB.

PROTECTED SECTION.

PRIVATE SECTION.

ENDCLASS.

CLASS ZCL_SBOOK_DB_IMPL IMPLEMENTATION.

METHOD ZIF_SBOOK_DB~FIND_BY_KEY.

   SELECT SINGLE *

     INTO RS_SBOOK

     FROM SBOOK

     WHERE CARRID = CARRID

       AND CONNID = CONNID

       AND FLDATE = FLDATE

       AND BOOKID = BOOKID.

ENDMETHOD.

ENDCLASS.

Now we could just stop there… We have a perfectly good database layer and it meets the requirements of whatever fictitious program we are creating. Lets assume we have some performance problems, or maybe we just noticed in ST05 that the same query is being executed multiple times. This is where the decorator pattern comes in to play:

CLASS ZCL_SBOOK_DB_CACHE_DECORATOR DEFINITION

PUBLIC

FINAL

CREATE PUBLIC

INHERITING FROM ZCL_SBOOK_DB_IMPL.

PUBLIC SECTION.

   METHODS: ZIF_SBOOK_DB~FIND_BY_KEY REDEFINITION.

PROTECTED SECTION.

PRIVATE SECTION.

   DATA: _CACHE TYPE HASHED TABLE OF SBOOK WITH UNIQUE KEY CARRID CONNID FLDATE BOOKID.

ENDCLASS.

CLASS ZCL_SBOOK_DB_CACHE_DECORATOR IMPLEMENTATION.

METHOD ZIF_SBOOK_DB~FIND_BY_KEY.

   READ TABLE _CACHE INTO RS_SBOOK WITH KEY CARRID= CARRID CONNID = CONNID FLDATE = FLDATE BOOKID = BOOKID.

   IF SYSUBRC NE 0.

     RS_SBOOK= SUPER->ZIF_SBOOK_DB~FIND_BY_KEY( CARRID = CARRID CONNID = CONNID FLDATE = FLDATE BOOKID = BOOKID ).

     INSERT RS_SBOOK INTO TABLE _CACHE.

   ENDIF.

ENDMETHOD.

ENDCLASS.

I think this is pretty easy to understand. We have defined a class that inherits from our basic implementation. It checks a private attribute (the cache) to see if it already has the item you need. If it doesn’t have it, then it delegates to the super class – our basic implementation – and queries the database then puts the result in to the cache.

I see a couple of advantages in using the decorator pattern in this way to implement caching:

  • The buffering technique is not coupled to the implementation of the database layer. If I wanted to use shared memory objects instead of a private attribute that change would be easy to implement and I could be confident that it would not impact my existing database layer.
  • I can easily decide in any program I write whether or not I want to utilize the buffer. To buffer I instantiate an instance of zcl_sbook_db_cache_decorator and to ensure I always go directly to the database I instantiate an instance of zcl_sbook_db_impl.
  • I can add buffering to any existing database layer classes I may have already written without touching the existing (and proven!) code in those classes just by sub-classing them.

Finally, I decided I better test the performance. I was pretty confident that the cache would be faster, but I guess you never know:

REPORT Z_TEST_SBOOK_DB_LAYER.

DATA: T1 TYPE I,

     T2 TYPE I,

     TDIFF TYPE I.

DATA: LV_CARRID TYPE S_CARRID VALUE ‘AA’,

     LV_CONNID TYPE S_CONN_ID VALUE ’17’,

     LV_FLDATE TYPE S_DATE VALUE ‘20121031’,

     LV_BOOKID TYPE S_BOOK_ID VALUE ’23’.

DATA: LO_SBOOK_CACHE TYPE REF TO ZIF_SBOOK_DB.

CREATE OBJECT LO_SBOOK_CACHE TYPE ZCL_SBOOK_DB_CACHE_DECORATOR.

WRITE: / ‘First read from the cache decorator will be from the database.’.

SET RUN TIME CLOCK RESOLUTION HIGH.

GET RUN TIME FIELD T1.

LO_SBOOK_CACHE->FIND_BY_KEY( CARRID = LV_CARRID

                            CONNID= LV_CONNID

                            FLDATE= LV_FLDATE

                            BOOKID= LV_BOOKID ).

GET RUN TIME FIELD T2.

TDIFF= ( T2T1 ).

WRITE: / ‘It took ‘, TDIFF, ‘ microseconds to read from the database.’.

WRITE: / ‘Second read from the cache decorator will be from the cache.’.

GET RUN TIME FIELD T1.

LO_SBOOK_CACHE->FIND_BY_KEY( CARRID = LV_CARRID

                            CONNID= LV_CONNID

                            FLDATE= LV_FLDATE

                            BOOKID= LV_BOOKID ).

GET RUN TIME FIELD T2.

TDIFF= ( T2T1 ).

WRITE: /‘It took ‘, TDIFF, ‘ microseconds to read from the cache.’.

And here are the results.

results.PNG

So as you can see, it’s a bit of an improvement 🙂 I hope you find this useful in your own development!

To report this post you need to login first.

11 Comments

You must be Logged on to comment or reply to a post.

  1. Jānis B

    Gimme one cache for the whole session (via singleton), that can be initialized at will, or give me none at all! 🙂

    cheers

    Janis

    (0) 
    1. Lucas Tétreault Post author

      That should be pretty easy… 🙂


      I haven’t actually tried this but it should work:


      CLASS zcl_sbook_db_cache_singleton DEFINITION

        PUBLIC

        FINAL

        CREATE PRIVATE

        INHERITING FROM zcl_sbook_db_cache_decorator.

        PUBLIC SECTION.

          METHODS: instance RETURNING VALUE(ro_db) TYPE REF                TO zcl_sbook_db_cache_singleton.

        PROTECTED SECTION.

        PRIVATE SECTION.

          class-data: _instance type ref to zcl_sbook_db_cache_singleton.

      ENDCLASS.

      CLASS zcl_sbook_db_cache_singleton IMPLEMENTATION.

        METHOD instance.

          IF _instance IS INITIAL.

            CREATE OBJECT _instance.

          ENDIF.

          ro_db = _instance.

        ENDMETHOD.

      ENDCLASS.

      (0) 
      1. Jānis B

        Yup, except Instance( ) needs to be class-method, the FINAL needs to be removed from the decorator, and I’d give a way to reset() it, just in case the cache gets inundated by those bookings… 🙂

        (0) 
  2. Arshad Ansary

    Hi Lucas ,

    Thanks for blog on how to use decorator pattern. For simple buffering use cases  do we need to have this kind of separation concern  and always feel this is some kind of overengineering we do with this design patterns.

    How about having an additional parameter IV_BUFFER in the class ZCL_SBOOK_DB_IMPL.if the user wants buffered data he should set it as ‘X’ otherewise read from DB.

    CLASS ZCL_SBOOK_DB_IMPL DEFINITION

    PUBLIC

    CREATE PUBLIC .


    PUBLIC SECTION.

    METHODs:FIND_BY_KEY IMPORTING CARRID TYPE S_CARR_ID

          BUFFER TYPE BOOLEAN OPTIONAL   

                               CONNID TYPE S_CONN_ID

                               FLDATE TYPE S_DATE

                               BOOKID TYPE S_BOOK_ID

                     RETURNING VALUE(RS_SBOOK) TYPE SBOOK.

    PROTECTED SECTION.

    PRIVATE SECTION.
    DATA: CACHE TYPE HASHED TABLE OF SBOOK WITH UNIQUE KEY CARRID CONNID FLDATE BOOKID.

    ENDCLASS.

    METHOD FIND_BY_KEY.

    IF IV_BUFFER IS INITIAL.”Dont read from Buffer

    SELECT SINGLE *

         INTO RS_SBOOK

         FROM SBOOK

         WHERE CARRID = CARRID

           AND CONNID = CONNID

           AND FLDATE = FLDATE

           AND BOOKID = BOOKID.

    ELSE.Read from Buffer

    READ TABLE CACHE INTO RS_SBOOK WITH KEY CARRID= CARRID CONNID = CONNID FLDATE = FLDATE BOOKID = BOOKID.

    if sy-subrc ne 0.

    SELECT SINGLE *

         INTO RS_SBOOK

         FROM SBOOK

         WHERE CARRID = CARRID

           AND CONNID = CONNID

           AND FLDATE = FLDATE

           AND BOOKID = BOOKID.
    INSERT RS_SBOOK INTO TABLE _CACHE.

    endif.

    ENDIF.

    ENDMETHOD.

    Regards

    Arshad

    (0) 
    1. Suhas Saha

      For simple buffering use cases  do we need to have this kind of separation concern

      I think it is more about the flexibility of the solution(read – loose coupling) than separation-of-concerns (SoC). Tbh i don’t understand why do you say SoC, can you please explain?

      From the caller/client’s perspective if it doesn’t matter from where the data is fetched, then he can instantiate the interface with the latter type. If he wants to read the data directly from the DB he can use still use the same interface method, but the instantiation has to be of the former type.

      BR,

      Suhas

      (0) 
    2. Lucas Tétreault Post author

      Arshad,

      If you’re not accustomed to using design patterns they can seem like over-engineering… What they do for us in this case is allow each class to have a single responsibility. This might not seem important when you’re first writing the code but it will definitely be important when you need to extend or add features. In the earlier comment from Janis I provided a simple to way to extend the functionality even more by ‘decorating’ the base class yet again with a singleton pattern. With your example, where the class is already doing more than one thing, I might be tempted to just implement it in that class…. Further cluttering the class.

      Obviously the code you provided works, but it seems like that just isn’t fully taking advantage of the benefits objects provide us.

      (0) 
  3. Jacques Nomssi

    Hello Lucas,

    I would say pattern here is a  cache proxy not a decorator. Quote from a fireside chat (page 472) with proxy and decorator getting intentional 🙂 :

    – Decorator: you’re just a Decorator in disguise

    – Proxy: I’m convinced you’re just a dumb proxy


    regards,


    JNN

    (0) 
  4. Paul Hardy

    In the UK there used to be a magazine called VIZ which had a pedants corner for people trying to point out the most trivial “mistakes” they could e.g.

    The song lyric “Yesterday papers, wth yesterdays news” was tagged as incorrect because yesterdays papers would in fact have the day before yesterdays news.

    This entries would start with “no one is more pedantic than I”.

    So, no one is more pedantic than I, so I am going to say that whilst the ABAP exmple above does the same thing as the decorator pattern, it is not really the decorator pattern in the way the decorator pattern is described in say “Head First Design Patterns” or “Design Patterns using Object Orientated ABAP” or indeed in the blog mentioned right at the start.

    In all of the latter three the decorator takes in an instance of the class it is decorating as an input parameter during construction. Then you create the instance passing in 0 to many decorators. When the client calls a method of the instance it is blisfully unaware of whether the instance does just it’s usual business or does all sorts of other things first (or aftewards).

    Anyway, as I said, I am just being pedantic. Time to go and drink with my mates and kill a few more brain cells, assuming I have got any left.

    Cheersy Cheers

    Paul

    (0) 
    1. Rüdiger Plantiko

      Paul – I agree:

      The main point of the decorator pattern is to make the decorated call look like the plain, undecorated one, where the components chosen for decoration are specified in instance creation.

      With other words – if you have an instance variable

        data: lo_sbook type ref to zcl_sbook_db.

      and zcl_sbook_db has a select() method, then you can simply write in the client code

        lo_sbook->select(  ).

      but the select will be performed differently, depending on the bound instance:

      • without caching if the instance is created as a “plain” db object
        lo_sbook = new ZCL_SBOOK_DB( )
      • with caching if the instance is decorated with the cache decorator
        lo_sbook = new ZCL_SBOOK_CACHED( new ZCL_SBOOK_DB( ) ).
      • with caching and logging if the instance is decorated with both of those
        lo_sbook = new ZCL_SBOOK_LOG( new ZCL_SBOOK_CACHED( new ZCL_SBOOK_DB( ) ),
      • … only with logging, and so forth.

      Regards,

      Rüdiger

      Here is my ABAP paste for it: [ABAP] Decorate a selection with a cache – Pastebin.com

      (0) 
  5. Michal Mastalski

    Hello Lucas,

    Some time ago I was also thinking about using decorator pattern to fetch data in ABAP. But the example you provided works only in cases when full method interface is given (carrid,connid,fldate,bookid). If one of those values will be passed as initial, it might return wrong results. For each case, when you need some other selection fields, you’ll have to prepare some additional logic before executing your method or create another subclass. For me is a little bit too much work and effort to just get some data from database. But, anyway, it might be usefull in case of performance issues 🙂

    Regards

    Michał

    (0) 

Leave a Reply