写代码的指导思想:如何写出易测、清晰、健壮的牢固代码

代码只是形式,逻辑和思考才是神韵。


背景

写出 BUG 不算糟糕,给人埋坑,让别人写出 BUG ,耗时耗力才更令人讨厌。要想不写出 BUG, 不埋坑,需要用心写出 “易测、清晰、健壮” 的牢固的代码。95% 的代码,能做到这一点,就可以保证几乎无问题了;3%的代码能做到“可复用、可扩展”,善莫大焉!

本文结合之前的经历和案例,探讨如何写出“易测、清晰、健壮”的代码。

代码三性

代码的基本标准是:易测、清晰、健壮。

易测

易测,是说代码容易测试。写代码的第一思想是:不要对代码过于自信

有人说,一行代码能出什么问题? 可耗资巨费的火箭因为一行代码发射失败,金融行业因为一行代码使数亿美元蒸发,都是已发生过的案例。我个人也遭受过一次小小的惩罚,可参见:“订单搜索分页失效的教训:怠惰必受惩罚”

有人说,异常有什么好测的 ? 甚至连是否能编译通过都不检查。结果就出现了这个案例:“遗留问题,排雷会炸,不排也会炸!”。走到了异常分支,你才知道痛!

因此,写代码宜慎之又慎,一行尚且会潜藏问题,何况百行?

那么,如何写出容易测试的代码呢?其基本思想是:将可测逻辑与主流程、外部依赖分离。可测逻辑,主要指含有了 if, if-else, while, for 等条件、循环的逻辑。与主流程、外部依赖分离,就是集中兵力解决最核心部分。放在主流程里,就导致易测逻辑受其他部分影响,连带考虑的东西太多,耗费脑力,且测试很容易不彻底(不容易覆盖到所有情形);与外部依赖放在一起,就要 mock 外部依赖,写一些不必要的测试代码。

“改善代码可测性的若干技巧” 这篇文章讲述了一些用于编写易测代码的基本技巧:“代码语义化”、“分离独立逻辑”、“分离实例状态”、“表达与执行分离”、“分离纯函数”、“面向接口编程”; “使用Groovy+Spock轻松写出更简洁的单测” 这篇文章则将如何使用好的框架去容易地编写单测。

清晰

清晰,是说把代码写明白,一看就懂,无需猜测。

要把代码写清晰,就要保证“语义与细节分离”,即将“做什么”(dowhat) 与 “怎么做”(howtodo)分离开。在主流程里,坚持只叙述 dowhat ,只在函数或方法里写清楚 howtodo 。

要达成良好清晰的语义,需要望文知义的命名。这又涉及到一个基本思想:单一事实职责。相信 80% 的开发者都听说过这个基本思想,但真正能始终贯彻的人不多。单一事实职责,就是每个方法,每个类只做分内的事情。你把自己当成一个将军,给每个方法、每个类去分配职责,能够把这个职责分配清楚,才有做将军的才能。

从“单一事实职责”以及其他软件思想中,可进一步提炼出一个核心原则:“关注点分离”。关注点分离原则,是确保写出清晰代码的最重要的根本原则。后文再详述。

把代码写清晰,也要落实到注释里。有的注释:“有个场景需要下单后马上查看订单详情”,就写的不明白:哪个场景?当人员变动时,后面的人就要花很大的沟通成本去找到这个场景。

健壮

健壮,主要指代码应对错误的能力。 健壮,可分为技术健壮性和业务健壮性。

技术健壮,就是从技术层面来考察错误。技术健壮是健壮性的基本考量。比如一次请求调用,如果超时怎么处理,如果依赖服务报错怎么处理 ? 如果资源不存在怎么处理 ?对象的一次方法调用,如果对象为空怎么处理?如果方法未能执行成功抛异常怎么处理? 要保证技术健壮性,需要采用“防御式编程”。

业务健壮,就是从业务层面来考察错误和变化。比如同城配送异常检测功能,是同城配送是否成功的检测。正常场景下,“同城送订单自动呼叫失败”,做个标识,是正常流程处理。可是,业务上还允许,同城配送失败后可以发货完成,这个就是业务的“异常场景”。如果不考虑这个场景,那么订单实际上配送成功了,但依然显示配送异常。再考虑一个栗子。零售网店订单派送仓库发货,正常场景下,一个订单只会有一个派送记录;但是,网店订单还可以改派,这时候,一个订单会有多个派送记录,必须过滤掉那些无效的派送记录。要保证业务健壮性,需要有“闭环与全局“思想。不仅仅只想到眼前这个业务,还要考虑这个业务与其他业务联合的情形,以及该业务 5%-10% 的非常规路径。

很多线上问题 和 BUG,都是健壮性不佳所导致; 如果在关键路径上健壮性不佳,还可能导致重大故障。

不报问题,勿以为安全无忧;不是没有,只是没发现,或没报上来,或没计较。等要计较时,麻烦就来了。因为这麻烦有可能正好插在你非常忙的时候,更容易令人心烦意乱。

要避免日后的麻烦,就要彻底干净地做好健壮性。有时会看到一段代码,捕获了异常,却什么都没做,或者简单打了个日志 log.error("failed") 。等于什么都没做。可见这做事态度毛毛糙糙。 三年写这样的代码,三年没长进。 学了技术,做事态度依然毛糙,是没法令其担当大任的。

错误日志,要做规范细致,需要尽量能够一眼就能断定问题所在。可参见: “如何使错误日志更加方便排查问题”

指导思想

凡事都可以分为基本原理和实战经验两部分。 基本原理,是帮助人认识一些基本情况,给予一些基本指导和方法;实战经验,则是遇到具体情况时,应用和扩展基本原理来解决问题。

要写出“易测、清晰、健壮”的代码,是有一些基本指导思想的;在基本指导思想之下,有一些法则及技巧辅助。

关注点分离

这是我认为的最核心的指导思想,可以推演出很多其他的软件设计和开发思想。

我深认为,软件中蕴藏着不计其数的大大小小的关注点。软件开发和设计的本质,就是将关注点分离、组织、连接。 能够将不同的关注点分离开,再合理有序地组织起来,呈现在代码里,就离写出清晰代码不远了。

关注点,可以分为技术关注点和业务关注点。通俗地理解,一个“关注点”,可视之为一个“事实”;但关注点的含义,比事实要广泛得多。

技术关注点,侧重于解决某一类技术问题。比如线程池、连接池、事务、幂等、切面、异步、MVC等。 “代码抽象与分层” 列举了代码里很多细小的抽象和关注点。

业务关注点,侧重于描述某个业务点。比如订单已发货、订单全额退款、获取订单已退金额,等都是业务关注点。业务关注点可大可小,小至一个业务常量,大至一个下单的基本流程。

能够将技术关注点和业务关注点抽离出来,对实现可复用、可扩展、可定制大有裨益。

从“关注点分离”思想,可以推导出很多重要的软件开发和设计思想。

  • 从关注点分离思想,可以推导出“语义与细节”分离思想。因为语义与细节分别是一类关注点:语义是宏观性的,细节是微观性的;
  • 从关注点分离思想,可以推导出“单一事实”(SRP)和“开闭原则”(OCP)。因为关注点分离出来,就更容易地维护、复用和扩展;
  • 从关注点分离思想,可以推导出 “基于接口设计与编程”思想,因为接口是关注点的抽象,而实现是关注点的具体实施;基于接口设计与编程的示例可见:“基于接口编程:使用收集器模式使数据获取流程更加清晰可配置”“基于接口设计与编程”
  • 从关注点分离思想,可以推导出“封装和多态”思想,因为封装本质上就是关注点的重组,而多态则是“将相异关注点进行分离,然后将相同关注点和相异关注点进行组合”;
  • 从关注点分离思想,可以推导出“组件化”思想,因为组件实质是多层次关注点的组合。

关注点分离,是近乎于“道”的统摄全局的根本思想


make-it-right-then-good

好代码并非一蹴而就的。如果有人能一次性写出易测、清晰、健壮的代码,那这人很了不得。

和大多数开发者一样,我也急于完成业务逻辑。但是,实现业务逻辑只是起点,并不是结束。从起点到结束,还需要持续的修改和完善。

持续小幅重构,精炼代码“精练代码:一次Java函数式编程的重构之旅” 展示了如何用函数式编程思想和技巧,来持续改善代码,使得代码更加精练而可复用。

有人觉得,这样费事不 ? 代码只是形式。写代码的本质,是表达逻辑,表达思考的结果。 最终的代码,会折射出思考和表达的质量。 这才是一个程序员的核心竞争力。

不可变思想

不可变思想,是从函数式编程借鉴过来。意为“优先使用不可变量”。

在编写函数和方法的代码时,忌修改传参。一旦修改传参,就会使参数变成隐式的全局变量,在大的业务系统里,久而久之就很难知道里面究竟有什么,什么时候有什么时候没有,不知道能依靠和信任什么。全局变量是那种一旦出错,能耗上好几个小时排查让头发瞬间花白的东西,属于 “no zuo no die” 的神奇代码物种。

因此,函数和方法如果需要返回值,尽量使用返回值,而不是修改传参。这样做的一个好处是,代码也更容易测试。

软件开发的一项挑战是“软件行为的可预测性”。不可变思想,可以增强软件行为的可预测性,减少很多推断行为。比如说,使用线程安全的变量和共享不可变变量,孰优孰劣? 前者虽然也能保证并发的安全性,可说到底还需要反复分析推敲,且规模越大时越难以分析推导;后者就是不可变的,根本无需分析和推导。当然,软件中不可能一直使用不可变量,因此,原则是:优先使用不可变量

知己知彼

代码的目标是提供正确有效的服务,而 BUG 的目标则是阻止代码实现目标或完全实现目标;从目标意义来看, BUG 就是代码的敌人;从根源来看,BUG 又与代码相生相伴。

知己知彼,才能百战不殆。每一次开发、测试、发布,实现需求或优化,都是一场战役。要想不出错,除了慎之又慎,还需要知彼。知彼指知道代码中的常见问题以及如何避免。

“代码问题及对策” 列举了在程序员职业生涯中可能会遇到的 90% 的错误;“故障常见原因归类分析及预防和应对措施” 列举了可能导致故障的各种原因及预防应对措施。

写下的每一行代码,能够评估出可能出现什么问题,有心避免这些问题,就已经很了不起了。

小处着手

这么多思想,这么多法则,这么多技巧,这么多问题,从何处着手呢?

从小处着手。 在写代码时,始终牢记“关注点分离”思想,坚持编写单一事实的短小方法,这是基本功;其次,多思考如何写得更清晰简洁;再次,适当学点设计思想和模式,让代码出有据,具柔性;最后,勤积累,在实战中积累经验,及时总结。

匠心态度

写代码应持匠心。要用一种精致的态度去写代码,才能写出优美而牢固的代码。

忌随意散漫。随意散漫,并不是说闭着眼睛写代码,或者来个葛优躺地写代码;而是说,写代码只考虑快速去实现逻辑,而不考虑如何更清晰简洁地表达这个逻辑。这样,很容易养成一些不良习惯而不自知,久而久之,代码乃至设计的水平就会停滞不前,仅学会了一点技术来充饥。

在代码里随意写个魔数,就是一种随意散漫的习惯。因为魔数,本质上就是一个细小的关注点,也是轻忽不得的。“代码的味道” 列举了若干个比较典型的不良习惯及如何纠正;“如何编写可信赖的代码” 列举了许多细小的法则和技巧来帮助编写可信赖的代码。

匠心态度,也需要自律和努力写标准规格的代码。建筑、机械、电子等行业,对于许多零部件,都有各种标准规格的制定,而在实际构建成品时,基本是采用标准规格的部件。反观软件行业,基本没有多少标准规格的东西,总觉得自己能造出更好的轮子,结果大部分都被丢弃了,很多工作成果都没有得到很好的继承和发展。一个业务系统的大部分代码,放到另一个业务系统里,基本不可用。

小结

代码的基本标准是:易测、清晰、健壮。要做到这点,“关注点分离”思想是根本指导思想,是近乎于“道”的统摄全局的根本思想;辅以多种思想、法则和技巧,熟知各种常见问题,就能写出牢固的代码。

道是生发出法则和技巧的根本层面。遇事犹疑不决时,要回溯到道的层面来解决问题。

posted @ 2019-10-13 11:53  琴水玉  阅读(1781)  评论(0编辑  收藏  举报