万字详文阐释程序员修炼之道

作者:cheaterlin,腾讯 PCG 后台开发工程师

综述

我写过一篇《Code Review 我都 CR 些什么》,讲解了 Code Review 对团队有什么价值,我认为 CR 最重要的原则有哪些。最近我在团队工作中还发现了:

  • 原则不清晰。对于代码架构的原则,编码的追求,我的骨干员工对它的认识也不是很全面。当前还是在 review 过程中我对他们口口相传,总有遗漏。

  • 从知道到会做需要时间。我需要反复跟他们补充 review 他们漏掉的点,他们才能完成吸收、内化,在后续的 review 过程中,能自己提出这些 review 的点。

过度文档化是有害的,当过多的内容需要被阅读,工程师们最终就会选择不去读,读了也仅仅能吸收很少一部分。在 google,对于代码细节的理解,更多还是口口相传,在实践中去感受和理解。但是,适当的文档、文字宣传,是必要的。特此,我就又输出了这一篇文章,尝试从'知名架构原则'、'工程师的自我修养'、'不能上升到原则的几个常见案例'三大模块,把我个人的经验系统地输出,供其他团队参考。

知名架构原则

后面原则主要受《程序员修炼之道: 通向务实的最高境界》、《架构整洁之道》、《Unix 编程艺术》启发。我不是第一个发明这些原则的人,甚至不是第一个总结出来的人,别人都已经写成书了!务实的程序员对于方法的总结,总是殊途同归。

细节即是架构

(下面是原文摘录, 我有类似观点, 但是原文就写得很好, 直接摘录)

一直以来,设计(Design)和架构(Architecture)这两个概念让大多数人十分迷惑--什么是设计?什么是架构?二者究竟有什么区别?二者没有区别。一丁点区别都没有!"架构"这个词往往适用于"高层级"的讨论中,这类讨论一般都把"底层"的实现细节排除在外。而"设计"一词,往往用来指代具体的系统底层组织结构和实现的细节。但是,从一个真正的系统架构师的日常工作来看,这些区分是根本不成立的。以给我设计新房子的建筑设计师要做的事情为例。新房子当然是存在着既定架构的,但这个架构具体包含哪些内容呢?首先,它应该包括房屋的形状、外观设计、垂直高度、房间的布局,等等。

但是,如果查看建筑设计师使用的图纸,会发现其中也充斥着大量的设计细节。譬如,我们可以看到每个插座、开关以及每个电灯具体的安装位置,同时也可以看到某个开关与所控制的电灯的具体连接信息;我们也能看到壁炉的具体位置,热水器的大小和位置信息,甚至是污水泵的位置;同时也可以看到关于墙体、屋顶和地基所有非常详细的建造说明。总的来说,架构图里实际上包含了所有的底层设计细节,这些细节信息共同支撑了顶层的架构设计,底层设计信息和顶层架构设计共同组成了整个房屋的架构文档。

软件设计也是如此。底层设计细节和高层架构信息是不可分割的。他们组合在一起,共同定义了整个软件系统,缺一不可。所谓的底层和高层本身就是一系列决策组成的连续体,并没有清晰的分界线。

我们编写、review 细节代码,就是在做架构设计的一部分。我们编写的细节代码构成了整个系统。我们就应该在细节 review 中,总是带着所有架构原则去审视。你会发现,你已经写下了无数让整体变得丑陋的细节,它们背后,都有前人总结过的架构原则。

把代码和文档绑在一起(自解释原则)

写文档是个好习惯。但是写一个别人需要咨询老开发者才能找到的文档,是个坏习惯。这个坏习惯甚至会给工程师们带来伤害。比如,当初始开发者写的文档在一个犄角旮旯(在 wiki 里,但是阅读代码的时候没有在明显的位置看到链接),后续代码被修改了,文档已经过时,有人再找出文档来获取到过时、错误的知识的时候,阅读文档这个同学的开发效率必然受到伤害。所以,如同 golang 的 godoc 工具能把代码里'按规范来'的注释自动生成一个文档页面一样,我们应该:

  • 按照 godoc 的要求好好写代码的注释。

  • 代码首先要自解释,当解释不了的时候,需要就近、合理地写注释。

  • 当小段的注释不能解释清楚的时候,应该有 doc.go 来解释,或者,在同级目录的 ReadMe.md 里注释讲解。

  • 文档需要强大的富文本编辑能力,Down 无法满足,可以写到 wiki 里,同时必须把 wiki 的简单描述和链接放在代码里合适的位置。让阅读和维护代码的同学一眼就看到,能做到及时的维护。

以上,总结起来就是,解释信息必须离被解释的东西,越近越好。代码能做到自解释,是最棒的。

让目录结构自解释


ETC 价值观(easy to change)

ETC 是一种价值观念,不是一条原则。价值观念是帮助你做决定的: 我应该做这个,还是做那个?当你在软件领域思考时,ETC 是个向导,它能帮助你在不同的路线中选出一条。就像其他一些价值观念一样,你应该让它漂浮在意识思维之下,让它微妙地将你推向正确的方向。

敏捷软件工程,所谓敏捷,就是要能快速变更,并且在变更中保持代码的质量。所以,持有 ETC 价值观看待代码细节、技术方案,我们将能更好地编写出适合敏捷项目的代码。这是一个大的价值观,不是一个基础微观的原则,所以没有例子。本文提到的所有原则,或者接,或间接,都要为 ETC 服务。

DRY 原则(don not repeat yourself)

在《Code Review 我都 CR 些什么》里面,我已经就 DRY 原则做了深入阐述,这里不再赘述。我认为 DRY 原则是编码原则中最重要的编码原则,没有之一(ETC 是个观念)。不要重复!不要重复!不要重复!

正交性原则(全局变量的危害)

'正交性'是几何学中的术语。我们的代码应该消除不相关事物之间的影响。这是一给简单的道理。我们写代码要'高内聚、低耦合',这是大家都在提的。

但是,你有为了使用某个 class 一堆能力中的某个能力而去派生它么?你有写过一个 helper 工具,它什么都做么?在腾讯,我相信你是做过的。你自己说,你这是不是为了复用一点点代码,而让两大块甚至多块代码耦合在一起,不再正交了?大家可能并不是不明白正交性的价值,只是不知道怎么去正交。手段有很多,但是首先我就要批判一下 OOP。它的核心是多态,多态需要通过派生/继承来实现。继承树一旦写出来,就变得很难 change,你不得不为了使用一小段代码而去做继承,让代码耦合。

你应该多使用组合,而不是继承。以及,应该多使用 DIP(Dependence Inversion Principle),依赖倒置原则。换个说法,就是面向 interface 编程,面向契约编程,面向切面编程,他们都是 DIP 的一种衍生。写 golang 的同学就更不陌生了,我们要把一个 struct 作为一个 interface 来使用,不需要显式 implement/extend,仅仅需要持有对应 interface 定义了的函数。这种 duck interface 的做法,让 DIP 来得更简单。AB 两个模块可以独立编码,他们仅仅需要一个依赖一个 interface 签名,一个刚好实现该 interface 签名。并不需要显式知道对方 interface 签名的两个模块就可以在需要的模块、场景下被组合起来使用。代码在需要被组合使用的时候才产生了一点关系,同时,它们依然保持着独立。

说个正交性的典型案例。全局变量是不正交的!没有充分的理由,禁止使用全局变量。全局变量让依赖了该全局变量的代码段互相耦合,不再正交。特别是一个 pkg 提供一个全局变量给其他模块修改,这个做法会让 pkg 之间的耦合变得复杂、隐秘、难以定位。

全局 map case
单例就是全局变量

这个不需要我解释,大家自己品一品。后面有'共享状态就是不正确的状态'原则,会进一步讲到。我先给出解决方案,可以通过管道、消息机制来替代共享状态/使用全局变量/使用单例。仅仅能获取此刻最新的状态,通过消息变更状态。要拿到最新的状态,需要重新获取。在必要的时候,引入锁机制。

可逆性原则

可逆性原则是很少被提及的一个原则。可逆性,就是你做出的判断,最好都是可以被逆转的。再换一个容易懂的说法,你最好尽量少认为什么东西是一定的、不变的。比如,你认为你的系统永远服务于,用 32 位无符号整数(比如 QQ 号)作为用户标识的系统。你认为,你的持久化存储,就选型 SQL 存储了。当这些一开始你认为一定的东西,被推翻的时候,你的代码却很难去 change,那么,你的代码就是可逆性做得很差。书里有一个例证,我觉得很好,直接引用过来。

与其认为决定是被刻在石头上的,还不如把它们想像成写在沙滩的沙子上。一个大浪随时都可能袭来,卷走一切。腾讯也确实在 20 年内经历了'大铁块'到'云虚拟机换成容器'的几个阶段。几次变化都是伤筋动骨,浪费大量的时间。甚至总会有一些上一个时代残留的服务。就机器数量而论,还不小。一到裁撤季,就很难受。就最近,我看到某个 trpc 插件,直接从环境变量里读取本机 IP,仅仅因为 STKE(Tencent Kubernetes Engine)提供了这个能力。这个细节设计就是不可逆的,将来会有人为它买单,可能价格还不便宜。

我今天才想起一个事儿。当年 SNG 的很多部门对于 metrics 监控的使用。就潜意识地认为,我们将一直使用'模块间调用监控'组件。使用它的 API 是直接把上报通道 DCLog 的 API 裸露在业务代码里的。今天(2020.12.01),该组件应该已经完全没有人维护、完全下线了,这些核心业务代码要怎么办?有人能对它做出修改么?那,这些部门现在还有 metrics 监控么?答案,可能是悲观的。有人已经已经尝到了可逆性之痛。

依赖倒置原则(DIP)

DIP 原则太重要了,我这里单独列一节来讲解。我这里只是简单的讲解,讲解它最原始和简单的形态。依赖倒置原则,全称是 Dependence Inversion Principle,简称 DIP。考虑下面这几段代码:

package dip

package dip

type Botton interface {
    TurnOn()
    TurnOff()
}

type UI struct {
    botton Botton
}

func NewUI(b Botton) *UI {
    return &UI{botton: b}
}

func (u *UI) Poll() {
    u.botton.TurnOn()
    u.botton.TurnOff()
    u.botton.TurnOn()
}
package javaimpl

import "fmt"

type Lamp struct {
}

func NewLamp() *Lamp {
    return &Lamp{}
}

func (*Lamp) TurnOn() {
    fmt.Println("turn on java lamp")
}

func (*Lamp) TurnOff() {
    fmt.Println("turn off java lamp")
}
package pythonimpl

import "fmt"

type Lamp struct {
}

func NewLamp() *Lamp {
    return &Lamp{}
}

func (*Lamp) TurnOn() {
    fmt.Println("turn on python lamp")
}

func (*Lamp) TurnOff() {
    fmt.Println("turn off python lamp")
}
package main

import (
    "javaimpl"
    "pythonimpl"
    "dip"
)

func runPoll(b dip.Botton) {
    ui := NewUI(b)
    ui.Pol
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值