最近在工作中碰到了一个bug,这个bug非常充分地表明:SAP的开发语言ABAP是一种hybrid型语言, 即A combination of OOP & FP.
这个bug是在这样的一个业务场景下产生的:
在一个维护客户主数据的APP中,需要支持两种类别的数据的维护,即通用的数据(比如地址信息,对应的账套科目,默认付款协议,交货协议等)和国家特定的数据(比如分支机构码等泰国独有的信息)。从程序设计的角度来看,这个APP的设计既需要保证标准数据能够正常维护,又能确保客户自定义的数据也能正常维护(比如某个客户可以增添客户主数据的一些属性,视客户的具体需求而定)。
一个很直观的设计如下:
FORM: check_data.
if_customer_master~check_data();
if_customer_master_cs~check_data();
ENDFROM.
FORM: save_data.
if_customer_master~save_data();
if_customer_master_cs~save_data();
ENDFORM.
PERFROM check_data.
...
PERFOEM prepare_save_data.
PERFROM save_data.
这个APP的画面逻辑布局如下图所示,高亮显示的"Thailand Branch Detail"是一个按钮,点击后进入一个新的维护分支机构码的页面(ps:内部员工一直在吐槽SAP这个八九十年代的UI设计,SAP现在其实已经有全新的Fiori UI,很酷炫的哦 ^ - ^ ,但我没有截图,因为内在很重要,噗):
介绍了业务场景后,具体的需求是:当用户在维护主数据时,需要去检查有没有维护定制化数据(在这个例子里,是Branch),如果没有,则抛给用户一条警告消息。
这个需求很简单,因为这个check的逻辑加在if_customer_master_cs~check_data()
里即可。但是由于check_data接口的传入参数太少,导致无法直接通过传入参数去完成这段检验(需要公司机构码信息),所以之前的开发者选择把这段逻辑加在了if_customer_master_cs~save_data()
这里。这种方案在正常情况下能够成功运行没有问题,当客户没有维护定制化数据而直接点击保存时,其正常运行的截图如下:
请注意此时用户是允许点击按钮“Thailand Branch Detail”的,所以用户的第一反应是点击按钮去维护定制化数据,此时如果重新保存,就会出现定制化数据已成功保存在其对应的数据库表中,但是通用数据的保存却失败了。因为这个警告消息是在代码块save_data
时抛出的,通用数据的提交并写入数据库是发生在用户接受这个警告消息之后的,可是此时用户新开了一个UI(维护定制化数据),此时重新保存就会出现待保存通用数据被丢弃,因为重新保存会重新调用check_data
和save_data
。
这个问题的解决方案当然是把这段检验逻辑加在if_customer_master_cs~check_data()
这里,那么如何去得到接口没有传入的参数信息呢?答案是通过函数池(Function Group)。如下面的代码所示:
METHOD if_customer_master_cs~check_data().
CALL FUNCTION check_cs_data_filled();
ENDMETHOD.
函数check_cs_data_filled
是属于函数池FG_TH_BRANCH的,这个函数池除了包含一系列的函数,还有一个一致的空间来维护一些全局变量的值:
所以函数check_cs_data_filled
可以访问这个函数池其他函数被调用时赋值的全局变量,从而弥补接口参数设计带来的局限。
这个bug很快就被解决了,它也证明了ABAP是一种面向对象和面向函数的混合型(hybrid)语言,因为从纯OO的角度看,在一个类的方法里,应该调用的是其自身的方法或者是另外一个类的方法,但是这里却直接调用了函数编程语言的函数池里的一个函数,从而使得一些全局变量可以被没有关联关系的方法所共享,避免了需要额外引入类来处理这一难题,因为引入额外的类需要处理类与类之间的关系和接口设计。
Summary
这个例子涉及到了OO中典型的多态以及FP中的函数池,ABAP将这两种设计的特点融为一体,使得其扩展性得提升的同时也避免了复杂的接口设计及其可能的变化带来的问题。