思考软件开发的可持续性

2020-03-20  本文已影响0人  老猫_jerry

一个软件生命周期示例

第一年,新兴的业务规划,用户体验和软件的设计都非常漂亮,我们新增feature与bug fix的速度都非常快,业务发展的也非常好。
第二年,由于feature越来越多,业务变化比较快,代码复杂度增加导致了交付的速度与bug fix的速度都有所下降。因此,团队通过增加人手来加快交付速度,但这也增加了团队沟通成本和管理成本。新人由于对code base的理解程度及业务压力,短期内只会做一些临时方案满足需求,导致代码的复杂度进一步增加。
第四年,交付速度与质量严重影响了业务的发展,开发人员与业务人员向管理层提出重写该应用。重写启动后,团队分为两个组。一个组继续维持现有系统,另一个组负责重写原系统。
第五年,团队同时背负两套系统的工作压力。重写的新系统在实施过程中会发现很多在重写前没有评估到业务及技术细节,严重低估了重写的工作量。同时,新业务在不断发展,重写的系统同时要保证做增量开发。重写系统没有上线前是不产生价值的。导致负责重写的团队压力很大。为了应对上述种种情况,团队往往会做一些折中方案,能够给使系统尽快上线。最终结果是,更加加快了代码的腐化程度,重复到上一步。

不能说所有的软件生命周期都一定是这个样子,但上述的小例子真实的发生在我们这个行业,而且在不同的公司,不同的业务,不同的时间在不断地重演。

软件不变的真理

在讨论如何打破僵局前,我们先讨论下是如何造成上述局面的。交付与bug fix的速度与用户体验的下降,很大程度上的原因是因为软件的内部质量,代码质量的下降导致的(烂代码)。
没有开发人员主观意愿上想写烂代码的。变化是造成我们代码腐化的主要原因之一。

变化的维度

  1. 业务变化: 业务是具有不确定性的,也正是由于业务的变化,才使我们的业务在不断的向前发展
  2. 组织的变化:人员更替,组织变革,都一定程度上影响代码的变化。老人员带走了业务知识和技能,新人员带来了新的思想和方法。
  3. 需求在沟通过程中的误解:业务人员与开发人员的沟通,管理层与开发人员的沟通,开发人员与开发人员的沟通,很多时候我们理解的并不一定是对方理解的
  4. 对业务理解,技术理解的深度的变化:我们对一个业务,对某项技术,随时间的变化,理解也是不同的。

软件的本质是变化,即软件唯一不变的真理是变化。

应对变化的方式

应对的变化的方式一般有以下几种:

    instance of someType
    或
    if(type) {

    }else{

    }

重构

相信大家对重构都非常了解和熟悉,来自于马丁.福勒的经典书籍。主要包含两大部分内容。

但这里想和大家讨论的是以下方面的思考

path                                                                                                                           commits    active days

pom.xml....................................................................................................................... 185         88
src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java........................................ 121         96
src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java............................... 120         97
src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java............................................ 119         99
src/main/resources/changelog.txt.............................................................................................. 104         66
src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java................................. 96          82
src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java..................................... 94          79
src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java............................................ 65          53
src/main/java/org/springframework/data/redis/core/RedisTemplate.java.......................................................... 62          59
src/test/java/org/springframework/data/redis/connection/jedis/JedisConnectionIntegrationTests.java............................ 55          51


注意,按如上方法统计在在某些情况下会有遗漏,如一个很糟糕的类从A工程迁移到了B工程,这个类在B工程中的提交次数就会很少,我们就遗漏掉了这个需要重点关注的对象。

为了保险起见,可以通过如下命令做二次筛选。统计最近30天提交次数超过5次的文件:git effort --above 5 -- --since="30 days ago"
PS:该方法通过本人实际项目经验,非常有效。曾经只将一个项目的前10几个对象重构完成后,整个团队的交付速度提升了数倍。

康威定律:一个组织的产出物,与这个组织的协作方式一致。

敏捷组织中,通过康威定律总结出全栈小组,避免沟通壁垒,减少沟通中的误解带来的软件实现的复杂度。(关于康威定律的理解,强烈推荐肥侠同学的<微服务架构的理论基础 - 康威定律>)。该定律同样适用于非敏捷团队。如果团队内部任务分配是以项目为单位进行划分,那么就会带来如下几种结果。

  1. 代码实现耦合项目名称。
    项目名称通常不能表明我们在做什么。如沙漠行动,雷霆计划等。如果以这种项目为任务的划分单元。代码中难免会有这样的体现
         if(沙漠行动){
             doSomething();
         }
         if(雷霆计划){
             doSomething();
         }
         if(***二期){
             doSomething();
         }

如果负责该项目的同学还在团队,那么后面的同学势必会反复的询问这样项目的含义和背景。直到所有团队的人都问一遍甚至几遍为止。如果该同学已经不再团队了,那么这就变成了谁都不敢碰的迷之地带。

  1. 各种对象的mapping,各种概念不一致的命名。
    A项目用了一个概念,B项目可能同时也用,因为两个同学之间没有交叉,各自独立完成自己的任务,导致大家分别对同一概念进行建模。而后续集成时,因为DeadLine临近,已经来不及做大的改动,索性mapping一下,后续有时间再改吧。这里的后续基本等于永远不。

  2. 项目成员单兵作战,无法得到团队的全面支持。
    每个同学自己负责一个项目,当项目出现技术难点或项目滞后时,其他同学很难给予及时且有用的帮助。因为其他同学也有自己的项目压力,也没有相关的项目背景知识,无法提供有效的帮助。

  3. 项目上线后无可靠的back up。如果该同学休假或工作变动,该项目是无法短时间内被其他同学接手的。

解依赖与可阅读

依赖过重与阅读性差,都是造成我们代码继续腐化,难以维护的重要原因。我们要尽量加避免。

依赖:可按照如下方式对依赖进行分类。

可阅读性

有意义的命名

代码到处都在命名,据说大师眼中编程最难的部分是起一个好名字。好名字需要有一定的上下文,但是统一的标准是Don't make me think。不要让读者去想。名字要表达做什么,而非怎么做。举几个例子

过度抽象:通常难以阅读和理解

如果没有重构能力,为了应对变化,很容易产生过度抽象,当什么都能做的时候,其实什么都没有做好。按层次划分,各层次经常出现的可能存在过度抽象的命名。

     select from timer
      if(timer_type1){
        doSomeSpecial for this type
      }
      if(timer_type2){
        doSomeSpecial for this type
      }

为什么如上例子均属于过度抽象呢?因为上述命名均跨领域,可以适用于所有软件。丢失了大量业务模型本身自解释的概念。
*** 强调上述阐释的内容并非所有这么做都一定有问题,只是这样实现的代码出现的问题的几率比较大。当涉及到类似情况时,请多用心思考是否存在上述问题。 ***

规范,约束和团队协作

好吧,上面说了好多正确的废话,接下来我们该谈谈实际可量化,可执行的一些方法和工作方式。

约束规范

无规矩不成方圆。绝对的自由带来的是混乱。只有在一定的规则约束下,方能有序的发展。
约束和规范,每个公司都会制定,Google,阿里,甚至一些小的公司都会制定一些非常详细且非常合理的代码规范。有一个问题,各个规范都是开源的,大家都可以学习,为什么取得的成效确不容乐观呢?对这个问题我的思考是,规范太多了。少说几十条,多则几百条。作为开发很难事无巨细的都记住。即使有各种插件提示warning,依然无法遏制住不规范的现象(因为程序员喜欢忽略warning,只关注error)。人类的大脑每次只能记住七种概念,事无巨细,无人遵守的规范,不如少儿精大家都遵守的规范。所以有必要对代码规范进行简化。如下是我总结的几个规范,供大家参考,分为基础版本与进阶版本(所涉及的数值仅供参考,具体数值由团队决定,并持续跌进)

基础版本

无效代码包括不执行的代码(死代码)、注释掉的代码。无效代码唯一的作用就是增加维护工作量。我们需要去阅读它,维护它,如果代码比较混乱,我们完全无法理解的代码,新增功能的时候,为了保险起见,可能还要使无效代码新增功能。这些都是巨大的浪费。针对于这样的代码,我们要毫不留情的删除。代码的回退是版本控制的职责,不是保留这些无效代码的理由。而且删除无效的代码成本非常低,可以说是投入产出比非常高的一个行为。

进阶版本:(在基础版本上)

Code Review

Code Review的频率和覆盖面与代码质量,交付速度成正比。Code Review越前置,需要重构的成本越低。当然Code Review需要建立一种Open的文化,团队共享代码,把别人对代码的问题反馈当做是学习和探讨的机会。可以参考Google如何做Code Review
同时强烈建议团队一起学习clean code,非常简单,但是非常有用。
附上一篇:王垠版本的Clean code 编程的智慧

沟通

构建高质量的软件很重要的一个前提是向正确的人问正确的问题。我们要多问问题,问正确的问题。问清楚了,总比事后去修改软件成本要低。最直接有效的沟通是面对面的沟通。少使用电话或者社交软件。

分享交流

多做技术交流,形成规范和共识。将最佳实践分享到团队,受益最多的是我们自己。

可测性

测试成本与交付速度成反比,尽可能降低测试成本。有UT更好,但不强制,一味强调测试覆盖率,即不可能,也带来不了最大收益

回顾迭进

定期回顾,频次团队来定,回顾这一阶段,我们那些地方做的好,继续推广发扬

迭代

短期迭代交付,持续交付,PD,测试与研发团队紧密合作(全栈团队更佳)

总结

软件开发是一项复杂的团队协作的工作。细节决定成败,没有银弹,只有我们不懈努力,才能得到更好的结果。

上一篇 下一篇

猜你喜欢

热点阅读