如何有效地对在特定点仅略有不同的共享代码段建模? 个别实现策略模式组成

问题描述

我正在编写一个用于许多场景(供应商、客户、成本中心、REFX 合同等)的数据导出应用程序。 最后导出的方式主要有两种:保存到文件或者调用webservice。

所以我的想法是创建一个接口 if_export,为每个场景实现一个类。 问题是,实际调用调用 web 服务的代码略有不同:每次调用方法都有不同的名称

我目前处理这个问题的想法是:

  1. 抽象 cl_webservice_export 和每个场景的子类。覆盖包含实际调用方法
  2. cl_webservice_export 成员类型为 if_webservice_call。实现 if_webservice_call 方法 call_webservice()
  3. 的每个场景的类
  4. 内部动态CALL METHOD webservice_instance->(method_name) 包含实际调用并将 (method_name) 传递给 cl_webservice_export 的具体 cl_webservice_export 方法

我的代码export_via_webservicecl_webservice_export 或通过 if_export

提供的公共接口
      METHODS export_via_webservice
      IMPORTING
        VALUE(it_xml_strings)    TYPE tt_xml_string_table
        io_service_consumer      TYPE REF TO ztnco_service_vmsoap
      RETURNING
        VALUE(rt_export_results) TYPE tt_xml_string_table.


        METHOD export_via_webservice.
    
        LOOP AT it_xml_strings INTO DATA(lv_xml_string).
          call_webservice(
            EXPORTING
              io_service    = io_service_consumer
              iv_xml_string = lv_xml_string-xmlstring
            RECEIVING
              rv_result     = DATA(lv_result)
          ).
          rt_export_results = VALUE #( BASE rt_export_results (
                                                lifnr = lv_xml_string-xmlstring
                                                xmlstring = lv_result ) ).
        ENDLOOP.
    
      ENDMETHOD.

if_webservice_call 覆盖或提供的实际网络服务调用

    METHODS call_webservice
      IMPORTING
        io_service       TYPE REF TO ztnco_service_vmsoap
        iv_xml_string    TYPE string
      RETURNING
        VALUE(rv_result) TYPE string.

    METHOD call_webservice.
    TRY.
        io_service->import_creditor(
          EXPORTING
            input              = VALUE #( xml_creditor_data = iv_xml_string )
          IMPORTING
            output             = DATA(lv_output)
        ).
      CATCH cx_ai_system_fault INTO DATA(lx_exception).
    ENDTRY.

    rv_result = lv_output-import_creditor_result.

    ENDMETHOD.

您将如何解决这个问题,也许还有其他更好的方法

解决方法

我知道解决这个问题的三种常见模式。它们按质量升序排列:

个别实现

创建一个接口 if_export,以及一个为您需要的每个 Web 服务导出变体(即 cl_webservice_export_variant_acl_webservice_export_variant_b 等)实现它的类

enter image description here

主要优点是直观简单的类设计和实现的完全独立性,避免了从一种变体到另一种变体的意外溢出。

主要缺点是不同变体之间可能存在大量代码重复,如果它们的代码仅在少数、次要位置发生变化。

您已经将其概述为选项 2,并且还强调了它是您场景中最不理想的解决方案。代码重复是不受欢迎的。更是如此,因为您的 Web 服务调用仅在某些方法名称上略有不同。

综上所述,这种模式比较差,不应该主动选择。它通常独立存在,当人们从变体 a 开始,几个月后通过复制粘贴现有类添加变体 b,然后忘记重构代码以去除重复部分。

策略模式

这种设计通常称为 strategy design pattern。创建一个接口 if_export,以及一个实现该接口并包含大部分 Web 服务调用代码的 abstractcl_abstract_webservice_export

除了这个细节:应该调用的方法的名称不是硬编码的,而是通过调用 protected 子方法 get_service_name 来检索的。抽象类实现这个方法。相反,您创建抽象类的子类,即 cl_concrete_webservice_export_variant_acl_concrete_webservice_export_variant_b 等。这些类仅实现继承的受保护方法 get_service_name,提供它们的具体需求。

enter image description here

主要优点是这种模式完全避免了代码重复,对进一步扩展开放,并已成功应用于许多框架实现中。

主要缺点是当第一个不完全适合的变体到达时,模式开始侵蚀,例如因为它不仅会改变方法名称,还会改变一些参数。演进需要对所有相关类进行深入的重新设计,这可能会产生相当大的成本。另一个缺点是继承设置可能会使编写单元测试变得很麻烦:例如,对抽象类进行单元测试需要组成一个测试替身,将其子类化并使用感知和模拟代码覆盖受保护的方法 - 所有这一切都是可能的但不如类之间的接口那么整齐。

您已经将此草图作为您的选项 1。总而言之,如果您可以控制所有涉及的类并且愿意花费一些额外的努力来保持模式清洁,以防万一,我建议选择此模式完全合身。

组成

组合意味着避免继承,有利于独立类之间的松散交互。创建接口 if_export 及其单独的具体实现,如 cl_webservice_export_variant_acl_webservice_export_variant_b

将共享代码移到类 cl_export_webservice_caller 中,该类接收它需要的任何数据和变体(例如方法名称)。让变体类调用这个共享代码。要完成类设计,请引入另一个接口 if_export_webservice_caller,将变体类与调用者类分离。

enter image description here

主要优点是所有的类都是相互独立的,并且可以通过几种不同的方式重新组合。例如,如果将来您需要引入一个变体 X 以完全不同的方式调用其 Web 服务,您可以简单地添加它,而无需重新设计任何其他涉及的类。与策略模式相比,为所有涉及的类编写单元测试是微不足道的。

这种模式没有真正的缺点。 (它需要一个更多接口的表面上的缺点并不是真正的缺点 - 面向对象的目的是清楚地分离关注点,而不是最小化类/接口的总数,如果需要的话,我们不应该害怕添加更多的增加了整体设计的清晰度。)

此选项听起来与您绘制的选项 3 相似,但我不能 100% 确定。无论如何,这将是我会投票支持的模式。