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:
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!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
2 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 |