Skip to Content
Author's profile photo Kerem Koseoglu

A General Purpose ABAP Multiton Class

In this post, I will share a general purpose class covering the multiton design pattern. By implementing a simple interface, you can add multiton functionality to your existing classes.

What is multiton, anyway?

Multiton is a performance oriented design pattern. It is based on the idea of caching and re-using objects corresponding to the same key. For each object key (such as a vendor number), a static object instance of a class (such as a vendor class) is kept in a central location. Whenever a client requests an object corresponding the key, the existing object is returned instead of creating a new one. This approach reduces the memory footprint due to the decreased number of objects, and avoids the performance cost to re-create objects having the same key. (source: Design Patterns in ABAP Objects)

A typical multiton class would have the following skeleton structure.

CLASS zcl_vendor DEFINITION
  PUBLIC FINAL CREATE PRIVATE.

  PUBLIC SECTION.

    DATA gv_lifnr TYPE lifnr READ-ONLY

    CLASS-METHODS get_instance
      IMPORTING !iv_lifnr TYPE lifnr
      RETURNUNG VALUE(ro_obj) TYPE REF TO zcl_vendor.

  PRIVATE SECTION.

   TYPES:
      BEGIN OF t_multiton,
        lifnr TYPE lifnr,
        obj TYPE REF TO zcl_vendor,
      END OF t_multiton,

      tt_multiton TYPE HASHED TABLE OF t_multiton
        WITH UNIQUE KEY primary_key COMPONENTS lifnr.

    CLASS-DATA gt_multiton TYPE tt_multiton.

    METHODS constructor
      IMPORTING
        !iv_lifnr TYPE lifnr.

  PROTECTED SECTION.

ENDCLASS.

 
CLASS zcl_vendor IMPLEMENTATION.

  METHOD constructor.
    “ Check if IV_LIFNR exists in LFA1 and raise error if not
    gv_lifnr = iv_lifnr.
  ENDMETHOD.

  METHOD get_instance.

    ASSIGN gt_multiton[ KEY primary_key
      COMPONENTS lifnr = iv_lifnr ]
      TO FIELD-SYMBOL(<ls_multiton>).

    IF sy-subrc NE 0.

      “ Check if IV_LIFNR exists in LFA1 and raise error if not
      DATA(ls_multiton) = VALUE t_multiton( lifnr = iv_lifnr ).
      ls_multiton-obj = NEW #( iv_lifnr ).
      INSERT ls_multiton 
        INTO TABLE gt_multiton
        ASSIGNING <ls_multiton>.

    ENDIF.

    ro_obj = <ls_multiton>-obj.

  ENDMETHOD.
 

ENDCLASS.

When ZCL_VENDOR=>GET_INSTANCE( ‘12345’ ) is called for the first time, a new instance of ZCL_VENOR is created and stored in GT_MULTITON; and that very instance is returned.

When ZCL_VENDOR=>GET_INSTANCE( ‘12345’ ) is called again, the existing instance of ZCL_VENDOR in GT_MULTITON is returned instead of a new instance. That saves memory and runtime.

Now, what is the value-add of this post?

Instead of creating a specialized multiton implementation into every required class, I have created a general purpose multiton class which does all the hard work of caching objects. All you have to do is to implement an interface into your existing class to add multiton functionality.

Let’s assume that our vanilla class looks like this.

CLASS zcl_bc_multiton_demo DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    data:
      gv_id    type char15,
      gv_erdat type erdat,
      gv_ernam type ernam.

    methods:
      constructor importing iv_id type char15.

  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS zcl_bc_multiton_demo IMPLEMENTATION.

  method constructor.
    gv_id = iv_id.
    gv_erdat = sy-datum.
    gv_Ernam = sy-uname.
  endmethod.

ENDCLASS.

Pretty simple, huh? This is the class we presumably need multiton functionality on.

This is the interface we need to implement.

interface ZIF_BC_MULTITON
  public .

  class-methods:
      get_instance
        importing
          !iv_objectid type CDOBJECTV
        returning
          value(ro_obj) type ref to ZIF_BC_MULTITON
        raising
          CX_SY_CREATE_OBJECT_ERROR.

endinterface.

After implementing the interface, our vanilla class looks like this.

CLASS zcl_bc_multiton_demo DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    interfaces ZIF_BC_MULTITON.

    data:
      gv_id    type char15,
      gv_erdat type erdat,
      gv_ernam type ernam.

    methods:
      constructor importing iv_id type char15.

  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.



CLASS zcl_bc_multiton_demo IMPLEMENTATION.

  method constructor.
    gv_id = iv_id.
    gv_erdat = sy-datum.
    gv_Ernam = sy-uname.
  endmethod.

  METHOD zif_bc_multiton~get_instance.
    ro_obj ?= new zcl_Bc_multiton_demo( conv #( iv_objectid ) ).
  ENDMETHOD.

ENDCLASS.

And here is the general purpose class that does the caching.

CLASS zcl_bc_multiton DEFINITION
  PUBLIC
  FINAL
  CREATE public .

  PUBLIC SECTION.

    class-METHODS:
      get_obj
        IMPORTING
          !iv_clsname   TYPE seoclsname
          !iv_objectid  TYPE cdobjectv
        RETURNING
          VALUE(ro_obj) TYPE REF TO zif_bc_multiton
        RAISING
          cx_sy_create_object_error.

  PROTECTED SECTION.
  PRIVATE SECTION.

    TYPES:
      BEGIN OF t_multiton,
        clsname  TYPE seoclsname,
        objectid TYPE cdobjectv,
        cx       TYPE REF TO cx_sy_create_object_error,
        obj      TYPE REF TO zif_bc_multiton,
      END OF t_multiton,

      tt_multiton
        TYPE HASHED TABLE OF t_multiton
        WITH UNIQUE KEY primary_key COMPONENTS clsname objectid.

    class-DATA:
      gt_multiton TYPE tt_multiton.

ENDCLASS.



CLASS zcl_bc_multiton IMPLEMENTATION.

  METHOD get_obj.

    ASSIGN gt_multiton[
        KEY primary_key COMPONENTS
        clsname  = iv_clsname
        objectid = iv_objectid
      ] TO FIELD-SYMBOL(<ls_mt>).

    IF sy-subrc NE 0.

      DATA(ls_mt) = VALUE t_multiton(
        clsname  = iv_clsname
        objectid = iv_objectid
      ).

      TRY.

          CALL METHOD (ls_mt-clsname)=>zif_bc_multiton~get_instance
            EXPORTING
              iv_objectid = ls_mt-objectid
            RECEIVING
              ro_obj      = ls_mt-obj.

        CATCH cx_sy_create_object_error INTO ls_mt-cx ##no_handler.
        CATCH cx_root INTO DATA(lo_diaper).

          ls_mt-cx = NEW #(
            textid    = cx_sy_create_object_error=>cx_sy_create_object_error
            classname = CONV #( ls_mt-clsname )
            previous  = lo_diaper
          ).

      ENDTRY.

      INSERT ls_mt
        INTO TABLE gt_multiton
        ASSIGNING <ls_mt>.

    ENDIF.

    IF <ls_mt>-cx IS NOT INITIAL.
      RAISE EXCEPTION <ls_mt>-cx.
    ENDIF.

    ro_obj = <ls_mt>-obj.

  ENDMETHOD.

ENDCLASS.

Now, if we need to create an instance of ZCL_BC_MULTITON_DEMO bypassing the multiton cache, all we need to do is to create the object regularly as demonstrated below.

DATA(lo_obj) = NEW zcl_bc_multiton_demo( 'DUMMY' ).

If we need to get advantage of multiton, here is what we need to do.

DATA(lo_obj) = CAST zcl_bc_multiton_demo(
  zcl_bc_multiton=>get_obj(
    iv_clsname = 'ZCL_BC_MULTITON_DEMO'
    iv_objectid = 'DUMMY'
  )
).

Pretty neat, eh? Having ZCL_BC_MULTITON_DEMO, we don’t need to deal with caching anywhere else. This class will do the multiton caching for you, and return the cached instance in case you re-call GET_OBJ with the same object id.

Assigned Tags

      12 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Jacques Nomssi Nzali
      Jacques Nomssi Nzali

      Hello Kerem,

      I get your point here but as always, the developer has to balance complexity (new class + interface implementation) with flexibility.

      I made my caching code a little simpler with the DEFAULT idiom:

      result = VALUE #( table_expr DEFAULT general_expr ).

      If you are like me and not like Horst Keller, you have been frustrated by the reader/writer positions, functional operand positions, numeric expression position... but this is a case of general expression position so instead of writing code with IF we can separate Query from Modifier by extracting a new_instance( ) method

       CLASS-METHODS new_instance IMPORTING iv_lifnr      TYPE lifnr
                                  RETURNING VALUE(ro_obj) TYPE REF TO zcl_vendor.
      
        METHOD new_instance.
          ro_obj = NEW #( iv_lifnr ).
          INSERT VALUE #( lifnr = iv_lifnr
                          obj = ro_obj ) INTO TABLE gt_multiton.
        ENDMETHOD.

      and simplify get_instance( )

        METHOD get_instance.
          ro_obj = VALUE #( gt_multiton[ KEY primary_key COMPONENTS lifnr = iv_lifnr ]-obj
                              DEFAULT new_instance( iv_lifnr ) ).
        ENDMETHOD.

      With this, I did not feel I needed a central caching/registry yet, but maybe in the future.

       

      best regards,

      JNN

      Author's profile photo Iftah Peretz
      Iftah Peretz

      Classy (pun intended)!!!

      Author's profile photo Suhas Saha
      Suhas Saha

      Hello Jacques Nomssi,

      What do you think of defining the multiton "cache" as PUBLIC READ-ONLY?

      BR,

      Suhas

      Author's profile photo Jacques Nomssi Nzali
      Jacques Nomssi Nzali

      Hello Suhas,

      as always, the developer has to balance complexity with flexibility, so it will depend on your use case (I hope you did not expect any other answer).

      I can image using a such a pattern for singleton, multiton (now that I learned from Kerem), memoization, cache proxy. I will assume you want to provide a special access point.

      Let me try to evaluate the strengths and limits of the pattern:

      Making the cache public read-only has the merit to provide a single global access point to all client with the best performance possible. As it cannot be changed outside the class, it mitigate one problem with global variables. It is often the simplest thing that could possibly work and you will know how to change it later.

      On the other hand, the cache is now part of the public interface of your object. Exposing attributes that are not absolutely needed will yield code that cannot be redirected or extended/decorated like a method call. This makes e.g. unit testing more difficult (no test SEAM). Changes are likely to break existing clients.

      JNN

      Author's profile photo Bruno Esperança
      Bruno Esperança

      Hi Jacques,

      Good to see you!

      Do you really use a static method to create instances normally, or was this just for demonstration purposes?

      Cheers,
      Bruno

      Author's profile photo Jacques Nomssi Nzali
      Jacques Nomssi Nzali

      Hello Bruno,

      nice to read from you!

      In this case the pattern requires a single access to the "cached" objects so the static method is needed. But yes, I generally I like using factory method. Nowadays I name those new( ) instead factory( ).

      JNN

      Author's profile photo Joachim Rees
      Joachim Rees

      Awesome, thanks for sharing!

      Author's profile photo nikhil k
      nikhil k

      good one Kerem Koseoglu.

       

      Regards,

      Nikhil Kulkarni

      Author's profile photo Michelle Crapo
      Michelle Crapo

      Very nice!!!!!   So nice of you to share.  I can see where it will help me out.  I love the way you wrote the blog too.   It "talked" to me.

      <Sigh> This is another one I'll bookmark.

      Michelle

      Author's profile photo Robert Forster
      Robert Forster

      Hi,

      very nice one.

      But please keep in mind that as long as your table holds the reference the ABAP garbage collection can not remove the objetcs. So unused objects can blow up the memory consumption, which is contradicting your purpose.

       

      BR

      Author's profile photo Waldemar Schakiel
      Waldemar Schakiel

      Hi Kerem,

      your approach with a common factory multiton class is very nice. It is particularly suitable for entities with a one key field.

      What is your strategy for entities with multi fileld keys? Are you trying to convert the multi field key into a string field?

      Maybe you have developed another suitable method? Let me know what is your prefered approach for such cases.

      Greetings

      Waldemar

      Author's profile photo Kerem Koseoglu
      Kerem Koseoglu
      Blog Post Author

      A string combination would work, yes. Preferably separated by semicolon or brackets.