软件工程师与复杂度

1.软件开发理论与现实的差异

在中国,软件开发总是与加班联系在一起的。我最近看了几本书,《人件》、《人月神话》、《代码整洁之道》,我在这些书中都可以看到一个共同的观点:常态化的加班对整个软件开发项目是有损害的。这些书都是计算机开发的经典之作,也就是说理论上软件项目开发不应该把加班作为常态,但是软件开发需要经常加班却成为大部分中国软件开发者的常识。

为什么理论和实际会出现如此大的差异?

首先理论和中国实际哪个是对的?哪个才是软件开发真正的样子?

问题的核心在于软件开发本质上是体力劳动还是脑力劳动,或者说软件开发最核心的生产力是来自于创造性的脑力劳动还是重复性的体力活动?问题的核心在于软件开发本质上是体力劳动还是脑力劳动,或者说软件开发最核心的生产力是来自于创造性的脑力劳动还是重复性的体力活动?注意这里的脑力劳动最主要指创造性的思维活动,类似解决数学题的构造思路一样的思维活动,这种思维活动最后应当产生创造性的思维成果。

如果软件开发最重要的是脑力活动,那么就不应该加班,因为常态化的加班会损害大脑的创造性,而且创造性的成果是以质取胜而不是以量取胜的。牛顿三大定律不超过一百字,但却简单明了的揭示了支配整个宇宙宏观物理运动的物理规则。
如果软件开发是重复性的体力活动,那么为了赶项目,为了抢占市场,以支付员工更高薪资为代价,让员工加班加点赶项目也是商业公司的合理行为。

在讨论软件开发是体力劳动还是脑力活动前,就绕不开软件这个事物。

2.软件的组成和软件开发工程师的工作

软件开发工程师的视角来看,软件可以分为两部分。

一个是软件应该给其目标用户提供哪些功能,以文档的形式表现就是软件的需求文档,也可以称之为业务需求。比如以电子商城为例,商城可以支持用户在其上面浏览商品、添加商品到购物车、下单购买商品、评价商品等等这些功能。

另一个是软件的技术实现,为了支持软件提供的功能,就需要一系列的技术组件来进行支持。比如为了实现电子商城,就需要选择一门合适的语言来编写代码,同时为了简化开发,需要使用该语言一些特定的框架和库。需要使用合适的操作系统来作为软件部署的环境、需要使用数据库来存储数据。有时为了性能方面的问题,就需要使用算法、缓存等一系列事物。

对于技术实现,大学的课程主要教授的是这些东西,特定编程语言的语法、操作系统、计算机网络、数据结构与算法,如果一个大学生有好好学习这些课程,对于这些理论知识的底层原理是很清楚的。但是到了工作中,会有一个情况,其实这些理论知识很少用到。绝大多数时候软件开发都是用别人造好的‘轮子‘,编译器、操作系统、数据库这些东西自己是不需要自己深入到底层进行开发的,就算企业因为某些考虑要发展自己的操作系统、数据库,也会招聘这方面专门的人员进行开发。

我们学习到这些计算机技术实现理论的知识,更多的时候是让我们知道如何恰当、准确的使用这些工具。比如,知道了计算机进程、线程的知识后,编码的时候就知道什么场景需要创建一个新进程来满足功能需求,什么场景创建线程就可以功能需求。

所以,一个优秀的软件开发工程师不一定需要对使用到的技术的底层原理有很具体的理解,很多场景下只需要知道如何恰当和正确使用已有技术工具来作为实现软件功能的基础。比如就数据存储方面来说,有时可能用简单的文件存储即可满足功能的需求,有时可能需要用MySQL、ORACLE等数据库来实现数据存储的功能,有时又可能需要分布式文件系统来实现数据存储。

底层技术组件选择一般由架构师或者项目总工程师确定,大多软件开发工程师一般的工作是在已经确定的技术组件上,用既定的语言编码实现软件的功能,也就是所谓的编写业务代码。

那软件开发是脑力劳动还是体力劳动这个问题就可以转换为:编写业务代码是脑力劳动还是体力劳动?

在语言、技术架构等等都已确定好的情况下,实现一个软件功能看起来应该是一个体力活动。开发者只需要在已经确定的架构中,根据业务需求编码,把业务代码添加到项目中即可。

对于同一个功能,一千个开发者写出来的代码可能都不一样。这一千种代码质量有好有坏,好的代码让人很容易理解其背后的业务逻辑,在其上面做改动和拓展也很容易。而差的代码则与之相反,看起来晦涩难懂,在其上面的维护拓展更是苦难重重。
质量差别的根本原因在哪里?问了论述这些问题,下面需要引入一个概念:软件的复杂度。

3.软件的复杂度

这里引用下维基百科对软件复杂度的定义:


Complex, on the other hand, describes the interactions between a number of entities. As the number of entities increases, the number of interactions between them would increase exponentially, and it would get to a point where it would be impossible to know and understand all of them. Similarly, higher levels of complexity in software increase the risk of unintentionally interfering with interactions and so increases the chance of introducing defects when making changes. In more extreme cases, it can make modifying the software virtually impossible.


翻译如下:


复杂度,描述了多个实体间的交互,随着实体数量的增加,实体间的交互爆炸性的增长,达到一定的节点之后。这些实体及其之间的交互变成无法为人类理解。软件的复杂程度越高,就越有可能无意中干扰到互动。所以当对软件进行修改的时候,会增加引入缺陷的风险。在更为极端的情况下,软件无法进行任何修改。


简单点来说,随着软件的复杂度越来越高,其代码越来越难以理解、越来越难以维护、越来越难以扩展。

维基百科上把复杂度大致分为两种类型

本质复杂度(Essential complexity)

本质复杂度来源于要解决的问题的特征,这部分复杂度不能被减少。一个电子商城,拥有添加物品到购物车、下单、评价这些功能,那么系统中就一定需要有这些功能对应的代码,也就必然拥有了其最低的复杂度。

偶然复杂度(Accidental complexity)

这个复杂度可以分为两部分。
第一部分,软件除了最核心的业务逻辑外,还需要大量的技术性支撑性工具及代码,比如实现语言、容器、中间件、框架。业务逻辑无法脱离这些技术支撑性的事物独自运行。而在项目开发的过程中.与这些技术实现相关联的复杂度可以称为技术复杂度。
现在很多技术组件都已经有了很成熟的实现,开发者可以很容易的找到需要的技术组件来来支撑软件的实现。比如数据库可以用MySQL、ORACLE,可以各种框架来减少编码与配置。
在强大的硬件性能支撑下,很多项目只要使用最简单的技术架构就可以满足其业务需求。
可以这么说,在大多数项目中,技术复杂度对软件质量的决定性影响很小。

第二种偶然复杂度才是很多时候决定软件质量的根本因素,也是这篇文章要讨论的重点。
软件的业务逻辑需要通过编码进行实现,但是这些业务逻辑如果没有组织好。就会出现本该聚合在一起的逻辑分散开发、同一个业务逻辑被多次实现、应该出现在A处的逻辑出现在B处等等情况。这种业务逻辑的错误分配会产生偶然的复杂度,而且随着软件的维护、拓展,这种复杂度会快速增长。
这种复杂度是软件开发过程中损害软件质量的主要因素。也是此文关注的重点。
那怎能才能控制、降低这种复杂度?
基于业务需求构筑合适的抽象,然后进行相应地建模设计,合理地确定业务逻辑的归属分配。有一种相关的设计方法论便是DDD。

4. 一个例子

下面用一个例子来说明复杂度对软件开发地影响。
以刚才举的电子商城为例子。我们先借鉴日常的网上购物体验。
首先,用户在电子商城中有一个账户,用户可以往这个账户充值。

用户要购买一样物品,当其填写好收货信息之后,点击提交。后天接收到请求之后会先检查用户当前账户的余额是否能够支付订单,如果可以支付则创建订单,如果余额不够则不创建订单。

在省略不必要的细节之后,代码如下。实际开发的时候这个方法需要返回创建结果,传入参数也需要包含商品的信息。但是此次论述的重点是如果用户当前账户余额如果小于订单花费,那么就不会创建订单。所以不必要的细节都被省略了。

/**
 * 创建订单
 * @param account 用户账户
 * @param cost 订单花费
 */
public void createOrder(Account account,double cost){
    //如果用户账户余额小于订单总花费,那么不创建订单
    if(account.getBalance() < cost){
        return;
    }
    //创建订单对象和相关逻辑,省略
}

account即用户账户对象,account.getBalance()即获取账户的余额。
注意判断用户余额是否能够支付订单花费

account.getBalance() < cost

这行代码的背后隐藏着一个逻辑,即判断账户是否能够支付给一定数额金钱的逻辑。
而这行代码用这个逻辑来判断账户是否能够支付订单。’ 判断账户是否能够支付一定数额金钱’的逻辑被已经暴露在创建订单的业务代码中。这个逻辑可能在别的业务流程中被用到。
再考虑一个新的业务:用户可以把账户中的余额提现到银行卡中,提现的金额不应大于账户余额。省略掉多余细节之后,提现方法实现如下;

    /**
 * 余额提现
 * @param account 用户账户
 * @param cash 提现金额
 */
public void withdrawal(Account account,double cash){
        //如果用户账户余额小于提现金额,那么不进行提现
        if(account.getBalance() < cash){
        return;
        }
        //提现相关逻辑,省略
        }

同样注意

account.getBalance() < cash

这行代码也使用了’判断账户是否能够支付一定数额金钱’的逻辑。

现在这个逻辑在创建订单和提现都使用到了这个逻辑,而且各自在代码中实现这个逻辑。

这样做会导致两个弊端。

  1. 这样一个if除非加注释,否则很难让后续的开发者知道这个判断逻辑代表什么,为什么需要加这个一个语句。

  2. 后续的业务变更、扩展的时候’判断账户是否能够支付一定数额金钱’的逻辑变更,需要维护这两份代码。比如后续商城可以允许用户根据信用等级的不同有一定的贷款,这个时候‘’判断账户是否能够支付一定数额金钱’的实现就应该变为

账户余额+可贷款数额  > 给定数额

提现和创建订单的部分都需要改代码。

从复杂度的角度来描述。
’判断账户是否能够支付一定数额金钱’的这个业务逻辑使必须要存在的,因为业务功能中需要使用到这个逻辑。所以代码必须有实现这个逻辑的代码。这是软件的本质复杂度。但是前面的例子中因为对这个业务逻辑的错误处置,在两个地方实现了这段业务逻辑。这种对业务逻辑的错误处置给软件引入了不必要的复杂度。

接下来需要对前面的两个方法进行调整,首先需要把’判断账户是否能够支付一定数额金钱’逻辑的实现代码聚合到一个地方,很自然的Account类可以有这个方法。

/**
 * 当前账户是否能支付给定数额的金钱
 * @param money
 * @return
 */
public boolean canAfford(double money){
    return balance >= money;
}

原先的两个方法改动

/**
 * 创建订单
 * @param account 用户账户
 * @param cost 订单花费
 */
public void createOrder(Account account,double cost){
    if(!account.canAfford(cost)){
        return;
    }
    //创建订单对象和相关逻辑,省略
}

/**
 * 余额提现
 * @param account 用户账户
 * @param cash 提现金额
 */
public void withdrawal(Account account,double cash){
    if(!account.canAfford(cash)){
        return;
    }
    //提现相关逻辑,省略
}

改动之后,前面说的两个问题被修复了。

  1. canAfford 方法名体现了判断账户是否能够支付一定数额金钱’业务逻辑,无需额外的注释即可说明这段代码的含义,代码本身即表达了设计。

  2. 后续的业务逻辑改动只需要调整canAfford一个方法即可。

3.总结

总结:
软件开发中软件的损害软件质量的因素是其复杂度,而很多时候复杂度增长的主要原因在于业务逻辑的不适当分配,而控制这种复杂度增长的方法就是建模设计。
构筑高质量的软件就需要有以下几点条件:

  1. 负责项目的开发团队成员需要有专业的软件开发素养,能够察觉到该针对当前业务需求,需要进行怎样的设计。设计建模是针对具体业务而言的,所以开发人员需要业务有一个清晰、明确的认知,同时开发团队需要有一种能有效传递这种抽象认知的方式。上述条件都要求软件开发人员对相关理论有充足的理解和实践经验,特别是DDD。
  2. 随着业务的发展和变化,之前的设计不再与业务适配,这个时候就需要变化设计。变化设计就需要对当前代码进行重构,项目重构的一个必要条件是有完备的自动化测试方案。
  3. 设计是一项创造性的脑力劳动,需要公司为开发团队创造合适的环境条件,同时也需要确保上班时间的稳定。让开发团队中各成员的大脑在工作的时候处于最适合进行创造性思维的状态。

最后,对本文开头的问题做出回答:软件开发中最重要、最具价值的部分需要经过创造性的脑力劳动才可以得到。如果忽视了这部分,那么软件开发很容易陷入复杂度的泥潭中,无止尽的加班,永远赶不上的进度,最后的产品质量还很糟糕。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值