实施工厂方法

学习目标

  • 使用工厂方法。

工厂方法

有时,类需要控制其实例。这可能是为了防止创建多个实例。为此,必须阻止其用户自己创建实例。通过在 CLASS DEFINITION 语句中使用 CREATE PRIVATE 附加在 ABAP 中执行此操作。现在,任何在 类外使用 NEW 运算符的尝试都将导致语法错误。请注意,其他编程语言通过更改构造函数方法的可见性来实现此效果。ABAP 不支持此操作。

由于 类的用户无法创建自己的实例,因此 类必须提供公共静态方法,用于创建实例并将其返回给用户。这称为工厂方法。

试用:工厂方法实施

  1. 创建实施接口 IF_OO_ADT_CLASSRUN 的新类。

  2. 切换到 局部类型 标签并将以下代码片段复制到编辑器中:

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.
  1. 切换到 全局类 标签并将以下代码片段粘贴到方法 if_oo_adt_classrun~main() 的实施中:

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.
  1. 使用 Ctrl + F3 激活类。

  2. 通过编码进行导航。特别是,调试 main() 方法并记录分配给工厂方法返回对象的内部实例编号。

单例模式

单例模式旨在确保应用程序中只有一个类实例。这可以是类的单个实例,在我们的案例中,可以是每个关键属性组合的单个实例。

观看以下视频,了解有关单例模式的详细信息。

视频:本节含 SAP 官方视频(视频 ID 1_6tqf4vhw),需在 learning.sap.com 在线观看。

视频:本节含 SAP 官方视频(视频 ID 1_dmvj9k29),需在 learning.sap.com 在线观看。

视频:本节含 SAP 官方视频(视频 ID 1_w69iw7td),需在 learning.sap.com 在线观看。

试用:单例模式实施

  1. 创建实施接口 IF_OO_ADT_CLASSRUN 的新类。

  2. 切换到 局部类型 标签并将以下代码片段复制到编辑器中:

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.
  1. 切换到 全局类 标签并将以下代码片段粘贴到方法 if_oo_adt_classrun~main() 的实施中:

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.
  1. 使用 Ctrl + F3 激活类。

  2. 通过编码进行导航。特别是,调试 main() 方法并记录分配给工厂方法返回对象的内部实例编号。

实施工厂方法

您注意到当前形式的代码允许存在代表同一航空公司的多个 LCL_CARRIER 实例。要修复此问题,您将定义并实施工厂方法,以确保只能为给定承运方标识值创建一个实例。

模板:

  • /LRN/CL_S4D401_OOS_INTF_USAGE(全局类)

解决方案:

  • /LRN/CL_S4D401_OOS_FACTORY(全局类)

任务 1: 复制模板(可选)

复制模板类 /LRN/CL_S4D401_OOS_INTF_USAGE。如果已完成上一练习,则可以跳过此任务并继续编辑类 ZCL_##_SOLUTION

步骤

  1. 将类 /LRN/CL_S4D401_OOS_INTF_USAGE 复制到您自己的包中的类(建议名称:ZCL_##_SOLUTION,其中 ## 代表组编号)。

  2. 在项目浏览器中,右键单击类 /LRN/CL_S4D401_OOS_INTF_USAGE 以打开上下文菜单。

  3. 从上下文菜单中,选择复制....

  4. 在包字段中输入的名称。在名称字段中,输入 ZCL_##_SOLUTION,其中 ## 代表组编号。

  5. 选择下一步

  6. 确认传输请求并选择完成

  7. 激活复制。

  8. Ctrl + F3 激活类。

任务 2: 定义工厂方法

使用快速修复为本地类 LCL_CARRIER 定义工厂方法(建议名称:GET_INSTANCE)。在工厂方法中,在实际创建实例之前执行存在性检查和权限检查。然后限制此类的实例创建以强制使用工厂方法。

步骤

  1. 在局部类 LCL_CARRIER 中,使用快速修复定义工厂方法。

提示

缺省情况下,将构造函数参数添加到工厂方法的签名中。因此,不要显式选择 carrier_id 属性作为要初始化的属性。

  1. 导航至 LCL_CARRIER 的定义。

  2. 在 CLASS…DEFINITION 语句中,将光标放在 lcl_carrier 上并按 Ctrl + 1 调用可用的快速修复。

  3. 从可用快速修复列表中,选择生成工厂方法创建

  4. 名称字段中,输入 GET_INSTANCE

  5. 选择要初始化的属性部分,选择取消全选,然后选择完成

  6. 快速修复应已添加以下方法定义:

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

…以及以下实施:

METHOD get_instance.
r_result = NEW #( i_carrier_id = i_carrier_id ).
ENDMETHOD.
  1. 将 SELECT 语句和 AUTHORITY-CHECK 语句从 CONSTRUCTOR 移动到工厂方法的实施。确保在实例化之前执行检查。

  2. 导航到本地类 LCL_CARRIERCONSTRUCTOR 方法的实施。

  3. 选择 SELECT 语句和 AUTHORITY-CHECK 语句,包括系统字段 SY-SUBRC 的评估以及引发异常,然后按 Ctrl + C 将代码片段复制到剪贴板。

  4. Ctrl + < 在每个所选代码行前添加注释符号。

  5. 导航到方法 GET_INSTANCE 的实施,将光标置于参数 r_result 的值分配之前,然后按 Ctrl + V 插入代码片段。

  6. 代码现在应如下所示:

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.
  1. 由于已从 CONSTRUCTOR 方法的实施中删除 RAISE EXCEPTION 语句,因此现在删除 CONSTRUCTOR 方法定义签名中的两个相应 RAISING 附加项。

  2. 导航至 LCL_CARRIERCONSTRUCTOR 方法的定义。

  3. 按如下所示调整方法定义:

METHODS constructor
IMPORTING
i_carrier_id TYPE /dmo/carrier_id
*    RAISING cx_abap_invalid_value
*            cx_abap_auth_check_exception
.
  1. 返回到 GET_INSTANCE 方法的实施。在 SELECT 语句中,调整 INTO 子句。将结果存储在内联声明结构中(建议名称:详细信息),而不是实例属性**名称**和 currency_code

  2. 如下调整代码:

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).
  1. 实例化运输公司对象后,将**详细信息**结构中的值分配到新实例的相应实例属性。

  2. 如下调整代码:

r_result = NEW #( i_carrier_id = i_carrier_id ).
r_result->name          = details-name.
r_result->currency_code = details-currency_code.
ENDMETHOD.
  1. 使用快速修复为这两个异常添加引发声明。

  2. 选择第一个 RAISE EXCEPTION 语句左侧带有灯泡的警告图标以调用可用的快速修复。

  3. 从可用快速修复列表中,选择添加引发声明

  4. 对其他 RAISE EXCEPTION 语句重复此操作。

  5. 快速修复应按如下方式调整代码:

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.
  1. 限制本地类 LCL_CARRIER 的实例化。确保仅在类本身内允许实例化。

  2. 导航至 LCL_CARRIER 的定义。

  3. 如下调整代码:

CLASS lcl_carrier DEFINITION CREATE PRIVATE.
  1. CONSTRUCTOR 方法的定义移动到专用部分。

注意

这是可选的,但为了提高可读性,应该这样做。

  1. 将方法 CONSTRUCTOR 的 METHODS 语句从公共部分复制并粘贴到专用部分。

  2. 在全局类的 IF_OO_ADT_CLASSRUN~MAIN 方法中,将 NEW 表达式替换为工厂方法 GET_INSTANCE 的调用。

  3. 如下调整代码:

*  DATA(carrier) = NEW lcl_carrier(  i_carrier_id = c_carrier_id ).
DATA(carrier) = lcl_carrier=>get_instance(  i_carrier_id = c_carrier_id ).
  1. 在本地测试类 LTCL_FIND_FLIGHTSCLASS_SETUP 方法中,将 NEW 表达式替换为工厂方法 GET_INSTANCE 的调用。

  2. 如下调整代码:

*  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 ).
  1. 以控制台应用形式激活并测试全局类。调试 GET_INSTANCE 方法的执行。

  2. Ctrl + F3

  3. F9

任务 3: 扩展工厂方法

在局部类 LCL_CARRIER 中,定义一个静态属性,可在其中存储对 LCL_CARRIER 实例的参考(建议名称:实例)。然后扩展工厂方法的实施:创建实例后,将新实例的参考存储在属性**实例中。仅当实例尚未参考请求承运方标识的对象时,才创建新实例**。

步骤

  1. 在局部类 LCL_CARRIER 中,使用快速修复为此类创建表类型(建议名称:tt_carriers)。

  2. 导航到本地类 LC_CARRIER 的定义。

  3. 在 CLASS…DEFINITION 语句中,将光标放在 lcl_carrier 上并按 Ctrl + 1 调用可用的快速修复。

  4. 从可用快速修复列表中,选择为 lcl_carrier 生成表类型

  5. 在新的 TYPES 语句中,将 tab 替换为 tt_carriers

  6. TYPES 语句现在应如下所示:

TYPES: tt_carriers TYPE STANDARD TABLE OF REF TO lcl_carrier
WITH DEFAULT KEY.
  1. 根据刚创建的表类型(建议名称:实例)定义专用静态属性。

  2. 向下滚动至 PRIVATE SECTION 语句。

  3. 如下调整代码:

PRIVATE SECTION.
CLASS-DATA instances TYPE tt_carriers.
  1. 调整工厂方法 GET_INSTANCE 的实施。实例化后,将新实例的引用添加到 inst**anc**es 属性。

  2. 如下调整代码:

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.
  1. 在实例化之前,但在存在性和权限检查后,实施从属性**实例**中读取的单个记录。读取属性 carrier_id 的值等于导入参数 i_carrier_id 值的实例。将结果存储在返回参数 r_result 中。

提示

请记住,在具有非结构化行类型的内部表中,使用伪组件 table_line 寻址数据。

  1. 如下调整代码:

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.
  1. 将单个记录读取访问、实例化、属性值分配以及 APPEND 语句与 TRY...ENDTRY 控制结构。

  2. 如下调整代码:

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.
  1. 如果引发系统异常 CX_SY_ITAB_LINE_NOT_FOUND,请确保仅创建实例并将其添加到属性**实例**。

  2. 如下调整代码:

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.
  1. 在全局类中,调整方法 IF_OO_ADT_CLASSRUN~MAIN 的实施:使用与现有调用相同的输入,但结果有不同的参考变量(建议名称:carrier2)对 GET_INSTANCE 工厂方法进行第二次调用。

  2. 如下调整代码:

DATA(carrier)  = lcl_carrier=>get_instance(  i_carrier_id = c_carrier_id ).
DATA(carrier2) = lcl_carrier=>get_instance(  i_carrier_id = c_carrier_id ).
  1. 以控制台应用形式激活并测试全局类。调试代码以确保工厂方法的两次调用返回相同的 LCL_CARRIER 实例。

  2. Ctrl + F3

  3. F9