好好写代码之命名篇——推敲

简介: 好好写代码之命名篇——推敲

(贾)岛初赴举京师,一日驴上得句云:“鸟宿池边树,僧敲月下门”。始欲着“推”字,又欲着“敲”字,练之未定,遂于驴上吟哦,时时引手作推敲之势。

—— 宋·胡仔《苕溪渔隐丛话》卷十九引《刘公嘉话》

命名,是编码中最为紧要的事情,其之于程序,便如脸面之于少女。好的命名,能清晰的传达代码的意图,甚而,有一种韵律的美感。而懒散随意的起名,则令人如堕云雾,不忍卒读,会一遍遍地消耗维护者的精气神儿。此外,混乱的命名体系,能轻巧的掩藏 BUG,贻祸千里。

因此,我们在写代码时,有必要花一点时间,对关键命名进行推敲,与人方便,与己方便。对于生命周期越长的项目,其益处便越明显。那么该如何推敲呢?结合自己写代码、看代码、Review 代码的一些经验,聊聊我的一些体会。

最近写 golang 多一点,因此例子用的都是 golang ,但都是伪代码,有些例子并不严格遵从语法。此外,例子大多出于现造,因此可能并不是特别贴合。

作者:木鸟杂记 https://www.qtmuniao.com/2021/12/12/how-to-write-code-scrutinize-names 转载请注明出处

诚信

说到命名,其实有很多原则,我思来想去,觉得最需要强调的一个思想便是——诚信(笑)。我们在写代码、改代码时决计不能挂羊头卖狗肉,做有意无意的骗子。换言之,要让命名要真实、完整的体现意图。否则,维护者很容易受命名误导,先入为主,忽略一些细节,甚而,忽略一些 bug。

最常见的有几种情形。

其一,函数在做 A 的逻辑,却随意起了一个 B 的名字。这种情况要么是起名太随意了,觉得无关紧要,要么是函数逻辑太复杂,找不到合适的名字,便随意安了一个。前者态度有问题,我们按下不表。后者一般是需要将逻辑进行拆分,拆到名字能清晰体现其逻辑为止。

其二,函数有副作用,但是名字中没有体现

func (c *Company) check(p *Person) {
 if p.age < 0 {
  panic("age can't be negative")
 } 
 c.p = p
}

上面代码中,既然名字说只是 check,便不能偷偷的 set。如果也想 set,可以改名为 checkAndSet()

另外一个典型的例子:看起来像纯函数,却偷偷的改变了全局状态;

其三,对代码进行了改动,名称却没有随之变动。比如,我们改造一个函数时,塞了一些新逻辑,却维持其名字不变;甚而,我们完全改变了代码逻辑,却没有改动名字;当然最常见的是,代码变了,注释却忘改了。

// pseudocode
func (c *Content) dumpToFile() error {
 // origin code
 f, _ := os.Open(fiename)
 f.Write()
 // added code
 db, _ = getDb()
 db.Add()
}

其四,用一些约定俗成的名字去指代不同含义。比如在 golang 中,说到 context,我们一般会想到指官方库中用于控制生命周期的 Context。那么在命名时,就不要随意占用这个名字,如某种情况下,我们需要一个保存任务运行上下文的类,务必加上前缀,比如 JobContext ,这时如果直接用 Context 作为类名,哪怕细读之后能理解其含义,也会让人感觉很别扭。

直白

当我们实现某个功能时,会反复思考很多细节,这些细节便是我们当时思想的上下文。处在这些上下文之中,我们很容易写出一些当时觉得无比自然,后面看来无比迷惑的代码。因为随着时间流逝,你脑中的上下文会消失。

比如,当判断某个条件时,用了一个很隐晦、间接或者反直觉的判断语句。这种情况,可以换成更直接的信号,或者通过增加一个变量,以变量名字来进行释义。

func (p *Project) Get(req *GetRequest) (*GetResponse, error){
 if req.names == nil {
  resp := &GetResponse{}
  resp.Teacher = &Teacher{}
  return resp, nil
 }
 // xxxxx
}

上面例子本来想表达, 当 req.names 是一个非空数组时,则只对该数组指定学生信息进行返回;如果 req.names == nil 时,则表明为项目组全部学生,需要额外返回老师的信息。但是后面这个信息就完全是隐式的、反直觉的。

可以通过一些手段将其变为显式,比如 bool isAll = req.names == nil 。同理,对于长一些匿名函数,最好也将其赋给一个变量,通过变量名来对函数进行释义。

因此,最好能不断带入他人视角思考,持续消除隐式上下文依赖,才能写出符合直觉、无须过多注释的代码。另外,多找几个不具有这种实现细节(但最好明白设计方案)上下文的人来 Review 也是一种很好的消除隐式依赖手段。

简洁

有的命名简直又臭又长,须知超过三个词的命名并不能使语义更清楚,还会加重理解负担。出现这种情况,一般是没有仔细推敲,利用程序结构来消除信息冗余,举几个例子。

其一,**通过层级信息消除冗余。**比如包名(或者命名空间,看语言而定)、类名。

package student
type Student struct {}
func NewStudent() (*Student, error)

上述包名中已经表明是 student ,则函数名中可以省去 Student 字样,即改为 func New()(*Student, error) ,使用时 student.New()  便可以清楚地看出 New 的对象类型。

借助数据结构的视角来看,通过树形组织来概念,可以让单个树节点的命名变得相对简单,同时利用树的路径来表达足够丰富的含义。

其二,利用参数名来消除冗余

func handleStudent(student *Student)

参数名即函数的处理对象,因此函数名中不需要再次说明:func handle(student *Student)

其三,**作用域越小,名字可以越短。**最常见的便是迭代变量、小函数局部变量。因为作用域越小,其冲突的可能性就越小。

// case 1: iteration
for i, s := range students {
 fmt.Println(i, s)
}
// case 2: small function
sort.Slice(students, func(i, j int) bool { return students[i].Name < students[j].Name })

但简洁是有度的,以不引入二义性为限。仍以上面 Student 为例:

package student
type Student struct {}
type StudentManager student{}
func New() (*StudentManager, error)

此间的 New 函数就有点太短,因为在调用时 student.New() 很容易引起误解,以为返回的是 Student 类型,因此最好改成 student.NewManager()

系统

最后,但也是最重要的,命名是需要成体系的。而只有对所解决的问题有了足够的认识,才能做出足够贴切的抽象,写出足够简明扼要的代码,命出简短、对称、一致的名字。仍然借用数据结构来概括下命名系统组织原则:宏观角度看,代码是分层的树形组织;微观角度看,层与层之间是类二分图组织。

下面来从几个侧面举几个例子。

其一,一致性、相容性。在自己设计代码时,表现为多个组件间风格的一致性;在修改别人代码时,表现为延续其风格的相容性。

// student.go
func get(name string) (*Student, error)
func process(student *Student) error
// teacher.go
func fetch(name string) (*Teacher, error)
func handle(teacher *Teacher) error

上面例子便是一个反面,同样的意思,用了不同名字。更恶劣的是,相似的地方、相似的名字,却指代不同的含义。这会给维护者带来极大的心智负担。

其二,原子性、正交性。单个函数尽量短小,函数名才能完整体现代码意思;基础函数尽量正交,才能去除冗余,通过组合来表达强大的生命力。

一个常见的例子,是 WebService 中围绕某种实体的 CRUD,如针对 StudentStudentManager 。上层便可以利用这些基本的增删改查完成更细节的业务逻辑。

type StudentManager struct {}
func(m *StudentManager) Create(id, name string, age int)
func(m *StudentManager) Remove(id string)
func(m *StudentManager) Update(s *Student)
func(m *StudentManager) Delete(id string)

其三,体系性。使用某种手段,将系统拆解为几个很自然的模块。这种自然,本质上是通过利用你和读者共享的上下文来做到的。

如需要适配多种存储后端时,关于 Storage 的抽象。

type Storage interface {
 func Create(uri string) (*File, error)
 func Remove(uri string) error
 func Open(uri string, mode int)(io.ReaderWriter, error)
}

如需要管理任务生命周期时 ,关于  Task 的抽象。

type Task interface {
 func Start() error
 func Stop() error
 func Suspend() error
 func Resume() error
 func IsRunning() bool
 func IsSuspending() bool
}

其惯常的做法是,参考数据结构、操作系统、数据库、网络中一些常见的抽象,比如消息队列、文件系统、进程线程、传输协议等等,做适当变化来为我们服务。大多数程序员都共享这些基本概念,因此可以很快速的理清你代码的脉络。

另一种常用的方式,是借鉴日常生活中大家都熟悉的概念,围绕其性状、时空等特性,对系统进行拆解。比如之前呆过的一个公司,利用鹰(Eagle),鹰眼(监控)、鹰爪(爬取)等概念来拆解一个监控系统。

小结

古人写文章,讲究反复推敲,方能有佳作。代码命名,也需要仔细锤炼,才能不断延长生命周期,免于过快腐烂。如果仅仅追求写代码快,第一反应是什么,就用什么做名字,代码便难逃运行一次就被重构甚至遗弃的命运。

见识所囿,必有诸多遗漏。关于代码命名,大家还有什么心得或吐槽,欢迎留言讨论。

相关文章
|
Go
说到内嵌命名冲突,他直接把我虐哭了......
说到内嵌命名冲突,他直接把我虐哭了......
109 0
|
3月前
|
敏捷开发 设计模式 C语言
软件工程师,要么不写代码,要么就写优雅的代码
软件工程师,要么不写代码,要么就写优雅的代码
40 7
|
7月前
|
算法 Java 数据安全/隐私保护
【Java开发指南 | 第二篇】标识符、Java关键字及注释
【Java开发指南 | 第二篇】标识符、Java关键字及注释
38 5
|
6月前
|
Web App开发 前端开发 定位技术
前端命名规范以及常用命名整理
这是一份关于HTML和CSS编码规范的摘要: - 文件编码统一使用UTF-8。 - 命名遵循语义化,CSS属性书写规范,推荐使用中线命名法(如`hello-world`),避免下划线和驼峰命名。 - 样式应复用,模块化,便于移植。 - 避免使用CSS Hack,优先考虑浏览器兼容性。 - 针对Firefox设计,用IE条件注释做修正。 - 使用英文命名,避免拼音,少用缩写,不以数字开头。 - 常见命名包括页面结构(如`container`、`header`)、导航(`nav`、`subnav`)、功能区域(`logo`、`search`)等,提供了一套常见的ID和Class命名约定。
|
7月前
|
设计模式 算法 Java
|
前端开发 程序员 PHP
程序员还在为变量取名苦恼,那是因为你不知道,这个变量命名神器
程序员还在为变量取名苦恼,那是因为你不知道,这个变量命名神器
204 0
|
前端开发 JavaScript 算法
你真的会代码命名吗 ? 优雅学会《如来神掌》 再也不怕不知道如何命名啦!
你真的会代码命名吗 ? 优雅学会《如来神掌》 再也不怕不知道如何命名啦!
249 0
你真的会代码命名吗 ? 优雅学会《如来神掌》 再也不怕不知道如何命名啦!
|
缓存 Java fastjson
Java开发都需要参考的一份命名规范
好的命名能体现出代码的特征,含义或者是用途,让阅读者可以根据名称的含义快速厘清程序的脉络。不同语言中采用的命名形式大相径庭,Java中常用到的命名形式共有三种,既首字母大写的UpperCamelCase,首字母小写的lowerCamelCase以及全部大写的并用下划线分割单词的UPPERCAMELUNSER_SCORE。通常约定,类一般采用大驼峰命名,方法和局部变量使用小驼峰命名,而大写下划线命名通常是常量和枚举中使用。
447 0
Java开发都需要参考的一份命名规范
|
存储 Java
来自三段代码的疑惑~
来自三段代码的疑惑~
119 0
|
前端开发
前端工作总结264-命名报错
前端工作总结264-命名报错
65 0

热门文章

最新文章