一家之言,可以在评论里探讨
写代码虽然大多数时候是个体力活,但不可否认,也需要一点品位。我曾经觉得代码质量很重要,后来写业务写多了,又觉得如果连代码正确都做不到,又谈何代码质量。后来我又醒悟了,这世上很难有 bug free 的代码,当出现 bug 的时候,好代码比烂代码会好改很多。我们今天就讨论下什么是好代码,毕竟一个不知道什么样的代码是好代码的人是不可能如有神助写出好代码的,写代码可以搜索复制黏贴三板斧,写好代码却是必须刻意练习的。
我觉得写代码分为两个部分:
所谓的结构设计不是说一定要画个架构图,写个系分文档什么的,结构设计和功能实现其实螺旋贯穿在整个写代码的过程中。当我们准备完成一个需求的时候,会把需求分成几个功能,这些功能如果互相独立,便不涉及交互,否则他们之间就需要沟通,可能是直接调用,可能是发送消息,可能是监听变化,可能是轮询结果等等。分了功能之后,要实现其中某个功能,又要递归的执行一遍上述过程,直到写下一行行代码。有同学可能觉得这种自顶向下的过程太宏观了,前期太费时间,什么模块什么交互,我就挑个功能一把梭,代码先写起来。这当然也可以,而且大多数人都是这么做的。但这其实也包含结构设计,你准备率先实现的那个功能,潜意识里你已经把它从整个系统中分离出来了,只是系统的其他部分暂时先不管而已。模块划分是个说烂的话题,但它又真的是软件工程的精髓,它的意义在于,人管理复杂度的能力是有限的,当一大坨代码怼在一起的时候,哪怕代码质量再高,注释再详尽,也会引起生理上的不适。这种不适最容易发生在当你要修改一个小功能,找了半天代码找不到的时候。划分了之后,哪怕是好几坨烂代码,但你改动的时候只改其中一个文件,其他代码也是眼不见心不烦。
那模块如何划分?我们可以说出一些普适的原则,譬如高内聚低耦合、单一职责原则、开闭原则等等,但这些东西说起来感觉很套路很不真诚,让人觉得无从下手。我个人觉得有两条很重要的原则:
关于反思代码结构,最重要的当然是自己的思考,不要迷信别人给你做的设计,也不要迷信自己当初的设计,我大概列举几种情况:
这些情况当然列举不完,团队中其实可以定一些硬性指标来辅助模块划分,譬如一个文件最多 300 行,一个函数最多 70 行什么的,放在 Lint 规则里。我们有很多约定俗成的“潜规则”其实都有它背后的逻辑,譬如以前天天说的 MVC 和 MVVM 两个模式,他们的最主要区别不在于模块划分,而是模块间的交互,在划分方面它们都致力于让 UI 和逻辑分开,为什么呢?因为 UI is cheap,UI 是隔三差五会被设计师推翻再来一套的,但逻辑和数据,相对会稳定一点,所以把他们分开,UI 迭代的时候涉及的改动就比较小。那为什么前端的 React/Vue 这些近年大火的框架,又提倡所谓的组件(Component),貌似 是要把 UI 和逻辑搞在一起呢?其实不是的,组件是一个小粒度的模块,它相对独立,具有很高的内聚性,组件中的“逻辑”更多的应该是 UI 相关的交互逻辑,而不是那些比较底层的逻辑,我们还是应该把相对稳定的逻辑抽出来放到更下层。
关于命名,很多同学可能不在意,觉得代码能跑就行了,取名字有什么关系,看不懂我加注释嘛。这是非常不好的习惯,因为命名的过程中其实就是在概括你这段代码,如果你的某个函数名叫 xxxAndxxx 那这个函数就应该被拆成两个函数,它明显违反了单一职责原则。大到业务模块小到辅助函数,只要你觉得不好命名,那就是一个信号,说明这段代码做的事情太多太杂,以至于你无法用几个单词概况出来。
现在我们具体聊聊代码实现的时候怎么体现代码品位,我觉得主要可以从两点着手提高:
有一个广为人知的观点,编程思路最重要,语言只是工具。乍一听,编程语言似乎无足轻重。如果只是一锤子买卖,写段代码实现个功能,写完离手,再不相干,那语言当然不重要。我相信大多数码农都可以很快上手一个新语言,因为要实现一个功能可能只需要一些通用的核心特性,对 C 系语言来说,知道函数/分支语句/循环语句/字符串/数组/散列表这些东西的使用就足够开发日常需求了,而这些特性说实话在 C 系语言中都大通小异。但如果要写好代码,就得向着精通这门语言努力。有些功能你写一堆蹩脚代码可能实现得马马虎虎,但用了某个特性,几行短小精悍的代码就解决了。我很排斥一个观点是,为了让团队的所有人都能看懂,鼓励只使用语言的基本特性,一些高级的或者不太常用的特性不准用,用了就是炫技。举个极端点的例子,有人觉得 if else 比三元表达式更可读,就鼓励只使用 if else。这样的“可读”在我看来只是迎合平庸的码农。有些语言特性确实有利有弊,有的甚至只有弊(JS 中就很常见),那尽量不用,或者根据实际场景做取舍。我们取舍的标准是“场景”,而不应该是人。什么叫合适的场景呢,还是拿三元表达式举例:
const data1 = a > b ? 100 : 200;
const data2 = a > b ? 300 : 400;
我看到过有同学这样用,这段代码虽然只有两行,但 a > b 这个条件要判断两次,性能我们且不论(a > b 只是个示例,实际的条件可能更复杂),至少已经重复了,写的时候写两次,读的人也要看两次,不如就
let data1 = 200;
let data2 = 400;
if (a > b) {
data1 = 100;
data2 = 300;
}
或者把 a > b 这个 condition 提前计算好,避免计算两次(这种优化不是针对性能,因为有些语言编译器在编译期会做类似这种优化,主要还是可读性):
const condition = a > b;
const data1 = condition ? 100 : 200;
const data2 = condition ? 300 : 400;
代码要简洁,但不是简短(代码行数少),简洁的意思是逻辑清晰,没有冗余信息。还有一个场景是我有看过同学用嵌套的三元表达式来替代多个 if else 的操作,那真的是可读性很差了,不要这样。
这方面也有一些具体的技巧,譬如我个人很喜欢用散列表去代替分支语句:
// if (key === 'x') return 'xxx';
// if (key === 'y') return 'yyy';
// if (key === 'z') return func;
const xxxMap = {
x: 'xxx',
y: 'yyy',
z: func
};
return xxxMap(key);
这种做法好像有个名字叫“表驱动设计”,这样做有很多好处,代码简洁是一点,这个 xxxMap 其实是一张配置表,以后可以抽出去放到单独的地方,甚至放到服务端去配置,就可以很容易实现一些动态化需求。更延伸开去讲,能配置化的东西尽量都配置化,方便以后扩展功能。
说了语言重要,那思路呢?当然也重要。我们日常生活中,逻辑清晰的人三言两语就直击要害,逻辑混乱的人兜兜转转还是云里雾里。代码也一样,有些糟糕的代码是糟糕在啰嗦,一个判断能搞定的事情它可能要变着法判断两三次。这种呢,就是本身思路不简洁,写出来的代码自然也简洁不了,只能努力提高自己的逻辑能力。
剩下的代码风格,也非常重要。长得好看的人是有特权的,长得好看的代码自然也有。多看看官方或者大厂的 Style Guide,平常多注意空格换行缩进命名风格等等,装个优秀的格式化插件也好。
差不多就先这样吧,品味不见得有好坏,但有高低,共勉。