在 ABAP 中编写更好代码的通用规则

遵循关注点分离原则

将程序分成多个单元,使各个单元的功能之间的重叠最小化。

关注点分离

"关注点分离"(Separation of Concerns,SoC)一词是埃兹格·W·迪杰斯特拉(Edsger W. Dijkstra)在 1974 年发表的《论科学思想的作用》一文中提出的:

"......但是,同时处理这些不同方面的问题并没有任何好处,恰恰相反!。这就是我有时所说的'关注点的分离',即使并非完全可行,但据我所知,这也是有效整理思维的唯一可用技巧。这就是我所说的'把注意力集中在某个方面':这并不意味着忽视其他方面,只是公正地对待这样一个事实,即从这个方面来看,其他方面都是无关紧要的"。(施普林格出版社,1982 年)

关注点分离是程序设计中使用的一种原则,它将应用程序分成若干单元,尽量减少各单元功能之间的重叠/耦合。关注点分离是通过模块化、封装和软件层安排来实现的。

虽然应用服务器 ABAP(AS ABAP)的经典三层架构非常适合基于 SoC 原则的 ABAP 编程,但这种可能性从未被探索过。应用程序(换句话说,模块池中的对话程序或可执行程序中的报表)通常以单块形式显示,系统同时对表现层的用户操作做出反应,完成应用逻辑,并执行对持久层数据的访问。

在当今的编程世界中,这种编程方式已不再适用,面向服务的架构(SOA)等概念已成为趋势

原则

遵循关注点分离原则。将应用程序严格建模为面向服务 的应用程序。尤其重要的是,要将应用程序的逻辑与表现逻辑、持久性以及与外部系统通信的逻辑分开。将各个关注点的存储库对象封装在单独的包中。

优点

SoC 原理确定了应用程序中具有特定用途的部分,并将这些部分封装在封闭的单元中。这些单元之间只使用指定的接口进行通信。由于采用了这一原则,原本过于复杂的软件被划分为易于管理的组件。因此,软件

  • 更稳定
  • 更容易理解
  • 更易于重复使用
  • 更易于传输
  • 更易于维护
  • 更易于测试

关于最后一点,甚至可以说,遵循 SoC 原则是执行隔离的自动化单元测试的前提条件。

不好的使用方式

下图显示了两个不遵循 SoC 原则的 ABAP 应用程序示例。

事实上,这里的两个坏例子是 SAP 在相当长的时间内传输的报告和对话编程的程序员模型!更准确地说,这个例子的糟糕之处不在于报告或事务编程本身,而在于这些应用程序通常的实现方式。下面源代码中的小 Report 程序就是一个典型的例子,说明在一个程序单元中,不同的关注点是如何混合在一起的。数据声明和函数实现都混合在一起。对持久数据的访问、数据处理、数据展示和相关声明都在一个单元中进行

ABAP 复制代码
REPORT z_non_soc_report.

PARAMETERS p_carrid TYPE spfli-carrid.

DATA: spfli_tab TYPE STANDARD TABLE OF spfli,
      alv     TYPE REF TO cl_salv_table,
      alv_exc TYPE REF TO cx_salv_msg.

SELECT *
       FROM spfli
       WHERE carrid = @p_carrid
       INTO TABLE @spfli_tab.
       
IF sy-subrc = 0.
  SORT spfli_tab BY cityfrom cityto.
  TRY.
      cl_salv_table=>factory(
        IMPORTING r_salv_table = alv
        CHANGING t_table = spfli_tab ).
      
      alv->display( ).
    CATCH cx_salv_msg INTO alv_exc.
      MESSAGE alv_exc TYPE 'I' DISPLAY LIKE 'E'.
  ENDTRY.
ENDIF.

当然,如果坚持认为即使是像上述源代码这样的短程序也应完全分离关注点,那就太过分了。然而,实际应用程序通常是很长的 ABAP 程序(可执行程序、模块池),其中所有关注点都是同时处理的。如果要进行模块化,通常仅限于重复使用功能单元,而很少关注实际可用层。此外,通常会创建大量的全局数据,用于不同的程序和层。因此,程序的所有部分在本质上相互依赖,无法单独进行测试。我们深信,这些程序的质量不仅可以通过遵循命名约定来提高,还可以通过改变用于编程任务的程序范式来提高。

下面的源代码证明,您可以使用经典的 ABAP 过程方法(本例中为子程序)实现 SoC 原理。该源代码的工作方式与上述源代码相同。不过,所有关注点都是在分配给各层的独立过程中实现的。正如我们已经提到的,这种实现方式对于一个简单的程序来说太过复杂。但是,如果需要使用单元测试对上述源代码中的关注点进行独立测试,唯一的可能就是对源代码进行如下调整。下面源代码中的程序现在可以很容易地在 ABAP 单元测试类中分配测试方法,从而测试各个程序。

ABAP 复制代码
REPORT z_soc_report.

SELECTION-SCREEN BEGIN OF SCREEN 100.
PARAMETERS p_carrid TYPE spfli-carrid.
SELECTION-SCREEN END OF SCREEN 100.

TYPES spfli_tab TYPE STANDARD TABLE OF spfli.

DATA: carrid TYPE spfli-carrid,
      table  TYPE spfli_tab,
      arc     TYPE sy-subrc.
      
START-OF-SELECTION.
  PERFORM get_carrid CHANGING carrid.
  PERFORM get_table  USING    carrid
                     CHANGING table
                              arc.
IF arc = 0
  PERFORM sort_table    CHANGING table.
  PERFORM display_table USING    table.
ENDIF.

* Presentation layer 表示层
FORM get_carrid
     CHANGING value(carrid) TYPE spfli-carrid.
  CALL SELECTION-SCREEN 100.
  IF sy-subrc = 0.
    carrid = p_carrid.
  ENDIF.
ENDFORM.

FORM display_table
     USING table TYPE spfli_tab.
  DATA: alv TYPE      REF TO cl_salv_table,
        alv_exc TYPE REF TO cx_salv_msg.
  TRY.
     cl_salv_table=>factory(
       IMPORTING r_salv_table = alv
       CHANGING t_table = table ).
     
     alv->display( ).
   CATCH cx_salv_msg INTO alv_exc.
     MESSAGE alv_exc TYPE 'I' DISPLAY LIKE 'E'.
  ENDTRY.
ENDFORM.

* Application layer 应用层
FORM sort_table
     CHANGING table TYPE spfli_tab.
     SORT table BY cityfrom cityto.
ENDFORM.

* Persistency layer 持久层
FORM get_table
     USING     carrid TYPE spfli-carrid
     CHANGING table   TYPE spfli_tab
              arc     TYPE sy-subrc.
  SELECT *
         FROM spfli
         WHERE carrid = @carrid
         INTO TABLE @table.
  arc = sy-subrc.
ENDFORM.

不过,上面这种使用子程序来分离关注点的做法并不能给人留下好印象。下面的源代码展示了如何使用特定关注点类中的方法来实现关注点分离。

良好的使用方式

下图显示了关注点分离后 ABAP 应用程序的样子。

在确定关注点后,这些关注点将在 ABAP 对象类中实现。图中所示的关注点是 ABAP 应用程序编程中通常执行的主要任务:

  • 使用 UI 服务与用户界面 (UI) 通信
  • 实际应用逻辑
  • 使用持久性服务访问持久性数据
  • 使用代理服务与外部系统通信

这些主要规则可以进一步细分,这通常是必要的。

图中各个关注点的方框代表软件包。属于某个关注点的所有存储库对象(类、数据类型)都应位于相应的包中。包概念(封装包)支持这种关注点分离。在封装包中,一个包中的资源库对象只能使用包接口访问另一个包中的对象,这一点会通过语法检查进行检查。一个软件包可以通过使用访问控制列表进一步限制其资源库对象的可用性。子软件包支持在软件包中细分关注点。

  • 例如,将应用程序的所有数据库表封装在持久性服务包中,可以防止任何不属于该包的程序访问这些数据库表。反之亦然。

  • 例如,持久层中的程序不能与表现层中的组件(如 Web Dynpro ABAP 应用程序)直接通信。您应该在包属性中准备包封装(选择包检查为服务器)。这些包具有包接口,在扩展程序检查过程中会执行包检查。

下面的源代码显示了如何从上述源代码中为程序本地类调整关注点分离。

  1. 首先定义数据:
erlang 复制代码
REPORT z_soc_class_report.

SELECTION-SCREEN BEGIN OF SCREEN 100.
PARAMETERS p_carrid TYPE spfli-carrid.
SELECTION-SCREEN END OF SCREEN 100.

TYPES spfli_tab TYPE STANDARD TABLE OF spfli.
  1. 定义和实现表示层服务类:
ABAP 复制代码
CLASS presentation_server DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS:
      get_carrid RETURNING VALUE(carrid) TYPE spfli-carrid,
      display_table IMPORTING VALUE(table) TYPE spfli_tab.
ENDCLASS.
  1. 实现表示层服务类
ABAP 复制代码
CLASS presentation_server IMPLEMENTATION.
  METHOD get_carrid.
    CALL SELECTION-SCREEN 100.
    IF sy-subrc = 0.
      carrid = p_carrid.
    ENDIF.
  ENDMETHOD.
  
  METHOD display_table.
    DATA: alv     TYPE REF TO cl_salv_table,
          alv_exc TYPE REF TO cx_salv_msg.
    TRY.
       cl_salv_table=>factory(
         IMPORTING r_salv_table = alv
         CHANGING t_table = table ).
        alv->display( ).
      CATCH cx_salv_msg INTO alv_exc.
        MESSAGE alv_exc TYPE 'I' DISPLAY LIKE 'E'.
     ENDTRY.
   ENDMETHOD.
ENDCLASS.
  1. 定义和实现应用层服务类:
ABAP 复制代码
CLASS application_server DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS
       sort_table CHANGING table TYPE spfli_tab.
    ENDCLASS.
    
CLASS application_server IMPLEMENTATION.
  METHOD sort_table.
    SORT table BY cityfrom cityto.
  ENDMETHOD.
ENDCLASS.
  1. 定义和实现持久层服务类:
ABAP 复制代码
CLASS persistency_server DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS
       get_table IMPORTING carrid TYPE spfli-carrid
                 EXPORTING table  TYPE spfli_tab
                           arc     TYPE sy-subrc.
ENDCLASS.

CLASS persistency_server IMPLEMENTATION.
  METHOD get_table.
   SELECT *
          FROM spfli
          WHERE carrid = @carrid
          INTO TABLE @table.
    arc = sy-subrc.
  ENDMETHOD.
ENDCLASS.
  1. 主程序
ABAP 复制代码
CLASS report DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS main.
ENDCLASS.

CLASS report IMPLEMENTATION.
  METHOD main.
    DATA: carrid TYPE spfli-carrid,
          table  TYPE spfli_tab,
          arc     TYPE sy-subrc.
    carrid = presentation_server=>get_carrid( ).
    persistency_server=>get_table( EXPORTING carrid = carrid
                                   IMPORTING table  = table
                                             arc     = arc ).
    IF arc = 0.
      application_server=>sort_table(
        CHANGING table = table ).
      presentation_server=>display_table( table ).
    ENDIF.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
report=>main( ).

乍一看,与第一个源代码相比,上述源代码显得非常多余。但这只是第一眼。一个真实的应用程序通常只有 25 行。应用程序越大、越现实,将关注点封装在类中所产生的开销比例就越小。如果 ABAP 对象的重用选项使用得当,甚至有可能减少源代码的数量。

此外,单个步骤现在被封装在类中,换句话说,是真正的程序单元(与第二个源代码不同)。实际上,封装并不是在一个程序中进行的,而是在全局类中进行的,这些全局类根据层的不同被分配到不同的包中。这些包通过包接口相互连接。只有使用这些接口,才能实现分离关注点的其他好处(除了在第二个源代码中实现的测试功能)。

参看官方链接:Separation of Concerns

编写能描述自我的代码

好的代码应该能够自我解释。自描述代码不需要那么多注释或大量文档。

让你编写的代码不言自明,例如:

  • 为变量、函数、类等选择有意义的名称(lv_ilv_indexlt_datalt_sale_orderscl_util , start()run_accounting_document_processing()
  • 将逻辑步骤转换为方法(例如,将方法 process_document() 拆分为一系列方法 prepare_document_data()is_doc_creation_possible()lock_tables()create_document()unlock_table() 等)
  • 减少编程块中的行数
  • 降低嵌套级别
  • 避免模糊不清

注释应该写你做了什么,而不是你如何做

不要对实现方面进行注释,自我描述的代码会为读者提供这方面的信息。从业务流程的角度注释逻辑,即读者无法从代码中提取的信息。

在最好的情况下,在方法头中或方法调用之前对业务逻辑单元进行简短的描述就足够了。

尽可能声明局部变量

创建范围尽可能小的变量、方法和属性。变量/方法的范围越大,程序的耦合程度就越高。

不要使用魔法数字

避免硬编码常量或未命名变量。

相反,将它们移至有意义的变量或常量中。请注意,仅仅移动具有相同名称的文本文字是不够的( ABC123lc_abc123 ),给它一个正确的描述( ABC123lc_storage_class)

不好的方式:

ABAP 复制代码
lo_doc_processor->change_document(
  iv_blart = 'AB'
  iv_bukrs = 'C123'
  iv_popup = lv_x
).

好的方式:

ABAP 复制代码
CONSTANTS:
  lc_clearing_document TYPE blart VALUE 'AB',
  lc_main_company      TYPE bukrs VALUE 'C123'.
DATA:
  lv_show_popup TYPE abap_bool.

*...
lo_doc_processor->change_document(
  iv_blart = lc_clearing_document
  iv_bukrs = lc_main_company
  iv_popup = lv_show_popup
).
相关推荐
捂月8 分钟前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn33 分钟前
依赖注入注解
java·后端·spring
Estar.Lee1 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪1 小时前
Django:从入门到精通
后端·python·django
一个小坑货1 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet271 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
uhakadotcom1 小时前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
Iced_Sheep2 小时前
干掉 if else 之策略模式
后端·设计模式
XINGTECODE2 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶2 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露