命名
- 类名:名词或名词短语;避免使用Manager、Processor、Data、Info之类的类名
- 方法名:动词或动词短语
- 使用解决方案领域名称,比如:AccountVisitor(访问者模式)
- 使用所涉问题领域名称
- 富有语境的名称,比如state单独拎出来不知道是什么意思,而AddrState就代表了地址的状态
- 不要添加无用的语境,比如一个固定的前缀或者后缀
函数
短小
只做一件事
每个函数一个抽象层次
自顶向下,让每个函数后面都跟着下一抽象层次的函数
UML时序图能很好体现抽象层次,按照先后顺序,从上到下层层组装
单一职责原则:函数只能做一件事情,那么这个函数就不能再拆分出此函数层次上的新函数
单一抽象层次原则:函数中的语句应该是同一个抽象层次上的步骤组合,也就符合单一职责原则
switch语句
违反单一职责原则:根据值来做不同的事情
违反开闭原则:当新增一个类型时,必须修改原有函数
解决方案:放在抽象工厂方法下面,用多态解决。这样,这个函数就是只做了一件事情:根据类型创建对象,符合单一职责原则;并且新增一个类型不同改动到原来的类的方法,新增一个子类即可
使用描述性的名称
能较好的描述函数做的事情
使用与模块名一脉相承的短语、动词和名称给函数命名
函数参数
最理想的参数数量为0,其次是1,再次是2,应尽量避免3
参数和函数名不在同一个抽象层次上,不容易理解
参数越多,测试越难
输出参数比输入参数更难理解:不要传入一个参数,然后去改变这个参数的值,通过返回值去输出
一元函数:一种较好的方式是函数里面输入一个事件,那这个函数要做什么事情就一目了然
标识参数:True/False,明显违反了单一职责原则
参数对象:当参数大于等于3个时,就应该封装为对象
动词与关键词:函数和参数应该形成良好的名称/动词对形式,比如set(name) 、setField(name),就能很清晰的说明给name属性设置值
无副作用:大部分函数都达不到
分隔指令和查询:要么修改对象的状态,要么查询对象信息(CQRS也是分隔指令和查询,但目的应该不大一样)
使用异常代替错误码:错误处理代码能从主路径中分离出来
抽离try/catch:错误处理就是一件事
别重复自己
如何写出这样的函数:画时序图
注释
- 需要解释的才需要注释,从语境、名称及抽象层次能一眼看懂的,就不需要
- 函数名如果高中生看不懂,就需要注释下了
格式
- 团队统一一个代码格式:比如谷歌代码格式
对象和数据结构
数据抽象:接口呈现了数据结构,并且固定了一套存取策略
数据、对象的反对称性
数据结构:暴露其数据,没有提供有意义的函数
过程式编码:便于在不改动数据结构的情况下新增新函数;但是一旦数据结构有变动,所有的函数都得改变
对象:将对象隐藏于抽象之后,暴露操作数据的函数
面向对象编号:便于在不改动既有函数的情况下添加新类;同样不利于添加新函数,因为必须改动所有类
得墨忒耳律:只跟朋友谈话,不跟陌生人讲话
类C的方法f只应该调用以下对象的方法:
C
由f创建的对象
作为参数传递给f的对象
C的实体变量持有的对象
火车失事:ctx.options.strachDir.absolutePath(与陌生人进行了对话)
混杂:一半是对象,一半是数据结构
隐藏结构:ctx.createScratchFileStream(classFileName)
数据传输对象:只有数据,没有函数。如果需要函数,应该再创建一个实体类
错误处理
- 使用异常而非返回码;算法和错误处理被隔离,错误处理在catch中得到解决
- 使用不可控异常
- 给出异常发生的环境说明
- 定义异常类
- 定义常规流程:特例模式,创建一个类和配置一个对象来处理特例
- 别返回null,返回空对象,比如空字符串、空列表
- 别传递null值
边界
使用第三方代码:避免直接使用(比如map),应该封装Map的使用,把它保留在类中(场景极少,无需如此)
浏览和学习边界:不要在生产代码中试验新东西
log4j
学习性测试的好处,不只是免费:编写第三方包的测试代码,帮助增进对api的理解
使用尚不存在的代码:比如要对接第三方通信系统,但是接口未给出,我们可以自己先定义自己的Adapter
整洁的边界
通过代码中少数引用第三方边界接口的位置来管理第三方边界
如map般包装他们,或者用Adapter模式将我们的接口转换为第三方接口
边界内部一致
单元测试
TDD三定律:限定大概在30秒的循环内,生产代码和测试代码一起编写
在编写不能通过的单元测试前,不能编写生产代码
只可编写刚好无法通过的单元测试,不能编译也算不过
只可编写刚好足以通过当前失败测试的代码
保持测试整洁:请重视生产代码一样,重视测试代码
整洁测试三要素:可读性、可读性、可读性
明确、简洁,有足够表达力
构造-操作-检验
每个测试一个断言
每个测试只测试一个概念;不要超长的测试函数,测试完这个再测试那个
F.I.R.S.T
快速:测试够快,且应该尽快运行
独立:测试相互独立
可重复:可以再任何环境中重复通过
自足验证:测试应该有布尔值输出
及时:测试应及时编写
类
类的组织
遵守Java约定
从一组变量列表先开始
从上到下:公共静态常量->私有静态变量->私有实体变量
很少会有公共变量(就不应该有,如果有则应该定义在Contants类中)
封装
尽量保持变量和函数的私有性
如果测试需要调用到,测试说了算
类应该短小
类的代码行数不能过多
无法给某个类以精准的命名,那么这个类就应该太长了
单一权职原则
类和模块应有且只有一条加以修改的理由
系统应该由许多短小的类组成,而不是少量巨大的类
内聚
类应该只有少量实体变量;类中的每个方法都应该操作一个或多个这种变量
方法操作的变量越多,越内聚
内聚高,意味着类中的方法和变量互相依赖,形成一个逻辑整体
保持内聚性,就会得到许多短小的类
为了修改而组织
在写代码的时候,就应该考虑到代码会修改,这时候应该为了符合开闭原则而组织代码
我们系统通过扩展系统而非修改现有代码来添加新特性
隔离代码(解耦,符合依赖倒置原则)
抽象类呈现概念,具体类包含细节。如果依赖具体类,当细节改变时,就会有风险(构造器、具体算法、实现逻辑改变)
系统
如何建造一个城市
系统层面保持整洁
抽象等级和模块
将系统的构造和使用分开(依赖注入)
延迟初始化与之相反:比如方法中调用new ServiceImp()来实例化对象
扩容
因需求而导致系统递增式的增长
持续的将关注点切分
横贯式关注面
事务、持久化、安全等
用AOP处理,也可以用模块、封装处理
测试驱动系统架构
语意不详,后续阅读测试驱动开发、有效的单元测试
优化决策
模块化和关注点切分->分散化管理和决策
模块化是将关注点集中管理,感觉微服务和DDD中的领域(核心域、支撑域等)是在模块化的基础上对业务的进一步分离关注
系统需要领域特定语言(DSL)
填平了问题域和实现域的壕沟
迭进
简单设计4条规则
运行所有测试:反向驱动设计
驱动:类短小且单一职责
驱动:遵循依赖倒置原则
促进:低耦合、高内聚
不可重复
表达了程序员的意图
尽可能减少类和方法的数量
抵制教条:比如字段和行为切分到数据类和行为
并发编程
并发防御原则
单一权职原则
并发代码有自己的开发、修改和生命周期
建议:分离并发相关代码和其他代码
限制数据作用域
锁:synchronized,Lock
并发更新共享数据的地方越多,越容易出错
使用数据副本
从多线程中收集所有副本的结果,单线程中合并这些结果
线程尽可能独立
不与其他线程共享数据;每个线程处理客户端的一个请求,从不共享的源头接纳请求数据,存储为本地变量(个人理解是数据作为入参传递给run方法)
尝试将数据分解到可被独立线程操作的独立子集
JDK
java.util.conurrent
executor
尽可能使用非锁解决方案
了解执行模型(不部分并发问题都在一下三种模型中的一个)
生产者-消费者模型
生产者线程某些创建工作,并置于缓存或队列中;消费者线程从缓存或队列中获取数据并完成工作
读者-作者模型
存在一个主要为读者线程提供信息源,偶尔被作者线程更新的共享资源
作者线程的更新操作,会锁住很多读者线程(共享资源被作者线程独占)
读写锁专门解决这个问题
宴席哲学家
信号量可以协调共享资源(每个共享资源的许可数量不一样,就能去协调资源)
警惕同步方法之间的依赖
建议:避免使用一个共享对象的多个方法
如果必须使用一个对象的多个方法
基于客户端锁定:客户端代码再调用第一个方法前锁定服务端
基于服务段锁定:在服务端内创建锁定服务端的方法
适配服务端:创建执行锁定的中间层
保持同步区域微小
很难编写正确的关闭代码
比如父线程等待所有子线程执行结束,然后释放资源并关闭;但是如果子线程死锁,资源就无法被释放
建议:尽早考虑关闭情况
测试线程代码
将伪失败看做是线程问题(伪失败=偶现问题)
先使非线程代码可工作
建议:不同同时追踪线程问题和非线程问题
编写可插拔的线程代码
用运行快速、缓慢和有变动的测试替身执行
将测试配置为能运行一定数量的迭代
编写可调整的线程代码
要允许线程数量可调节
运行多于处理器数量的线程
在不同平台运行
装置试错代码(增加对wait、sleep、yield、priority等方法的调用,改变代码执行顺序)
硬编码:手工向代码中插入sleep、wait、yield、prioty的调用
自动化:使用CGLIB、ASM等来装置代码