问题描述
我正在编写一个用于许多场景(供应商、客户、成本中心、REFX 合同等)的数据导出应用程序。 最后导出的方式主要有两种:保存到文件或者调用webservice。
所以我的想法是创建一个接口 if_export
,为每个场景实现一个类。
问题是,实际调用时调用 web 服务的代码略有不同:每次调用的方法都有不同的名称。
我目前处理这个问题的想法是:
- 抽象
cl_webservice_export
和每个场景的子类。覆盖包含实际调用的方法。 -
cl_webservice_export
成员类型为if_webservice_call
。实现if_webservice_call
方法call_webservice()
的每个场景的类
- 内部动态
CALL METHOD webservice_instance->(method_name)
包含实际调用并将(method_name)
传递给cl_webservice_export
的具体 cl_webservice_export 方法。
我的代码:
export_via_webservice
是 cl_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_a
、cl_webservice_export_variant_b
等)实现它的类
主要优点是直观简单的类设计和实现的完全独立性,避免了从一种变体到另一种变体的意外溢出。
主要缺点是不同变体之间可能存在大量代码重复,如果它们的代码仅在少数、次要位置发生变化。
您已经将其概述为选项 2,并且还强调了它是您场景中最不理想的解决方案。代码重复是不受欢迎的。更是如此,因为您的 Web 服务调用仅在某些方法名称上略有不同。
综上所述,这种模式比较差,不应该主动选择。它通常独立存在,当人们从变体 a 开始,几个月后通过复制粘贴现有类添加变体 b,然后忘记重构代码以去除重复部分。
策略模式
这种设计通常称为 strategy design pattern。创建一个接口 if_export
,以及一个实现该接口并包含大部分 Web 服务调用代码的 abstract
类 cl_abstract_webservice_export
。
除了这个细节:应该调用的方法的名称不是硬编码的,而是通过调用 protected
子方法 get_service_name
来检索的。抽象类不实现这个方法。相反,您创建抽象类的子类,即 cl_concrete_webservice_export_variant_a
、cl_concrete_webservice_export_variant_b
等。这些类仅实现继承的受保护方法 get_service_name
,提供它们的具体需求。
主要优点是这种模式完全避免了代码重复,对进一步扩展开放,并已成功应用于许多框架实现中。
主要缺点是当第一个不完全适合的变体到达时,模式开始侵蚀,例如因为它不仅会改变方法名称,还会改变一些参数。演进需要对所有相关类进行深入的重新设计,这可能会产生相当大的成本。另一个缺点是继承设置可能会使编写单元测试变得很麻烦:例如,对抽象类进行单元测试需要组成一个测试替身,将其子类化并使用感知和模拟代码覆盖受保护的方法 - 所有这一切都是可能的但不如类之间的接口那么整齐。
您已经将此草图作为您的选项 1。总而言之,如果您可以控制所有涉及的类并且愿意花费一些额外的努力来保持模式清洁,以防万一,我建议选择此模式完全合身。
组成
组合意味着避免继承,有利于独立类之间的松散交互。创建接口 if_export
及其单独的具体实现,如 cl_webservice_export_variant_a
、cl_webservice_export_variant_b
等
将共享代码移到类 cl_export_webservice_caller
中,该类接收它需要的任何数据和变体(例如方法名称)。让变体类调用这个共享代码。要完成类设计,请引入另一个接口 if_export_webservice_caller
,将变体类与调用者类分离。
主要优点是所有的类都是相互独立的,并且可以通过几种不同的方式重新组合。例如,如果将来您需要引入一个变体 X 以完全不同的方式调用其 Web 服务,您可以简单地添加它,而无需重新设计任何其他涉及的类。与策略模式相比,为所有涉及的类编写单元测试是微不足道的。
这种模式没有真正的缺点。 (它需要一个更多接口的表面上的缺点并不是真正的缺点 - 面向对象的目的是清楚地分离关注点,而不是最小化类/接口的总数,如果需要的话,我们不应该害怕添加更多的增加了整体设计的清晰度。)
此选项听起来与您绘制的选项 3 相似,但我不能 100% 确定。无论如何,这将是我会投票支持的模式。