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

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

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

  • 从代码“查逻辑”变得简单
  • 可以节省修改的时间
  • 降低在未来引入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中的代码,则属于实现。如下图

当然,如果该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, 纯函数

“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单元测试实践

如何减小ABAP业务代码的复杂度的更多相关文章

  1. 唱吧DevOps的落地,微服务CI/CD的范本技术解读----最大的难点并不是实际业务代码的编写,而是服务的监控和调试以及容器的编排

    1.业务架构:从单体式到微服务 K歌亭是唱吧的一条新业务线,旨在提供线下便捷的快餐式K歌方式,用户可以在一个电话亭大小的空间里完成K歌体验.K歌亭在客户端有VOD.微信和Web共三个交互入口,业务复杂 ...

  2. 朱晔的互联网架构实践心得S2E1:业务代码究竟难不难写?

    注意,这是我的架构实践心得的第二季的系列文章,第一季有10篇你也可以回顾. 见https://www.cnblogs.com/lovecindywang/category/1296779.html 最 ...

  3. CTR学习笔记&代码实现2-深度ctr模型 MLP->Wide&Deep

    背景 这一篇我们从基础的深度ctr模型谈起.我很喜欢Wide&Deep的框架感觉之后很多改进都可以纳入这个框架中.Wide负责样本中出现的频繁项挖掘,Deep负责样本中未出现的特征泛化.而后续 ...

  4. Magicodes.WeiChat——ASP.NET Scaffolding生成增删改查、分页、搜索、删除确认、批量操作、批量删除等业务代码

    关于T4代码生成这块,我之前写过几篇帖子,如:<Magicodes.NET框架之路——让代码再飞一会(ASP.NET Scaffolding)>(http://www.cnblogs.co ...

  5. 提升c++builder 代码输入流畅度的配置

    提高c++builder 代码输入流畅度 1.输入指针的函数名后,识别函数参数移动光标到括弧内,此功能太慢,有明显延迟,建议关闭.关闭以后,输入函数名不会自动添加(),需要自己手动输入括弧了,不过速度 ...

  6. JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码

    本文是<JVM 性能调优实战之:一次系统性能瓶颈的寻找过程> 的后续篇,该篇介绍了如何使用 JDK 自身提供的工具进行 JVM 调优将 TPS 由 2.5 提升到 20 (提升了 7 倍) ...

  7. golang写业务代码,用全局函数还是成员函数

    在golang中,函数划分为全局函数和成员函数,在使用的时候,有种情况,会产生一些疑惑的,就是在写业务代码的时候,使用全局函数好像会比较方便,一般业务代码,都不会复用,都是针对特定的业务进行编程,要复 ...

  8. 朱晔的互联网架构实践心得S2E2:写业务代码最容易掉的10种坑

    我承认,本文的标题有一点标题党,特别是写业务代码,大家因为没有足够重视一些细节最容易调的坑(侧重Java,当然,本文说的这些点很多是不限制于语言的). 1.客户端的使用 我们在使用Redis.Elas ...

  9. abap调用代码块

    1:abap 调用代码块. *&---------------------------------------------------------------------* *& Re ...

随机推荐

  1. JavaScript-----2初识

    1.介绍 JavaScript是一种运行在客户端(自己的电脑上)的脚本语言不是在服务器上 脚本语言:不需要编译,运行过程由JS解释器(js引擎)逐行进行解释并执行 JavaScript不仅可以做前端编 ...

  2. 【RN - 基础】之TextInput使用简介

    TextInput组件允许用户在应用中通过键盘输入文本信息,其使用方法和Text.Image一样简单,实例代码如下: <TextInput placeholder={'请输入用户名'} styl ...

  3. Chapter 05—Advanced data management(Part 1)

    一. R的数学函数,统计函数及字符处理函数 例01:一道实际应用题 一组学生其数学,科学和英语的成绩如下表: 任务:根据成绩,决定对每个学生的单独指导: 前20%的学生的成绩为A,次之为B,以此类推: ...

  4. 【Python】之format奇技淫巧的输出控制

    前置 环境:Python3.6.5 探讨点:输出print,字符串format控制, % 控制 print基础控制 简单示范: a = 1 b = '@Hello yanshanbei!' print ...

  5. 深入理解 BigDecimal

    .katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...

  6. Reachability的使用

    刚到一家新公司 做新项目 关于网络状态的监听和同事产生了不一样的看法 原来我的网络监听都是自己写的 后来发现自己不是一般的傻 有一个叫做Reachability的东西 很简单 很实用 很暴力 下面就是 ...

  7. Web 前端学习大纲

    什么是前端? 前端即网站前台部分,也叫前端开发,运行在PC端,移动端等浏览器上展现给用户浏览的网页.随着互联网的发展,HTML5,CSS3,前端框架的应用,跨平台响应式网页设计能够适应各种屏幕分辨率, ...

  8. C#实现在foreach遍历中删除集合中的元素(方法总结)

    目录 方法一:采用for循环,并且从尾到头遍历 方法二:使用递归 方法三:通过泛型类实现IEnumerator 在foreach中删除元素时,每一次删除都会导致集合的大小和元素索引值发生变化,从而导致 ...

  9. Happy Birthday! 今天我 1 周岁生日啦!

    2018.09.28,我第 1 天分享文章. 2019.09.28,我连续分享的第 365 天. 今天我 1 周岁啦! 生日意味着一个新的开端, 意味着重新把握生活的机会. 新的一岁,从新头像开始 愿 ...

  10. 【JS】325- 深度理解ES6中的解构赋值

    点击上方"前端自习课"关注,学习起来~ 对象和数组时 Javascript 中最常用的两种数据结构,由于 JSON 数据格式的普及,二者已经成为 Javascript 语言中特别重 ...