Implementing Factory Methods

Objective

After completing this lesson, you will be able to use factory methods.

Factory Methods

Sometimes a class needs to keep control of its instances. This could be to prevent multiple instances being created. In order to do so, it must prevent its users from creating instances themselves. You do this in ABAP by using the CREATE PRIVATE addition in the CLASS DEFINITION statement. Any attempt to use the NEW operator outside the class will now lead to a syntax error. Note that other programming languages achieve this effect by altering the visibility of the constructor method. ABAP does not support this.

Since users of the class cannot create their own instances, the class must then provide a public static method that creates instances and returns them to the user. This is called a factory method.

Try It Out: Factory Methods Implementation

  1. Create a new class that implements the interface IF_OO_ADT_CLASSRUN.
  2. Switch to the Local Types tab and copy the following code snippet into the editor:
    Code Snippet
    12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
    CLASS lcl_connection DEFINITION CREATE PRIVATE. PUBLIC SECTION. METHODS constructor IMPORTING airlineid TYPE /dmo/carrier_id connectionnumber TYPE /dmo/connection_id fromAirport TYPE /dmo/airport_from_id toAirport TYPE /dmo/airport_to_id. CLASS-METHODS get_connection IMPORTING airlineId TYPE /dmo/carrier_id connectionNumber TYPE /dmo/connection_id RETURNING VALUE(ro_connection) TYPE REF TO lcl_Connection. PRIVATE SECTION. DATA AirlineId TYPE /dmo/carrier_id. DATA ConnectionNumber TYPE /dmo/connection_id. DATA fromAirport TYPE /dmo/airport_from_id. DATA toAirport TYPE /dmo/airport_to_id. ENDCLASS. CLASS lcl_connection IMPLEMENTATION. METHOD constructor. me->airlineid = airlineid. me->connectionnumber = connectionnumber. me->fromAirport = fromAirport. me->toAirport = toAirport. ENDMETHOD. METHOD get_connection. DATA fromAirport TYPE /dmo/airport_from_id. DATA toAirport TYPE /dmo/airport_to_id. SELECT SINGLE FROM /dmo/connection FIELDS airport_from_id, airport_to_id WHERE carrier_id = @airlineid AND connection_id = @connectionnumber INTO ( @fromAirport, @toAirport ). ro_connection = NEW #( airlineid = airlineid connectionnumber = connectionnumber fromairport = fromairport toairport = toairport ). ENDMETHOD. ENDCLASS.
  3. Switch to the Global Class tab and paste the following code snippet into the implementation of method if_oo_adt_classrun~main( ):
    Code Snippet
    12345678910111213141516171819202122232425262728293031323334
    CLASS zcl_s4d401_factory DEFINITION PUBLIC FINAL CREATE PUBLIC . PUBLIC SECTION. interfaces if_oo_adt_classrun. PROTECTED SECTION. PRIVATE SECTION. ENDCLASS. CLASS zcl_s4d401_factory IMPLEMENTATION. METHOD if_oo_adt_classrun~main. data connection type ref to lcl_connection. * Debug the method to show that the class returns objects, but that there are different * objects for the same combination of airline and flight number connection = lcl_connection=>get_connection( airlineid = 'LH' connectionnumber = '0400' ). connection = lcl_connection=>get_connection( airlineid = 'LH' connectionnumber = '0400' ). ENDMETHOD. ENDCLASS.
  4. Activate the class with Ctrl + F3.
  5. Play around with the coding. In particular, debug the main() method and note the internal instance numbers assigned to the objects returned by the factory method.

Singleton Patterns

The purpose of the singleton pattern is to ensure that there is only one instance of a class in an application. This could be a single instance of the class or, in our case, a single instance for each combination of key attributes.

Watch the following videos to know more about the Singleton Pattern.

Try It Out: Singleton Pattern Implementation

  1. Create a new class that implements the interface IF_OO_ADT_CLASSRUN.
  2. Switch to the Local Types tab and copy the following code snippet into the editor:
    Code Snippet
    1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
    CLASS lcl_connection DEFINITION CREATE PRIVATE. PUBLIC SECTION. METHODS constructor IMPORTING airlineid TYPE /dmo/carrier_id connectionnumber TYPE /dmo/connection_id fromAirport TYPE /dmo/airport_from_id toAirport TYPE /dmo/airport_to_id. CLASS-METHODS get_connection IMPORTING airlineId TYPE /dmo/carrier_id connectionNumber TYPE /dmo/connection_id RETURNING VALUE(ro_connection) TYPE REF TO lcl_Connection. PRIVATE SECTION. TYPES: BEGIN OF ts_instance, airlineId TYPE /dmo/carrier_id, connectionNumber TYPE /dmo/connection_id, object TYPE REF TO lcl_connection, END OF ts_instance, tt_instances TYPE HASHED TABLE OF ts_instance WITH UNIQUE KEY airlineId ConnectionNumber. DATA AirlineId TYPE /dmo/carrier_id. DATA ConnectionNumber TYPE /dmo/connection_id. DATA fromAirport TYPE /dmo/airport_from_id. DATA toAirport TYPE /dmo/airport_to_id. CLASS-DATA connections TYPE tt_instances. ENDCLASS. CLASS lcl_connection IMPLEMENTATION. METHOD constructor. me->airlineid = airlineid. me->connectionnumber = connectionnumber. me->fromAirport = fromAirport. me->toAirport = toAirport. ENDMETHOD. METHOD get_connection. DATA fromAirport TYPE /dmo/airport_from_id. DATA toAirport TYPE /dmo/airport_to_id. IF NOT line_exists( connections[ airlineid = airlineid connectionnumber = connectionnumber ] ). SELECT SINGLE FROM /dmo/connection FIELDS airport_from_id, airport_to_id WHERE carrier_id = @airlineid AND connection_id = @connectionnumber INTO ( @fromAirport, @toAirport ). ro_connection = NEW #( airlineid = airlineid connectionnumber = connectionnumber fromairport = fromairport toairport = toairport ). DATA(new_instance) = VALUE ts_instance( airlineId = airlineId connectionnumber = connectionnumber object = ro_connection ). INSERT new_instance INTO TABLE connections. ELSE. ro_connection = connections[ airlineId = airlineId connectionnumber = connectionnumber ]-object. ENDIF. ENDMETHOD. ENDCLASS.
  3. Switch to the Global Class tab and paste the following code snippet into the implementation of method if_oo_adt_classrun~main( ):
    Code Snippet
    123456789101112131415161718192021222324252627282930313233
    CLASS zcl_s4d401_factory_singleton DEFINITION PUBLIC FINAL CREATE PUBLIC . PUBLIC SECTION. interfaces if_oo_adt_classrun. PROTECTED SECTION. PRIVATE SECTION. ENDCLASS. CLASS zcl_s4d401_factory_singleton IMPLEMENTATION. METHOD if_oo_adt_classrun~main. data connection type ref to lcl_connection. * Debug the method to show that the class always returns the same object * for the same combination of airline and flight number connection = lcl_connection=>get_connection( airlineid = 'LH' connectionnumber = '0400' ). connection = lcl_connection=>get_connection( airlineid = 'LH' connectionnumber = '0400' ). ENDMETHOD. ENDCLASS.
  4. Activate the class with Ctrl + F3.
  5. Play around with the coding. In particular, debug the main() method and note the internal instance numbers assigned to the objects returned by the factory method.

Implement a Factory Method

You notice that your code, in its current form, allows multiple instances of LCL_CARRIER to exist, representing the same airline. To fix this, you will define and implement a factory method that ensures only one instance can be created for a given carrier ID value.

Template:

  • /LRN/CL_S4D401_OOS_INTF_USAGE (Global Class)

Solution:

  • /LRN/CL_S4D401_OOS_FACTORY (Global Class)

Task 1: Copy Template (Optional)

Copy the template class /LRN/CL_S4D401_OOS_INTF_USAGE. If you finished the previous exercise, you can skip this task and continue editing your class ZCL_##_SOLUTION.

Steps

  1. Copy class /LRN/CL_S4D401_OOS_INTF_USAGE to a class in your own package (suggested name: ZCL_##_SOLUTION, where ## stands for your group number).

    1. In the Project Explorer, right-click class /LRN/CL_S4D401_OOS_INTF_USAGE to open the context menu.

    2. From the context menu, choose Duplicate....

    3. Enter the name of your package in the Package field. In the Name field, enter ZCL_##_SOLUTION, where ## stands for your group number.

    4. Choose Next.

    5. Confirm the transport request and choose Finish.

  2. Activate the copy.

    1. Press Ctrl + F3 to activate the class.

Task 2: Define a Factory Method

Use a quick fix to define a factory method for local class LCL_CARRIER (suggested name: GET_INSTANCE). In the factory method, perform the existence check and the authority check before you actually create an instance. Then restrict the instance creation for this class to enforce the usage of the factory method.

Steps

  1. In local class LCL_CARRIER, use a quick fix to define a factory method.

    Hint

    Constructor parameters are added to the signature of the factory method by default. Therefore, do not explicitly select the carrier_id attribute as an attribute to be initialized.
    1. Navigate to the definition of LCL_CARRIER.

    2. In the CLASS … DEFINITION statement, place the cursor on lcl_carrier and press Ctrl + 1 to invoke the available quick fixes.

    3. From the list of available quick fixes, choose Generate factory method create.

    4. In the Name field, enter GET_INSTANCE.

    5. In the Select attributes to initialize section, choose Deselect All, then choose Finish.

    6. The quick fix should have added the following method definition:

      ABAP
      12345
      CLASS-METHODS get_instance IMPORTING i_carrier_id TYPE /dmo/carrier_id RETURNING value(r_result) TYPE REF TO lcl_carrier.

      … and the following implementation:

      ABAP
      123
      METHOD get_instance. r_result = NEW #( i_carrier_id = i_carrier_id ). ENDMETHOD.
  2. Move the SELECT statement and the AUTHORITY-CHECK statement from the CONSTRUCTOR to the implementation of the factory method. Make sure your perform the checks before the instantiation.

    1. Navigate to the implementation of the CONSTRUCTOR method of local class LCL_CARRIER.

    2. Select the SELECT statement and the AUTHORITY-CHECK statement, including the evaluation of system field SY-SUBRC and the exception raising, and press Ctrl + C to copy the code snippet to the clipboard.

    3. Press Ctrl + < to add a comment sign in front of each selected code row.

    4. Navigate to the implementation of method GET_INSTANCE, place the cursor before the value assignment for parameter r_result and press Ctrl + V to insert the code snippet.

    5. The code should now look like this:

      ABAP
      12345678910111213141516171819202122
      METHOD get_instance. SELECT SINGLE FROM /lrn/carrier FIELDS concat_with_space( carrier_id, name, 1 ), currency_code WHERE carrier_id = @i_carrier_id INTO ( @me->name, @me->currency_code ). IF sy-subrc <> 0. RAISE EXCEPTION TYPE cx_abap_invalid_value. ENDIF. AUTHORITY-CHECK OBJECT '/LRN/CARR' ID '/LRN/CARR' FIELD i_carrier_id ID 'ACTVT' FIELD '03'. IF sy-subrc <> 0. RAISE EXCEPTION TYPE cx_abap_auth_check_exception. ENDIF. r_result = NEW #( i_carrier_id = i_carrier_id ). ENDMETHOD.
  3. Since you have removed the RAISE EXCEPTION statements from the implementation of the CONSTRUCTOR method, now delete the two corresponding RAISING additions in the signature of the CONSTRUCTOR method definition.

    1. Navigate to the definition of the CONSTRUCTOR method in LCL_CARRIER.

    2. Adjust the method definition as follows:

      ABAP
      123456
      METHODS constructor IMPORTING i_carrier_id TYPE /dmo/carrier_id * RAISING cx_abap_invalid_value * cx_abap_auth_check_exception .
  4. Return to the implementation of the GET_INSTANCE method. In the SELECT statement, adjust the INTO clause. Store the result in an inline declared structure (suggested name: details) instead of instance attributes name and currency_code.

    1. Adjust the code as follows:

      ABAP
      1234567
      SELECT SINGLE FROM /lrn/carrier FIELDS concat_with_space( carrier_id, name, 1 ) AS name, currency_code WHERE carrier_id = @i_carrier_id * INTO ( @me->name, @me->currency_code ). INTO @DATA(details).
  5. After the instantiation of the carrier object, assign the values from the details structure to the corresponding instance attributes of the new instance.

    1. Adjust the code as follows:

      ABAP
      12345
      r_result = NEW #( i_carrier_id = i_carrier_id ). r_result->name = details-name. r_result->currency_code = details-currency_code. ENDMETHOD.
  6. Use a quick fix to add raising declarations for the two exceptions.

    1. Choose the warning icon with the light bulb on the left-hand side of the first RAISE EXCEPTION statement to invoke the available quick fixes.

    2. From the list of available quick fixes, choose Add raising declaration.

    3. Repeat this for the other RAISE EXCEPTION statement.

    4. The quick fix should have adjusted the code as follows:

      ABAP
      12345678
      CLASS-METHODS get_instance IMPORTING i_carrier_id TYPE /dmo/carrier_id RETURNING value(r_result) TYPE REF TO lcl_carrier RAISING cx_abap_invalid_value cx_abap_auth_check_exception.
  7. Restrict the instantiation of local class LCL_CARRIER. Make sure instantiation is only allowed within the class itself.

    1. Navigate to the definition of LCL_CARRIER.

    2. Adjust the code as follows:

      ABAP
      1
      CLASS lcl_carrier DEFINITION CREATE PRIVATE.
  8. Move the definition of the CONSTRUCTOR method to the private section.

    Note

    This is optional, but should be done to improve readability.
    1. Copy and paste the METHODS statement for method CONSTRUCTOR from the public section to the private section.

  9. In the IF_OO_ADT_CLASSRUN~MAIN method of the global class, replace the NEW expression with a call of the factory method GET_INSTANCE.

    1. Adjust the code as follows:

      ABAP
      12
      * DATA(carrier) = NEW lcl_carrier( i_carrier_id = c_carrier_id ). DATA(carrier) = lcl_carrier=>get_instance( i_carrier_id = c_carrier_id ).
  10. In the CLASS_SETUP method of the local test class LTCL_FIND_FLIGHTS, replace the NEW expression with a call of the factory method GET_INSTANCE.

    1. Adjust the code as follows:

      ABAP
      12
      * the_carrier = NEW lcl_carrier( i_carrier_id = some_flight_data-carrier_id ). the_carrier = lcl_carrier=>get_instance( i_carrier_id = some_flight_data-carrier_id ).
  11. Activate and test your global class as console app. Debug the execution of the GET_INSTANCE method.

    1. Press Ctrl + F3.

    2. Press F9.

Task 3: Extend the Factory Method

In local class LCL_CARRIER, define a static attribute in which you can store references to the instances of LCL_CARRIER (suggested name: instances). Then extend the implementation of the factory method: After the creation of an instance, store the reference to the new instance in attribute instances. Only create a new instance if instances does not yet reference an object for the requested carrier ID.

Steps

  1. In local class LCL_CARRIER, use a quick fix to create a table type for this class (suggested name: tt_carriers).

    1. Navigate to the definition of local class LC_CARRIER.

    2. In the CLASS … DEFINITION statement, place the cursor on lcl_carrier and press Ctrl + 1 to invoke the available quick fixes.

    3. From the list of available quick fixes, choose Generate table type for lcl_carrier.

    4. In the new TYPES statement, replace tab with tt_carriers.

    5. The TYPES statement should now look like this:

      ABAP
      12
      TYPES: tt_carriers TYPE STANDARD TABLE OF REF TO lcl_carrier WITH DEFAULT KEY.
  2. Define a private static attribute based on the just created table type (suggested name: instances).

    1. Scroll down to the PRIVATE SECTION statement.

    2. Adjust the code as follows:

      ABAP
      12
      PRIVATE SECTION. CLASS-DATA instances TYPE tt_carriers.
  3. Adjust the implementation of the factory method GET_INSTANCE. After the instantiation, add a reference to the new instance to the instances attribute.

    1. Adjust the code as follows:

      ABAP
      123456
      r_result = NEW #( i_carrier_id = i_carrier_id ). r_result->name = details-name. r_result->currency_code = details-currency_code. APPEND r_result TO instances. ENDMETHOD.
  4. Before the instantiation, but after the existence and authority checks, implement a single record read from attribute instances. Read the instance in which the value of attribute carrier_id equals the value of import parameter i_carrier_id. Store the result in returning parameter r_result.

    Hint

    Remember that in internal tables with non-structured row type you address the data using pseudo component table_line.
    1. Adjust the code as follows:

      ABAP
      1234567891011
      IF sy-subrc <> 0. RAISE EXCEPTION TYPE cx_abap_auth_check_exception. ENDIF. r_result = instances[ table_line->carrier_id = i_carrier_id ]. r_result = NEW #( i_carrier_id = i_carrier_id ). r_result->name = details-name. r_result->currency_code = details-currency_code. APPEND r_result TO instances.
  5. Surround the single record read access, the instantiation, the value assignments to the attributes, and the APPEND statement with a TRY... ENDTRY control structure.

    1. Adjust the code as follows:

      ABAP
      123456789
      TRY. r_result = instances[ table_line->carrier_id = i_carrier_id ]. r_result = NEW #( i_carrier_id = i_carrier_id ). r_result->name = details-name. r_result->currency_code = details-currency_code. APPEND r_result TO instances. ENDTRY.
  6. Make sure you only create an instance and add it to attribute instances, if system exception CX_SY_ITAB_LINE_NOT_FOUND is raised.

    1. Adjust the code as follows:

      ABAP
      123456789
      TRY. r_result = instances[ table_line->carrier_id = i_carrier_id ]. CATCH cx_sy_itab_line_not_found. r_result = NEW #( i_carrier_id = i_carrier_id ). r_result->name = details-name. r_result->currency_code = details-currency_code. APPEND r_result TO instances. ENDTRY.
  7. In your global class, adjust the implementation of method IF_OO_ADT_CLASSRUN~MAIN: Implement a second call to the GET_INSTANCE factory method with the same input as the existing call, but a different reference variable for the result (suggested name: carrier2).

    1. Adjust the code as follows:

      ABAP
      12
      DATA(carrier) = lcl_carrier=>get_instance( i_carrier_id = c_carrier_id ). DATA(carrier2) = lcl_carrier=>get_instance( i_carrier_id = c_carrier_id ).
  8. Activate and test your global class as console app. Debug the code to ensure that the two calls of the factory method return the same instance of LCL_CARRIER.

    1. Press Ctrl + F3.

    2. Press F9.