Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member

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 - :smile:







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 SY-SUBRC 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= ( T2 - T1 ).


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= ( T2 - T1 ).


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


And here are the results.

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

11 Comments