1.有意义的命名
1.1名副其实
1.命名需要名副其实,比如我们声明一个天数变量,不应该写成int d;
,而是应更具体的表示,如int daySinceCreation;
。
2.对于一个对象的多个标记,我们可能会用int表示,比如0代表xx,1代表xx。更好的方法是比如写一个数据类,对标记进行单独声明,可读性会大大提升。
3.对于for
循环中使用的变量,也应该有明确的命名,而不是使用tempData
等随意的命名。
4.常用常改,使用更贴切的命名。
1.2避免误导
尽量避免使用和系统自带名称相近的命名,或者几个命名差异较小一眼看不出区别。
1.3做有意义的区分
1.ProductInfo
和ProductInfo
就看起来没什么区别。getAccount()
和getAccounts()
也难以分辨。尽量用更清晰的命名。还有就是省掉废话,如nameString
中的String
就没什么意义。
2.尽量少用晦涩的单词或者凭空自造的名称。
1.4使用可搜索的名称
尽量不要用比如e
、t
等做名称,搜索起来一大堆难以分辨。
1.5避免使用编码
作者说不喜欢使用前缀,如成员变量使用m_
前缀,接口写成IShape
等,觉得前缀是废话。这个因人而异吧。
1.6避免映射思维
for
循环里的计数传统惯用i、j(别用l,和1不好区分),但是单字母不是很好的做法。
1.7类名
类名应是名词或者名词短语,不要用动词。
1.8方法名
方法名应是动词或动词短语,比如set、get、is开头。
1.9别扮可爱
不要用梗或者俗语命名。
1.10每个概念对应一个词
命名应一以贯之,比如同一堆代码不要一会命名manager,一会又命名controller。
1.11别用双关语
遵循“一词一意”规则。
比如Add、Append、Insert虽然相近但代表不同的功能,不要都写成Add。
1.12使用解决方案领域名称
可以用术语、算法名、模式名等作为或组成命名。
1.13使用源自所涉问题领域的名称
如果不能用术语命名,也可以采用所涉问题领域命名,这样也方便维护者理解或搜索资料。
1.14添加有意义的语境
给读者提供语境。
比如一个state信息里有name、street、city、houseNumber,我们看下来可以推测这是一个地址。更好的做法是添加前缀,如name改为addrName,或者创建名为Address的类。
过长的函数且贯穿始终的变量也会使语境不清晰,建议将函数拆分,并将拆分出的函数使用符合语境的命名。
1.15不要添加没用的语境
只要短名称足够清晰就比长名好。添加不必要语境会导致难以区分和搜索。
总结
不要偷懒,及时重构。命名要清晰准确。对于数字的判断和Switch应写成枚举,能抽象成数据类的就用类。
2.函数
函数要短小。
if、while等其中的代码块应该只有一行,要么是逻辑要么是函数调用。
一个函数只做一件事。存在多个功能就考虑抽离了。
自顶向下读代码:向下规则。逐级抽离函数,比如底层实现逻辑就应跟函数调用逻辑分开。底层实现的函数层级在调用函数层级的下面。
switch应该埋在较低的抽象层级,而且永远不重复。可以考虑用工厂模式+多态类。
使用描述性的函数名。
函数参数越少越好,如果超过3个考虑使用数据类或者参数列表表示。
命名用动词开头,最好能表明参数。
函数名要符合实际逻辑,只做一件事,不要暗藏玄机。比如负责查询的模块就不要内含修改功能。
可以用Try/Catch代替错误码。错误码是“依赖磁铁”,多处都要导入和使用它,牵一发动全身。
对于Try/Catch内的代码块抽离成函数,避免混乱。
避免重复逻辑!
尽量避免使用goto。
高质量代码很难一蹴而就,第一遍写完相当于草稿,及时按照规则重构和打磨
3.注释
作者的观点是逻辑清晰、命名规范的高质量代码是不需要注释的。但是个人觉得国内程序员习惯不同,英文水平也参差不齐,对于容易出现歧义、公共接口或者稍复杂的代码还是需要有注释。
注释要做到简明扼要,一目了然,切忌废话连篇、喃喃自语或是误导他人。注释一般有提供信息、警告、法律信息、解释意图等功能。
日志式的注释已经过时,现在有完善的版本管理系统。
未做完或者需要完善的功能可以用TODO注释标记。
注释也有缺点,比如久远的注释可能在代码多次更迭后已经偏离实际代码的含义,亦或者繁杂的注释可能会增加理解成本。
作者一直在反复强调规范、高质量代码的重要性,毕竟注释可能会骗人,但是代码不会。
4.格式
关于格式细节现在的IDE都支持一键排版,不需要耗费太多精力。比如VS的快捷键就为CTRL+K
然后CTRL+F
。个人喜欢记成KFC(肯德基)方便记忆,K+F是排版,K+C是注释。
一行代码不要太长,比如需要滑动条才能看全的就是错误示例。
实体变量放到类的顶部。
概念相关的代码放到一起。
贯彻自顶向下的逻辑顺序,调用函数在被调用函数的上面。
5.对象和数据结构
过程式代码(使用数据结构的代码):便于在不改动既有数据结构的前提下添加新函数。(类似工厂设计模式,针对不同需求在函数中做处理)
面向对象代码:便于在不改动既有函数的前提下添加新类。(比如多态)
如何选择根据自己的具体需求。
得墨忒耳定律:软件的一个单元应该仅与其直接合作者进行交互。具体来说,如果一个对象A引用了对象B,而对象B又引用了对象C,那么对象A可以直接调用对象B的方法,但不应直接调用对象C的方法。
对象数据隐藏,操作暴露。避免代码“火车失事”(如A().B().C()
)。
6.异常
使用异常而非返回码。
1.可控异常(Checked Exception)
可控异常是在编译时就能被检测到的异常。这些异常通常是由于外部因素引起的,例如文件读取错误、数据库访问错误等。程序员必须在代码中显式地捕获和处理这些异常,否则程序将无法编译通过。常见的可控异常包括:
IOException:当发生 I/O 操作错误时抛出。
SQLException:当数据库访问错误时抛出。
ClassNotFoundException:当试图加载的类不存在时抛出。
2.不可控异常(Unchecked Exception)
不可控异常是在运行时才会被检测到的异常。这些异常通常是由于编程错误引起的,例如数组越界、空指针引用等。程序员可以选择捕获和处理这些异常,但这不是强制性的。常见的不可控异常包括:
NullPointerException:当应用程序试图在需要对象的地方使用 null 时抛出。
IndexOutOfBoundsException:当数组或集合的索引超出范围时抛出。
ArithmeticException:当出现算术运算错误时抛出,例如除以零。
使用不可控异常。
给出异常发生的环境说明。
根据需求自定义异常类。
别返回空值,也尽量不要传递null。会导致其他位置需要判空,白白增加工作量。可以用特定值或者默认值代替。
7.边界(第三方代码)
对于第三方代码中包含不需要使用的功能,我们可以通过封装类来避免。比如我们使用List
但是又不想暴露出它的Clear接口,就可以将List
封装在类中,将需要使用的接口封装成函数。
使用第三方或者是自己或团队不可控代码时,可以把这些代码打包封装起来。
对于第三方代码可以通过编写测试来检测功能。
对于暂时缺少的接口我们可以根据需求先模拟写出符合需求的接口,等后续再接入即可。
对于不同的第三方代码接口,可能不是完全满足我们的需求,这时我们可以通过Adapter模式(适配器模式)转换接口。
8.单元测试
TDD三定律:
1.在编写不能通过的单元测试前,不可编写生产代码。 这意味着在开始编写任何实际的功能代码之前,必须先编写相应的单元测试。
2.只可编写刚好无法通过的单元测试,不能编译也算不通过。 这表明编写的单元测试应该是具有挑战性的,但不应过于复杂以至于无法通过。如果测试代码无法编译或运行,那么它就不算是一个有效的测试。
3.只可编写刚好足以通过当前失败测试的生产代码。 一旦单元测试失败,应该只编写足够通过当前测试的生产代码,而不是编写过多的代码以满足未来的需求或预期。这样做可以确保代码的简洁性和可维护性。
9.类
类的构成顺序应为静态、公有、私有、常量、变量、函数为组合权重的顺序自上而下。
注意类中成员的私有性,但不执着于此。
类命名有助于权责划分。一个含糊的命名大多是要包含许多成员。
单一职责原则(SRP)。类和模块有且只有一个修改的理由。只专注于解决一个问题。
系统应由许多短小的类而不是少量巨大的类组成。每个类只有一个修改的理由。并与少数其它类一起协同达成期望的系统组成。
高内聚。如果类中的每个变量都被每个方法所使用,那么该类具有最大的内聚性。
保持内聚性有利于类的拆分。
遵循开放闭合原则(OCP),对扩展开放,对修改关闭。如果需要加功能可以使用扩展方法或者子类继承的方式。
隔离修改,对于需求的修改,我们应遵循依赖倒置原则(DIP),可以通过实现接口来规避频繁的修改,去调用接口就好。
10.系统
构造函数中不应实现具体逻辑。
简单工厂模式(工厂类中根据条件返回对应实例)。工厂方法模式(实现创建接口,子类继承并自定义返回实例)。抽象工厂模式(创建集合了返回不同实例函数的接口类,然后工厂类继承该接口并根据条件返回自定实例)。
依赖注入(DI)和控制反转(IOC)
基本是一个意思,因为说起来谁都离不开谁。简单来说,a依赖b,但a不控制b的创建和销毁,仅使用b,那么b的控制权交给a之外处理,这叫控制反转(IOC),而a要依赖b,必然要使用b的instance,那么通过
1.a的接口,把b传入;
2.通过a的构造,把b传入;
3.通过设置a的属性,把b传入;
这个过程叫依赖注入(DI)。那么什么是IOC Container?随着DI的频繁使用,要实现IOC,会有很多重复代码,甚至随着技术的发展,有更多新的实现方法和方案,那么有人就把这些实现IOC的代码打包成组件或框架,来避免人们重复造轮子。所以实现IOC的组件或者框架,我们可以叫它IOC Container。
系统也要追求整洁。如果可以应选择最简单明了的实现方式。
11.迭进
简单设计的四条原则(重要程度按先后顺序排列):
1.能够运行所有测试
2.不可重复
3.表达了程序员的意图
4.尽可能减少类和方法的数量
2-4条其实是在强调重构的重要性。
测试消除了清理代码就会破坏代码的恐惧。
可以用模板方法模式减少重复(将通用的算法步骤放在抽象类中,子类去实现具体的逻辑)。
12.并发
1.并发相关代码有自己的开发、修改和调优生命周期;
2.开发相关代码有自己要对付的挑战,和非并发相关代码不同,而且往往更为困难;
3.即便没有周边应用程序增加的负担,写得不好的并发代码可能的出错方式数量也已经足具挑战性。
建议:分离并发相关代码与其他代码。
尽量避免多个线程处理同一份数据。
建议:尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集。
生产者-消费者模式(Producer-Consumer Pattern)是一种经典的并发编程模式,用于解决生产者和消费者之间的数据交换与同步问题。
生产者生成数据:生产者线程生成数据并将其放入缓冲区。
缓冲区存储数据:缓冲区充当中介,存储生产者生成的数据。
消费者处理数据:消费者线程从缓冲区中取出数据并进行处理。
读者-作者模式(Reader-Writer Pattern)是一种常见的并发控制模式,用于解决多个线程同时访问共享资源时的同步问题。它特别适用于读操作频繁而写操作较少的场景。
读者优先:如果有读者在读取资源,新的读者可以继续读取,但作者必须等待。
作者优先:如果有作者在写入资源,新的作者可以继续写入,但读者必须等待。
公平性:确保读者和作者都能公平地访问资源,避免某一方长时间等待。
注意死锁、活锁、吞吐量和效率降低等问题。
建议:避免使用一个共享对象的多个方法。
尽可能减小同步区域。
处理好关闭问题,防止理应结束的线程一直运行无法关闭。
进行全面的测试,包括不同设备、强迫错误发生等测试。测试前对非线程代码和线程代码的错误做好确认。
偶现的BUG也应该注意。
13.逐步改进
在我们实现一个新的功能模块时,往往初版还是相对清晰的,在这基础上简单重构下,比如减少变量、修改命名、抽出重复、组合/拆解方法等,就能得到整洁的代码,这是因为我们在写新功能时肯定要思考好功能模块和架构。但是如果后续有修改或者扩展需求,我们常常为了方便会选择在这基础上打补丁,尽量不动原来的逻辑,长此以往这么做的话这份代码就会变成“屎山”,维护成本也会越来越高。
为了避免这个问题,当我们有改动需求的时候首先应该想到重构,三思而后行,而不是单单为了满足需求。利用书中提到的方法去及时重构代码,不要嫌麻烦和浪费时间,这其实是在保护我们的头发,写过或者遇到过“屎山”代码的朋友应该深有体会。