跳到主要内容

【技术交流】来自Owen Obrien团队的可信开发

· 阅读需 9 分钟
Software Engineer

上周五上午抽了个空,去参加了来自爱尔兰的Owen Obrien带来的基于云化架构下的可信软件开发实践的技术交流活动,收获不少,特此记录一下。

Owen的团队的主力都是来自微软、谷歌、IBM等拥有超过20年软件开发经验的老鸟,自然也积累了丰富的经验,能够站在他们的肩膀上向前走,对我和我们的团队来说都会是极大的助力。

技术交流的主题是可信软件开发实践,那么自然少不了对可信软件的定义。我司对于可信软件的定义包含了6个特征,分别是safety/security/reliability/resilience/availability/privacy,而Owen对于可信软件特征的定义则包括4个方面,分别是Design&Clean Code, Availability&Resiliency, Security&Privacy, Release&Response,下面我主要来谈谈对Design&Clean Code的理解:

Design&Clean Code

在讨论为什么要开发可信软件之前,我们不妨反过来想一想,如果软件开发不可信,会给我们带来哪些问题呢?

最低级的,代码跑不起来,有bug;缺失最基本的测试,问题都遗留到了后期,增加了解决的成本;大量的隐藏问题增加了维护的成本;缺少合理的注释和文档使得知识资产流失;一点点小小的改动可能会引入无法评估的风险;技术债务的积累导致开发周期不断拉长……熟悉不熟悉,可怕不可怕,我觉得我们的团队就存在这些问题,有些只是初露苗头,有些已经陷入了恶性循环。问题如果长时间得不到解决,迟早有一天会把项目拖垮。所以可信软件开发实践至关重要。

尽可能自动化

在这里,Owen提到了一个很关键的问题,那就是重复的手工任务问题。他认为,重复的手工任务会带来很多问题,同时也是解决众多问题的突破口之一。在我看来,重复的手工任务带来的最大的问题就是时间的浪费,如果一项任务每天需要操作6次,每次10分钟,每个团队里有8个人都要做同样的操作,算算这些时间就已经很多了。再加上对于重复的工作,人工去做难免会由于误操作等原因引入这样那样的问题,解决这些烦人的问题也会吞噬不少时间。如果组内信息的流通不够流畅的话,多个成员就可能重复解决相同的问题,带来巨大的人力浪费。此外,对于本应花费在具有创造性开发活动的时间都浪费在了手工任务上,这也无形地增加了开发活动的成本,容易引发恶性循环。

所以,将重复的手工任务自动化是解决这些问题的最好办法,自动化也是Owen自始至终都在强调的东西。所有可以自动化的东西都要自动化,自动化省下来的时间可以做更加有生产力的事情。仔细想想看,我们社会的发展其实本质上都是各种自动化的结果。农业生产因为有了自动化机械得以大大提高产量,而解放的这部分人就可以从事其他更有生产力的活动;工业生产也是一样,自动化的流水线减少了大量的操作工,降低了企业的生产成本,提高了总体的产量。各种形式的自动化解放了人力,让人可以从事更有创造力的活动,进而催生新的自动化革命,这不就是一个能够拥有指数增长能力的正向循环吗!因此,我的非常同意Owen的观点,这也是我在工作中一直注意和探索的方向,那就是尽可能地自动化来提高工作效率,让自己能分配更多的精力在更有创造力的事情或者解决关键性问题上,种好土豆,产好粮食。

SOLID

好了,回到Design for Clean Code这一方面,让我们先来看一看,在软件开发活动的最主要的环节,我们都需要注意哪些方面。

首先,我们要写出好代码的前提是知道什么样的代码是好代码,好代码的形式有很多种,它们都遵循一些基本的原则,那就是SOLID设计原则,这是面向对象设计和编程最基本的5个原则。那我们来看一看,这SOLID到底都包含哪些内容。

  • S代表Single Responsibility,也就是单一功能原则,意思是每个对象应该仅有一种单一的功能。
  • O代表Open-Close,也就是开闭原则,简单说就是对扩展开放,对修改关闭。
  • L代表Liskov Substitution里氏替换原则,是芭芭拉·丽斯科夫在一个会议上首次提出来的,描述的是程序中的对象应该可以在不改变程序正确性的前提下被它的子类所替换的概念。
  • I代表Interface Segregation,即接口隔离原则,是说多个拥有特定功能的接口要好于一个宽泛用途的接口。
  • D代表Dependency Inversion,即依赖反转原则,是一种解耦,认为方法应该依赖于一个抽象而不是一个实例,不管是高层和底层之间的依赖,还是抽象和细节之间的依赖。

对于SOLID原则,这里就不展开讨论了,内容很丰富,网上也有很多资料,之后我会新写一篇文章谈谈对SOLID的理解。

重构

回到Clean Code,第二个重要的概念就是重构。对于重构,Owen讲了一些重构为什么如此必要的原因,以及重构在CloudSOP团队应用的成果,而我认为对于重构来说,最重要的一点在于转变观念。在不少人的认知里,重构属于额外的工作,和日常的需求开发是相互独立的,而且重构似乎更容易引入新的问题,尤其是重构能力比较弱,对既有代码的业务逻辑又不是很清楚的时候。让我们来看看,真的是如此吗?

我们不妨还用逆向思维来思考一下,如果我们的开发活动中从来都不重构,会带来怎样的问题。

一开始开发的老代码可能会有这样那样的架构问题,我们慢慢会发现,新增一个特性需要修改很多地方,甚至可能会引入一些难以评估的风险,当然开发周期也会拉长;

修改一处bug往往需要同时修改好几个地方,如果因为失误有一处改漏了,程序就容易出现一些难以定位的bug,如果这些bug存活到了现网,还会增加维护的成本和风险;

由于较不合理的设计,我们慢慢会在代码中发现一些重复实现的功能,或者在不同的地方直接复制了相同的代码块,这些地方如果出了问题,往往需要同时修改多处地方,你说容易引入bug不?

有些程序的UI、业务逻辑以及数据库实现没有做到抽象分离,如果更换了数据库,还要联动修改业务逻辑甚至是UI;同样的情况也可能出现在对三方件的引用上,更换了三方件之后,很多调用到的地方都需要重写。

虽然说软件不会磨损,但是它会腐化,存在破窗户效应,相信上面这些例子已经足够说明缺少重构的开发活动会带来哪些问题了。如果我们不能及时止损,这些问题就会不断蚕食软件的质量,吞噬我们大量本可以用于特性开发的时间,带来无休止的加班。而重构就是解决这些问题的重要方法之一。

那么,重构到底是什么,我们到底应该如何看待它呢?这里我谈谈我自己的理解。

首先,我们不应该把重构看成是一项独立于需求开发之外的活动。重构不是一个独立的、包含巨大工作量的活动,它应该是众多开发实践中的一个子活动而已。比如,开发需求的时候,需要做JSON转换,这种功能很可能在整个项目里经常会用到,那么我们可以把这个功能单独提出来,做成通用的模块,这样下次再开发其他需求用到JSON转换的时候就可以直接使用而无需重写一套了;再比如,新增一个特性的时候发现需要修改很多个地方,修改的内容都一样,那么能不能把这部分实现机制重构一下,使得下次再新增特性的时候,只需要修改一处就够了,或者我新增特性的时候不需要修改既有的代码,而只需要新增代码就可以了(想想开闭原则和某些设计模式);再或者,我发现代码中很多地方是直接调用三方件的接口,如果三方件因为某些特殊的原因需要被替换掉,那么所有调用的地方都需要同步修改,但如果我把它通过接口或者类给封装起来,那么假如后续三方件更换了,我只需要在封装它的地方适配修改一下就够了,所有调用的地方完全没有感知。我们可以看到,重构其实就是日常开发的一项小活动,就跟写if...else...一样,一次重构一点点,时间长了代码自然就越来越clean了。

其次,管理人员和开发人员一样,都应该有重构的意识,并且给予足够的重视。为什么这么说呢,我举个例子大家就明白了。某次迭代,要求实现一个功能,PM问“这个需求什么时候能交付?”,开发人员估摸了一下“2周后的迭代吧”,PM有点不满“这么简单的需求要这么久,下周交吧!”,开发人员有点不愿意“因为这个需求涉及一处老代码的修改,这部分代码需要重构一下,否则下次再动到这部分代码的时候,改动量会更大,还容易引入bug”,PM皱了皱眉“哎这个需求比较紧急,重构以后再说,先把功能实现了!”。于是,需求一周后交付了。但过了1个月,新需求来了,上面的对话又发生了一遍,需求也一样如期交付了,但是开发人员知道,这部分的代码不管是以后维护起来,还是再新增特性,都会消耗更多的时间,如果后面接手的同事对这部分代码不熟悉,还容易引入bug。可是这些,PM不知道,除非以后出了问题。毕竟,屁股决定脑袋。难道PM不希望产品拥有一个高质量吗?当然不是,只不过他更关注项目整体的进展,没有出现明显的质量问题即可,对于代码质量高不高,并不太重视。孙子兵法讲“上下同欲者胜”,对于重构这么重要的活动,PM和开发人员没有“同欲”,重构就无法得到充分的实施,软件质量自然也就无法得到充分的保障。

最佳实践

下来,我们来聊聊最佳实践,这些是Owen团队在实践过程中总结出来的一些经验,我说说我的感受,抛砖引玉:

  1. 统一代码风格,践行良好的编码模式和原则。代码风格好说,而模式和原则需要大量的实践和思考,即使是设计模式,也要思考到底是不是一定要引入设计模式,而不要手里拿着锤子看什么都像钉子。
  2. 引入代码自动检查(代码风格和复杂度)。我司搞的各种告警检查,CodeDEX清理等等就是干这个事儿的。
  3. 开发单元测试,设置代码覆盖率指标。单元测试是一个很好的实践,能够把问题尽可能地阻截在开发活动的前面,也就是修复成本比较低的阶段。
  4. 把自动测试作为一等公民。如果把测试活动和开发活动视作重要程度相等的活动,并且尽可能地使测试自动化,我认为一方面可以节省很多测试的时间,另一方面也可以尽早发现问题尽早修复。
  5. 尽早频繁地进行测试,给开发本地测试的能力。已经有3条最佳实践都是关于测试的了,可见Owen是多么地重视测试活动,而我们好像并不是这样。对于测试活动来说,尽早地测试可以尽早地发现问题,频繁地测试可以及时发现由于代码修改而可能引入的问题。而开发人员很难进行测试的一个重要的原因就是难以在本地进行测试。想想看,当你需要测试一个功能的时候,你需要额外花1天时间协调环境和资源,你还想去测试吗?
  6. 让富有经验的专家进行代码检视。代码检视这个事情,我的看法是:首先,有检视强过没检视;其次,检视人员的能力决定了检视质量的高低。检视是一项很有价值的活动,因为每个人的经验和能力不同,对程序的理解也不同,检视其实就是一种交流,能够帮助团队成员提升编码能力,毕竟直接通过代码学习别人的经验是最直接的方式。如果一个团队里有一个大牛,那么通过这种方式,其他成员能很快地学习到大牛的编程思想和编码技巧;但如果大家的水平都比较平平,能力的提升就比较有限了,不过即使这样,也好过没有检视。
  7. 构建过程自动化。谁都不想手动去构建一个大型项目吧。
  8. 重构坏代码。关于重构,我上面已经说了很多,在这里补充一点,就是重构最好是以小步增量的方式进行,一次改一点,除非迫不得已先不大动。毕竟修改引入bug的可能性往往高于重构可能带来的好处,一次改一点可以把修改限定在一个可控的范围内,而且这样还有利于进行单元测试。

其实整体看下来,我有一个感受,那就是整个活动都是在提高效率管理bug的。对于效率提升来说,就是尽可能地自动化;对于bug管理来说,就是尽可能地让问题早暴露出来,越早修复成本越低,越早也越容易修改,越早还可以把控风险。不管怎样,如果你前期没有进行充分的测试,那么问题就会在后期比如上网之后暴露出来,你还是要解决,那个时候解决的成本和风险都会高很多,可能发布前你感觉还挺轻松,发布后就会因为修复各种各样的问题疲于奔命,形成恶性循环。但是反过来,如果你在发布前就能把问题找出来解决掉,也许你会花很多精力在测试上,但是上网之后,稳定的软件会让你轻松很多,也给你节省了很多时间可以投入下一阶段产品的开发中。简单说,债务就那么多,早还利息少,晚还玩命搞。