如何有效地对在特定点上仅略有不同的共享代码片段进行建模?

How do I model shared code pieces differing only slightly at a specific point effectively?

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

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

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

  1. 摘要 cl_webservice_export 每个场景都有子class。覆盖包含实际调用的方法。
  2. cl_webservice_export 成员类型 if_webservice_call。 class 对于实施 if_webservice_call 方法的每个场景 call_webservice()
  3. 动态CALL METHOD webservice_instance->(method_name)里面 具体 cl_webservice_export 方法包含实际调用并将 (method_name) 传递给 cl_webservice_export.

我的代码: export_via_webservice 是由 cl_webservice_export 或通过 if_export

提供的 public 接口
      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,以及一个 class 为您需要的每个 Web 服务导出变体实现它,即 cl_webservice_export_variant_acl_webservice_export_variant_b 等。

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

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

您已经将此草拟为您的选项 2,并且还强调了它是最不适合您的场景的最佳解决方案。永远不欢迎代码重复。更是如此,因为您的网络服务调用在某些方法名称中仅略有不同。

综上所述,这个模式比较差,不宜主动选择。它通常会自行存在,当人们从变体 a 开始,几个月后通过复制粘贴现有的 class 添加变体 b,然后忘记重构代码以摆脱重复的部分。

策略模式

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

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

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

主要缺点是当第一个不完全适合的变体到达时,模式开始腐蚀,例如因为它不仅改变了方法名称,还改变了一些参数。然后,进化需要对 所有 涉及的 class 进行深入重新设计,这可能会产生相当大的成本。另一个缺点是继承设置会使编写单元测试变得很麻烦:例如,单元测试抽象 class 需要组成一个测试替身,子classes它并覆盖受保护的带有传感和模拟代码的方法 - 所有可能但不如 classes.

之间的接口那么整洁

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

作文

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

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

分离

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

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

这个选项听起来和你草拟的选项 3 很相似,但我不是 100% 确定。无论如何,这将是我投票支持的模式。