通过接口标准化ABAP OO开发
本文是对接口编程的讨论,希望能对年轻的开发者有所帮助。
要点:
- 通过接口对类方法进行更高层的抽象
- 接口使代码清晰易读
- 接口使你可以创建模拟对象(Mockup Object)以提高代码的可测试性
- 帮助实现SOLID原则
- 可以在不使用RTTS和类型转换的前提下使用多种类的不同实例。
因为在学习ABAP之前,我曾经学习过其它面向对象语言,因此我很纠结于ABAP中不存在的一个特性——重载方法(overload)。
也许你会问,重载是什么?
重载就是函数或者方法有相同的名称,但是参数列表和实现不相同的情形。
没有了重载,在某种程度上,类也许会变的过大,并且难以追踪那些有着相似行为但是名字不同的方法。
接口不提供重载能力,但是通过限制名字不同但是功能相近的方法的数量,接口可以整理和简化你的代码。
本文链接:http://www.cnblogs.com/hhelibeb/p/8919767.html
英文原文:Using interfaces to standardize your ABAP OO Development
简介
在ABAP中类的继承是单一继承(每个类只能有一个父类),接口实现可以有多个。
例如,上图中的LCL_Child_Class继承LCL_Parent_Class中所有的非私有变量、方法、类型和常量,并且必须实现LINF_Utility和LINF_Saver接口中所有的功能。
为了解释接口的定义,我将使用个“不怎么专业”的描述——它是一个类似于类的实体,不包含所声明的方法的任何具体实现,但是它可能包含常量、类型和变量。接口无法被初始化。
默认情况下接口的所有方法都必须被实现——这是面向对象编程中的一个通常的强制规则。不能允许接口方法的实现变得可选择,但是这不属于本文的讨论范围,所以不会展开论述。
(译注:原文评论指出,在ABAP中,可以使用DEFAULT IGNORE|FAIL附加项指定可选的接口方法,虽然好像并没有什么用)
“真实”用例
设想下我们有个程序,需要从多种数据源获取数据并更新到表SFLIGHT:
- Excel上传
- RFC上传
- 在程序运行期间上传修改和插入的行
当然我们可以在该清单中添加ADBC源、经由HTTP客户端对象抓取的JSON/XML源等,但是我只是想介绍下要点,没必要穷举所有例子。
同时,因为本文只是对可能性的表述,因此我不会创建一个能真正工作的程序。
声明接口
我们将创建2个接口,不过在这个例子里只有一个是真实需要的。
第一个是最重要的,我命名它为linf_sflight_career,因为这是个用于EXCEL、RFC和本地表运输(carrier)的本地接口,在本地类中实现。
interface linf_sflight_carrier. types: tt_sflight type standard table of sflight with default key, st_sflight type sorted table of sflight with non-unique key mandt carrid connid, ht_sflight type hashed table of sflight with unique key mandt carrid connid fldate. methods: "! Returns hashed table SFLIGHT contents "! @parameter r_sflight | get_hashed_records returning value(r_sflight) type ht_sflight, "! Returns sorted table SFLIGHT contents "! @parameter r_sflight | get_sorted_records returning value(r_sflight) type st_sflight, "! Returns standard table SFLIGHT contents "! @parameter r_sflight | get_standard_records returning value(r_sflight) type tt_sflight. endinterface.
接口包含不同的表类型和三个方法,将会在EXCEL、RFC和表运输的类中实现。
下个接口由负责保存数据到数据库的类实现:
interface linf_sflight_saver. constants: "! Table lock types begin of lock_types, exclusive type enqmode value 'E', end of lock_types. constants: "! Scopes for table lock begin of scope_range, _2 type char01 value '2', end of scope_range. constants: _sflight type tablename value 'SFLIGHT'. methods: "! Save data from carrier object to SFLIGHT table "! @parameter i_carrier | Carrier object save_data importing i_carrier type ref to linf_sflight_carrier. endinterface.
在这里,你也许会问,为什么我们需要这么多类来完成一个很简单的工作?为什么我们不利用相似的类继承或者是单个类来实现目的?
答案是显然的:SOLID。如果你想要知道关于它的更多信息,可以留言回复,我将创建另一篇博客单独讲这一话题。
回到主题——接下来是类:
class lcl_excel_carrier definition. public section. interfaces: linf_sflight_carrier. aliases: tt_sflight for linf_sflight_carrier~tt_sflight, st_sflight for linf_sflight_carrier~st_sflight, ht_sflight for linf_sflight_carrier~ht_sflight, get_hashed_records for linf_sflight_carrier~get_hashed_records, get_sorted_records for linf_sflight_carrier~get_sorted_records, get_standard_records for linf_sflight_carrier~get_standard_records. protected section. private section. data: standard_sflight type tt_sflight, sorted_sflight type st_sflight, hashed_sflight type ht_sflight. endclass. class lcl_excel_carrier implementation. method get_hashed_records. r_sflight = hashed_sflight. endmethod. method get_sorted_records. r_sflight = sorted_sflight. endmethod. method get_standard_records. r_sflight = standard_sflight. endmethod. endclass. class lcl_rfc_carrier definition. public section. interfaces: linf_sflight_carrier. aliases: tt_sflight for linf_sflight_carrier~tt_sflight, st_sflight for linf_sflight_carrier~st_sflight, ht_sflight for linf_sflight_carrier~ht_sflight, get_hashed_records for linf_sflight_carrier~get_hashed_records, get_sorted_records for linf_sflight_carrier~get_sorted_records, get_standard_records for linf_sflight_carrier~get_standard_records. protected section. private section. data: standard_sflight type tt_sflight, sorted_sflight type st_sflight, hashed_sflight type ht_sflight. endclass. class lcl_rfc_carrier implementation. method get_hashed_records. r_sflight = hashed_sflight. endmethod. method get_sorted_records. r_sflight = sorted_sflight. endmethod. method get_standard_records. r_sflight = standard_sflight. endmethod. endclass. class lcl_table_carrier definition. public section. interfaces: linf_sflight_carrier. aliases: tt_sflight for linf_sflight_carrier~tt_sflight, st_sflight for linf_sflight_carrier~st_sflight, ht_sflight for linf_sflight_carrier~ht_sflight, get_hashed_records for linf_sflight_carrier~get_hashed_records, get_sorted_records for linf_sflight_carrier~get_sorted_records, get_standard_records for linf_sflight_carrier~get_standard_records. protected section. private section. data: standard_sflight type tt_sflight, sorted_sflight type st_sflight, hashed_sflight type ht_sflight. endclass. class lcl_table_carrier implementation. method get_hashed_records. r_sflight = hashed_sflight. endmethod. method get_sorted_records. r_sflight = sorted_sflight. endmethod. method get_standard_records. r_sflight = standard_sflight. endmethod. endclass.
上面的类有着相同的功能,但是根据具体的运输目的,完整的实现类会有某些特定的方法(比如从raw数据中过滤、检索数据等等)。
所有运输类需要实现linf_sflight_carrier——由此我们不再不得不在每个类中定义所有的方法了。不过,我使用aliases关键字增加了别名,以提高代码的可读性。
我们下一个将要创建的类是数据库保存者,名字是lcl_database_saver:
class lcl_database_saver definition. public section. interfaces: linf_sflight_saver. aliases: lock_types for linf_sflight_saver~lock_types, scope_range for linf_sflight_saver~scope_range, save_data for linf_sflight_saver~save_data, _sflight for linf_sflight_saver~_sflight. protected section. private section. methods: "! Creates table lock key for database lock "! @parameter i_sflight_ref | Reference to SFLIGHT table line "! @parameter r_varkey | Varkey returned create_varkey importing i_sflight_ref type ref to sflight returning value(r_varkey) type vim_enqkey, "! Locks table using passed varkey "! @parameter i_varkey | Table lock key "! @parameter i_tabname | Table name "! @parameter r_subrc | Information on lock creation. 0 = okay lock_table_line importing i_varkey type vim_enqkey i_tabname type tablename default _sflight returning value(r_is_locked) type abap_bool, "! Unlocks locked table line "! @parameter i_varkey | Table lock key "! @parameter i_tabname | Table name unlock_table_line importing i_varkey type vim_enqkey i_tabname type tablename default _sflight. endclass. class lcl_database_saver implementation. method save_data. loop at i_carrier->get_standard_records( ) reference into data(standard_line). data(varkey) = create_varkey( standard_line ). if lock_table_line( i_varkey = varkey ). modify sflight from standard_line->*. unlock_table_line( exporting i_varkey = varkey ). endif. endloop. endmethod. method lock_table_line. call function 'ENQUEUE_E_TABLEE' exporting mode_rstable = lock_types-exclusive " Lock mode for table RSTABLE tabname = i_tabname " 01th enqueue argument varkey = i_varkey " 02th enqueue argument _scope = scope_range-_2 exceptions foreign_lock = 1 system_failure = 2 others = 3. r_is_locked = xsdbool( sy-subrc = 0 ). endmethod. method unlock_table_line. call function 'DEQUEUE_E_TABLEE' exporting mode_rstable = lock_types-exclusive " Lock mode for table RSTABLE tabname = i_tabname " 01th enqueue argument varkey = i_varkey " 02th enqueue argument _scope = scope_range-_2. endmethod. method create_varkey. r_varkey = |{ i_sflight_ref->mandt }{ i_sflight_ref->carrid }{ i_sflight_ref->connid }{ i_sflight_ref->fldate }|. endmethod. endclass.
最后,运行例子:
initialization. data(excel_carrier) = new lcl_excel_carrier( ). data(rfc_carrier) = new lcl_rfc_carrier( ). data(database_saver) = new lcl_database_saver( ). try. database_saver->save_data( i_carrier = excel_carrier ). catch cx_sy_assign_cast_illegal_cast. catch cx_sy_assign_cast_unknown_type. catch cx_sy_assign_cast_error. endtry. try. database_saver->save_data( i_carrier = rfc_carrier ). catch cx_sy_assign_cast_illegal_cast. catch cx_sy_assign_cast_unknown_type. catch cx_sy_assign_cast_error. endtry.
如你所见,通过把抽象部分移动到接口层面,我们可以确保任何实现了linf_sflight_carrier接口的类可以被传递给saver方法并且被正确处理。
另一个该实现的优点是可以快速简单地创建模拟对象来进行单元测试。可测试的代码即是更好的代码。
这就是本文的全部内容了,愿你喜欢🙂。
总结
- 接口描述了一个会被类承诺提供的功能集。这是一个公共方法集。
- 接口中不含代码,只有公共方法和数据声明
- 任何实现了接口的类必须实现接口的承诺。例如,为每个这些公共方法写代码。
- 一个类也许会实现多个接口,这是ABAP中的多继承。
- 而子类与父类间的关系是“是”的关系(例如,鸭子是鸟),接口则表达了一种“有”的关系(如“类XYZ有一个用于发送邮件的方法”)。
- 通用的OO原则是倾向于组合(composition)而非继承(inheritance)。例如,在可能的情况下,不是使用子类而是使用实现接口的方式达到目的。
- 一个真实世界的例子是,某个人平日里是会计,而业余时间是救火志愿者。消防队只关心他救火的能力,相应地雇主则只在意他的会计能力。
- 在编程的世界里,类变量可以被定义为接口,然后任何实现该接口的类都可以被传入(需要这个接口的位置)。调用程序只能访问接口的方法,并不知道该类还有进行其它种类行为的能力。