在程序开发的过程中,相同的功能往往有不同的实现方式。对于可以实现同样功能的不同代码,复杂度是用于比较其质量优劣的重要指标。

在本文中,代码复杂度是指代码被理解/修改的难易程度。越容易被理解、修改的代码的复杂度越低;反之其复杂度越高。

复杂度低的代码比复杂度高的代码有更多好处,比如,

  • 从代码“查逻辑”变得简单
  • 可以节省修改的时间
  • 降低在未来引入bug的几率
  • 新人会更容易上手现有代码
  • 帮助整个系统更加“长寿”

ABAP开发是在SAP系统中进行的,而SAP是企业的核心信息系统,其中会包含复杂的业务逻辑,通常由ABAP实现,并需要长期的维护。在这样的工作中,ABAP代码的复杂度对系统维护成本甚至项目的成败有着重要的影响。

在下文中,我会介绍几种有助于最小化代码复杂度的通用思路,并尝试把它们和实际的ABAP开发工作结合起来,帮助理解。

作者水平有限,如果读者发现了任何问题,欢迎评论指出。

本文链接:https://www.cnblogs.com/hhelibeb/p/10871392.html

原创内容,转载请注明

1, 模块化设计

两年前,我第一次参与的SAP项目刚完成不久。那时我对自己的技术相当自信,在项目中,我不仅完成了多种类型的功能开发,而且也读完了整个项目的新开发代码。即使对于一些没做过的新的功能需求,我也往往能在不依赖乙方同事的情况下独立查找资料完成。自信满满的我决定换份新工作,并得到了一个面试机会。第一轮面试考察的是一些常用功能的实现和对业务流程的了解程度,如我预想的那样,自己顺利通过。在第二轮面试中,对方问到"对模块化的理解和实践",这是个出人意料的问题,我努力地思考了一番,却不知道该说什么,于是被客客气气请了出去…

经历了这次失败后的我,不断地思考着模块化设计。如果再次面对那个问题,也许我应该这样回答:

定义

模块是系统中独立的、可替代的单元。模块化设计,即是把系统分解为模块的集合。模块的形式多种多样,可以是form、method、function Module、class、或report等。在理想的世界中,每个模块都完全独立于其它模块:开发者在任何模块中工作的时候,都不需要知道有关其它模块的知识。在这种理想状态下,系统复杂度取决于系统中复杂度最高的模块。

当然,实践与理想不同,系统模块间总会多少有些依赖。当一个模块变化时,其它模块可能也需要随之而改变。模块化设计的目标就是最小化模块间的依赖。

为了管理依赖,我们可以把模块看成两部分:接口实现

接口包含了全部的在调用该模块时需要的信息。接口只描述模块做什么,但不会包含怎么做

完成接口所做出的承诺的代码被称为实现

接口中包含2种信息:正式的和非正式的。

正式的信息在代码中被显式指定,程序语言可以检查其中的部分正确性。比如,方法的签名就是正式的信息,它包含参数的名称和类型,返回值的类型,异常的信息。很多程序语言可以保证代码中对方法的调用提供了与方法定义相匹配的参数值。

接口里面也包含非正式的元素。非正式部分无法被程序语言理解或强制执行。接口的非正式部分包含一些高层行为,比如函数会根据某个参数的内容删除具有相应名字的文件。如果某个类的使用存在某种限制,比如方法的调用需要符合特定顺序,那这也属于接口的一部分。凡是开发者在使用模块时需要了解的信息,都可以算作模块接口的一部分。接口的非正式信息只能通过注释等方式描述,程序语言无法确保描述是完整而准确的。大部分接口的非正式信息都比正式信息要更多、更复杂。

正式的信息和非正式的信息都是复杂度的来源,清晰的接口定义有助于开发者了解在使用模块时需要知道的信息,从而避免一些问题。

示例

以function module为例,在function module编辑器中看到的function module名,和前6个标签,加上function module文档(如果有),都属于它的接口。而source code中的代码,则属于实现。如下图

如何减小ABAP业务代码的复杂度-LMLPHP

当然,如果该function包含任何隐性的使用信息,它也算做接口的一部分。比如,如果使用SAVE_TEXT保存长文本,通常要有一个COMMIT操作来提交修改。“需要使用COMMIT提交修改”,同样是SAVE_TEXT的接口的一部分。

进行模块化设计,是减小代码复杂度的第一步。因为,开发者在进行模块内部开发时,只需要关注 当前模块的接口+当前模块的实现+其它相关模块的接口。他只需要关注整个软件系统的一小部分,接触的东西变少,会使理解工作内容的速度大大增加,也会使犯错的机会变小。对于试图理解当前系统的部分功能而不做修改的人,这种设计同样会减轻人们的负担,因为人们通常只需要通过模块们的接口来了解程序的功能。

2, 减少异常的影响

经验较浅的程序员容易犯的一个错误是,只考虑程序中的正常情况,即所谓的happy path,而没有(足够多地)考虑异常情形。不周全的考虑可以让程序员快速完成功能,但是接下来则会导致测试中的频繁翻车,程序员不得不再对程序进行种种的修补,导致代码整体的复杂度迅速升高。此外,即便是在一开始已经考虑到了各种异常情形,为了处理它们,也会给程序增加一定的复杂度。本节内容的主题是,如何合适地尽量减小由异常情形引起的复杂度。下面介绍具体的三种办法,

从概念上消除异常

第一种办法是从概念上消除异常。异常是相对正常而言的,功能的定义可以影响到异常的定义。ABAP SQL有插入语句,代码如下,

INSERT ztable FROM TABLE @lt_something.

但是,开发者通常不得不考虑主键重复引起的异常处理。所以在这一语句后面,还要检查sy-subrc返回值,根据判断做进一步处理...代码因此变得复杂。

如何避免此处的异常处理?假设我们对功能的设定做出一些改变,从“把内表的数据插入数据库”改为“保证数据库中存在内表的数据”,在这个新功能的内部判断插入语句的执行情况,如果因为主键重复导致插入失败,则改为按主键更新数据库表,此时不再需要在调用时进行相关异常处理。

说到这里读者已经知道,这个“新功能”就是ABAP中的关键字MODIFY,

MODIFY ztable FROM TABLE @lt_something.

使用MODIFY而不是INSERT的话,一种常见异常的定义便消失了,代码的总量和复杂度因此会减少。当然,前提是MODIFY的功能和需求相匹配。有些资深开发者因为害怕新人不了解MODIFY的原理而禁止他们使用这个关键字,是因噎废食的做法。

隐藏异常

把异常隐藏在较低层面是第二种做法,这种做法可以使高层的代码在不需要了解异常存在的情况下进行工作,从而减少高层的复杂度。SAP系统中的一个例子是tRFC

对于tRFC而言,远端系统不需要在RFC客户端程序运行tRFC的时候可用。tRFC组件将被调用的RFC函数和相关数据存储在SAP系统的数据库里,包含一个唯一的事务标识符(transaction identifier,TID)。如果调用发送了,接收系统却是宕机状态,调用会保留在本地队列中一段时间。调用对话程序可以在不等待远程调用成功/失败的情况下继续运行。如果接收系统在一段时间后仍然不可用,调用将被计划为后台作业运行。

在tRFC的例子中,高层调用者不需要了解对方系统的状态,也不需要进行传输失败的处理,这一切都由低层完成了。高层程序的复杂度也会因此得到控制。

聚合异常

与其分散地处理程序中不同部分产生的异常,不如把它们集中交给高层的程序,进行统一的处理。SAP系统中的一个例子是BAPI。绝大多数BAPI使用一个名为RETURN参数返回所有的错误,这样一来就可以由高层调用者对可能产生的错误进行统一的处理,而不是在产生错误的地方进行分散、个别的处理。

3, 纯函数

如何减小ABAP业务代码的复杂度-LMLPHP

“Hi 氢氦,我在测试中遇到了这个错误消息,麻烦你看一下原因。”

“什么?这和我们的修改毫无关系,这个报错属于B功能,而你知道我们改的只是程序的A功能,!”

“是的,但是我得保证这次修改没有影响到B功能,所以请你调试调查原因。”

测试的一个难题是测试者不知道看似单纯的修改会带来什么样的复杂问题,于是只好求助于人工检查,开发者往往就是那个不幸的工人,使用纯函数可以帮助避免这类情况的发生。

定义

最近流行的函数式编程十分强调纯函数的概念。纯函数是指符合以下条件的函数,

  • 对于相同的输入,函数总有相同的输出。

这要求函数内部不能存在“副作用”。

它的输出结果的确定不应该依赖输入参数外的任何内容,例如,不可以因为本地测试环境中没有相应的数据库就产生“连接数据库异常”导致无法返回结果。

它也不应该改变除了返回结果以外的任何内容,例如,不可以改变全局可变状态。

满足以上条件的函数,可以被称为纯函数。

从模块化的角度来看,全局状态和对外部系统的连接都属于接口的一部分。纯函数不会与这些东西产生交互,因此它的接口会更简单,复杂度更低

虽然ABAP不是函数式语言,但它依然可以有纯函数,并且开发者可以通过写纯函数而受益。

在上面的例子中,如果开发者可以证明A、B功能分属2个模块,而且它们都属于纯函数,那么只要证明A的变更不会改变B的输入,即可证明修改没有导致对方给出的错误。

4, 需求文档与实现

系统中的模块可以分为接口和实现,换一个角度思考的话,代码也可以看作需求文档的实现。需求语言的准确性,对代码产物的质量有着直接的影响。在SAP开发中,业务逻辑的实现是首要目标,业务复杂度也往往是代码复杂度的最主要来源。

信息丢失

程序是需求的实现,因此代码应当尽量包含需求文档中的信息。在需求文档的质量可靠的前提下,这样做可以有效提高程序的可读性,从而降低其复杂度。信息丢失的一个极端例子是代码混淆,显然混淆后的代码复杂度将大大升高。

当然,程序语言和系统内部的信息和需求文档中的自然语言是有差别的,这也是程序员存在的意义。在实际的开发中,可以尝试通过增加一个抽象层的方式来保留需求文档中的信息。比如,在高层对需求语言进行建模,在低层实现实际的执行过程。

注意:如果需求文档很长,代码实现很少的话,意味着工作可能存在某些问题,可能是需求内容冗余、功能设计不合理、实现不完整、信息丢失等,此时要重新思考相关工作内容,以确保未出现这些问题。

词汇表

保留信息的前提条件是明确信息,需求文档中应当有词汇表,帮助开发人员迅速捕捉和理解最重要的业务语言,以便把它们落实在程序中。

对于在中文环境下的工作而言,这点尤其重要。ABAP并不适合使用中文命名,而普通的程序员没有能力把中文的业务语言转换成英文的,需要由业务顾问来完成这项意义重大的工作。

5,工作转移

最后,还有一种显而易见的办法是将某些业务逻辑转移到ABAP之外实现,比如下推到数据库层面、使用配置工具实现(BRF+)、交给中间件等。

优点:从ABAP角度来看,这种办法最有效地避免了复杂度的增加。

缺点:从全局来看,复杂度并没有真正消失,只是随着工作量转移到了另一个地方。

要从系统的整体复杂度的角度来考虑实现逻辑的位置,比如,人们常常使用配置表配合ABAP代码来实现自定义逻辑,BRF+中的decision table可以实现相似的功能,如果需求希望得到用户在实际使用程序时对不同逻辑的命中率的话,那么decision table可能会更合适一些,因为BRF+中包含跟踪模式,可以被直接利用,这样就可以通过写简单的代码来得到结果,避免引入更多的复杂度。

后记

长期关注本博客的读者可能会注意到,这个博客已经有段时间没有更新ABAP相关内容,这是因为从今年开始,我的工作重心已经逐渐转移到Spark和Dynamics等其它方面的开发,花在SAP上面的时间变得很少。恰好最近参加了一个新的SAP项目,它唤醒了我对ABAP的一些记忆,于是趁热打铁,写了这篇文章。这篇文章算是在ABAP角度上将最近学习到的东西进行的一个总结复习。希望它能对读者有所帮助,也真诚地希望能收到反馈,共同讨论相关话题。

参考链接:《A Philosophy of Software Design》

        《函数响应式领域建模》

        软件设计之Deep Module(深模块)

     我的Spark SQL单元测试实践

05-13 00:58