一、整洁代码
糟糕的代码可被形容为沼泽
稍后等于永不——LeBlanc法则
什么是整洁代码?
童子军规——让营地比你来时更干净
二、有意义的命名
-
名副其实
字段含义不需要注释来补充 eg.(可以用函数遮盖魔术数
-
避免误导
避免使用专有名词、相似外形的词等
-
有意义的区分
一般情况下Product、ProductInfo、ProductData没区别
-
使用读得出来的名称,而非自造词
-
使用可搜索的名称
WORK_DAYS_OF_WEEK 而非 5
-
避免无意义的编码
类或成员前缀、接口实现等
-
避免思维映射——方案术语
-
类名是名词或名词短语
避免manager、Processor、Data、info等
-
方法名应是动词或动词短语
-
每个概念对应每个词
controller和manager混用会使人疑惑
-
别用双关语
add 可以是insert,也可以是append
-
使用解决方案领域名称
-
使用源自所涉问题领域的名称
-
添加有意义的语境
addrFirstName、addrLastName即使单独出现也能理解意思
-
不添加没用的语境
三、函数
-
短小
函数的缩进层级不应该多于一层或两层
-
只做一件事
如果函数只做了该函数名下同一抽象层级上的步骤,还是只做了一件事。
判断:看看是否能再拆出一个函数
-
每个函数一个抽象层级
函数中语句都要处于同一抽象层级上
自顶向下读代码:向下规则
-
switch语句
埋在抽象工厂下或者隐藏于某种继承关系中
-
使用描述性名称
-
函数参数
尽量避免三个以上参数
-
无副作用
如果函数必须修改某种状态,那就修改所属对象的状态
-
分割指令和询问
函数要么做什么事、要么回答生命事,二者不可兼得
-
使用异常替代返回错误码
异常处理能使处理错误的代码逻辑从主要代码中分离出来
- 抽离try/catch语句
- 错误处理就是一件事——处理错误的函数不应该做其他事情
- Error依赖磁铁
-
重构重复
-
结构化编程
只要函数保持短小,偶尔出现的return、break、continue没有什么坏处,反而更有表达力
四、注释
别给糟糕的代码加注释——重新写吧!
注释总是一种失败,代码在变动,在演化,注释并不总是随之变动。不准确的注释比没注释坏的多。
-
注释用代码来阐述
只需要创建一个描述与注释所言相同的函数即可
-
好的注释
- 法律信息
- 提供信息的注释——解释了某个抽象方法的返回值
- 对意图的解释
- 警示
- TODO
- 公共API中的Javadoc
-
坏的注释
- 呐呐自语
- 多余的注释
- 误导性注释
- 循规式注释
五、格式
源文件应向报纸文章那样,名称一目了然,多数短小精悍。
-
概念间垂直方向上区隔
-
垂直方将上靠近
紧密相关的代码应该相互靠近
-
变量的垂直距离应该尽可能靠近使用位置
-
横向格式:100~120左右
六、对象和数据结构
-
数据抽象
数据抽象不仅仅是多一些接口、取值/赋值方法,而是要以最好的方式来呈现某个对象包含的数据,需要严肃的思考。
-
对象与数据结构的二分原理
过程式代码便于在不改动既有数据结构的前提下添加新函数;
面向对象代码便于在不改动既有函数的前提下添加新的类;
-
Demeter 定律
模块不应该了解它所操作对象的内部情况——方法不应调用任何好书返回的对象的方法
不应链式调用
隐藏其内部结构:防止当前函数因浏览它不该知道的对象而违反了Demeter定律
-
数据传送对象DTO
只有公共变量,没有函数的类
对象暴露行为,隐藏数据;数据结构暴露数据,隐藏行为。
七、错误处理
-
使用异常而非返回码
-
先写try-catch-finally语句
异常在程序中定义了一个范围,try像一个事务,无论try中发生了什么,catch代码都将程序维持在一种持续的状态
-
使用不可控异常
可控异常的代价就是违反开闭原则,必须在catch语句和判处异常间的每个方法签名中声明该异常。(如果在编写一套关键代码库,可控异常会很有用),对于一般应用来说,依赖成本高于收益
-
给异常发生的环境说明
应创建详细的错误消息,并随异常一起传递出去
-
依调用者的需要定义异常类
在定义异常类的时候考虑应如何捕获异常
有必要的时候可以对第三方的API进行打包,降低对其的依赖
-
定义常规流程
特例模式:创建一个类或配置一个对象,用来处理特例。异常行为被封装到特例对象中
-
别返回null
如果调用第三方API中可能返回null值的方法,可以考虑重新打包,在新方法中抛出异常或者返回特例对象
-
避免传递null值
创建异常或者使用断言
如果将错误处理隔离看待,独立于主逻辑之外,就能大大提升代码的可维护性。
八、边界
-
使用第三方代码
不要将Map在系统中传递,如果使用Map这样的边界接口,就把它保留在类或近亲类中。避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API
-
学习性测试?的好处不只是免费
-
使用尚不存在的代码
将未知包括起来,定义自己使用的接口,这样一旦未知接口被定义出来,就使用自己的接口来封装API,同时当API发生变动的情况时,封装就是唯一需要修改的地方
-
整洁的边界
边界代码需要清晰的分割和定义期望的测试,并且避免我们的代码过多的了解第三方代码中的特定信息,依靠你能控制的东西。
通过代码中少数引用第三方边界接口的位置来管理第三方边界
九、单元测试
-
TDD三定律
- 在编写不能通过的单元测试之前,不可编写生产代码
- 只可编写刚好无法通过的单元测试,不能编译也算不过
- 只可编写刚好足以通过当前失败测试的生产代码
-
保持整洁的测试
测试代码和生产代码一样重要,需要被思考,被设计
-
整洁的测试:可读性、可读性、可读性
需要明确、简洁、足够的表达力
测试框架提供的测试形式——构造-操作-检验(BUILD-OPERATE-CHECK)模式
- 构建测试数据
- 操作或处理测试数据
- 校验真实代码的输出结果是否与预期的一致
-
每个测试一个断言
-
FIRST准则
- Fast 快速
- Independent 独立
- Repeatable 可重复
- Self-Validating 自我验证
- Timely 及时
十、类
-
类的组织
公共静态变量——私有静态变量——私有实体变量——公共函数
封装:保持变量和工具函数的私有性,但并不执着于此
-
类应该短小
对于函数,通过计算代码行数衡量大小
对于类,通过计算权责
-
单一权责原则:类和模块应有且只有一条加以修改的理由
-
内聚:方法操作的变量越多,就越黏聚到类上
保持内聚性能得到很多短小的类
-
-
为了修改而设计
隔离修改:用接口和抽象类来隔离具体类的细节部分
依赖倒置DI原则认为:类应当依赖于抽象而不是具体细节
十一、系统
-
将系统应用与使用分开
软件系统应将启动过程与之后的运行逻辑分开,在启动过程中构建应用对象,也会存在相互缠结的依赖关系
比如对象构造的代码:应从正常运行的逻辑中分离出来,确保有解决主要依赖问题的全局性一致策略。
全局策略如果在程序中四散分布,缺乏模块组织性,通常会有许多重复代码
-
分解main:将全部构造过程搬到main或者main模块中
-
工厂:构造能力由工厂持有,而工厂又在main这边
-
依赖注入:DI和IoC IoC将第二权责从对象中拿出,转移到另一专注于此的对象中,从而遵循了单一权责原则
延迟初始化:多数DI容器在需要对象前不构造对象、许多DI容器提供调用工厂或构造代理的机制,这种机制可为延迟赋值或类似的优化处理所用
-
-
扩容
迭代/敏捷:应只去实现今天的用户故事,然后重构,明天再扩展系统,实现新的用户故事。
-
切面编程
- Java代理
- Java AOP框架
- AspectJ
-
测试驱动系统架构?
最佳的系统架构由模块化的切面领域该组成,每个切面均用纯Java对象实现。不同领域之间用最小侵害的切面或类切面工具组合起来,这种构架能测试驱动,就像代码一样。
-
优化决策
延迟决策:拥有模块化切面的POJO系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策
十二、迭进
-
运行所有测试
测试消除了对清理代码会破坏代码的恐惧
遵循有关编写测试并持续运行测试的简单、明确的规则,系统就会更贴近OO低耦合度、高内聚度的目标。
-
不可重复
小规模复用可大量降低系统复杂性
模板方法模式是一种移除高层级重复的通用技巧
-
表达力
-
尽可能短小