目录
- 为什么要重构
- 什么是重构
- 坏代码的问题
- 什么是好代码
- SOLID 原则
- 单一职责原则
- 开放-关闭原则
- 里氏替换原则
- 接口隔离原则
- 依赖反转原则
- 迪米特法则
- 合成复用原则
- 设计模式
- 代码分层
- 命名规范
- 约定俗称的惯例
- 类命名
- 方法命名
- 重构技巧
- 提炼方法
- 以函数对象取代函数
- 引入参数对象
- 移除对参数的赋值
- 将查询与修改分离
- 移除不必要临时变量
- 引入解释性变量
- 使用卫语句替代嵌套条件判断
- 使用多态替代条件判断断
- 使用异常替代返回错误码
- 引入断言
- 引入 Null 对象或特殊对象
- 提炼类
- 组合优先于继承
- 接口优于抽象类
- 优先考虑泛型
- 静态成员类优于非静态成员类
- 优先使用模板/工具类
- 分离对象的创建与使用
- 可访问性最小化
- 可变性最小化
- 测试驱动开发
- TDD 的开发周期
- 两个基本的原则
- 分层测试点
关于重构
为什么要重构
项目在不断演进过程中,代码不停地在堆砌。如果没有人为代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。
造成这样的原因往往有以下几点:
- 编码之前缺乏有效的设计
- 成本上的考虑,在原功能堆砌式编程
- 缺乏有效代码质量监督机制
对于此类问题,业界已有有很好的解决思路:通过持续不断的重构将代码中的“坏味道”清除掉。
什么是重构
重构一书的作者 Martin Fowler 对重构的定义:
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
根据重构的规模可以大致分为大型重构和小型重构:
大型重构:对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。
小型重构:对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名和注释、消除超大类或函数、提取重复代码等等。小型重构更多的是使用统一的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小。 什么时候重构 新功能开发、修 bug 或者代码 review 中出现“代码坏味道”,我们就应该及时进行重构。持续在日常开发中进行小重构,能够降低重构和测试的成本。
代码的坏味道
- 代码重复
- 实现逻辑相同、执行流程相同
- 方法过长
- 方法中的语句不在同一个抽象层级
- 逻辑难以理解,需要大量的注释
- 面向过程编程而非面向对象
- 过大的类
- 类做了太多的事情
- 包含过多的实例变量和方法
- 类的命名不足以描述所做的事情
- 逻辑分散
- 发散式变化:某个类经常因为不同的原因在不同的方向上发生变化
- 散弹式修改:发生某种变化时,需要在多个类中做修改
- 严重的情结依恋
- 某个类的方法过多的使用其他类的成员
- 数据泥团/基本类型偏执
- 两个类、方法签名中包含相同的字段或参数
- 应该使用类但使用基本类型,比如表示数值与币种的 Money 类、起始值与结束值的 Range 类
- 不合理的继承体系
- 继承打破了封装性,子类依赖其父类中特定功能的实现细节
- 子类必须跟着其父类的更新而演变,除非父类是专门为了扩展而设计,并且有很好的文档说明
- 过多的条件判断
- 过长的参数列
- 临时变量过多
- 令人迷惑的暂时字段
- 某个实例变量仅为某种特定情况而设置
- 将实例变量与相应的方法提取到新的类中
- 纯数据类
- 仅包含字段和访问(读写)这些字段的方法
- 此类被称为数据容器,应保持最小可变性
- 不恰当的命名
- 命名无法准确描述做的事情
- 命名不符合约定俗称的惯例
-
坏代码的问题
难以复用
- 系统关联性过多,导致很难分离可重用部分
- 难于变化
- 一处变化导致其他很多部分的修改,不利于系统稳定
- 难于理解
- 命名杂乱,结构混乱,难于阅读和理解
难以测试
- 分支、依赖较多,难以覆盖全面
什么是好代码
代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。如何重构
SOLID 原则
单一职责原则
一个类只负责完成一个职责或者功能,不要存在多于一种导致类变更的原因。
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。开放-关闭原则
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。里氏替换原则
子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
子类可以扩展父类的功能,但不能改变父类原有的功能
父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。接口隔离原则
调用方不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。 接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。依赖反转原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。迪米特法则
一个对象应该对其他对象保持最少的了解合成复用原则
尽量使用合成/聚合的方式,而不是使用继承。
单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。设计模式
设计模式:软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。
- 分支、依赖较多,难以覆盖全面
创建型:主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码
- 结构型:主要通过类或对象的不同组合,解耦不同功能的耦合
行为型:主要解决的是类或对象之间的交互行为的耦合 | 类型 | 模式 | 说明 | 适用场景 | | —- | —- | —- | —- | | 创建型 | 单例 | 一个类只允许创建一个实例或对象,并为其提供一个全局的访问点 | 无状态/全局唯一/控制资源访问 | |
| 工厂 | 创建一个或者多个相关的对象,而使用者不用关心具体的实现类 | 分离对象的创建和使用 | |
| 建造者 | 用于创建一种类型的复杂对象,通过设置不同的可选参数进行“定制化” | 对象的构造参数较多且多数可选 | |
| 原型 | 通过复制已有对象来创建新的对象 | 对象的创建成本较大且同一类的不同对象之前差别不大 | | 结构型 | 代理 | 不改变原始类和不使用继承的情况下,通过引入代理类来给原始类附加功能 | 增加代理访问,比如监控、缓存、限流、事务、RPC | |
| 装饰者 | 不改变原始类和不使用继承的情况下,通过组合的方式动态扩展原始类的功能 | 动态扩展类的功能 | |
| 适配器 | 不改变原始类的情况下,通过组合的方式使其适配新的接口 | 复用现有类,但与期望接口不适配 | |
| 桥接 | 当类存在多个独立变化的维度时,通过组合的方式使得其可以独立进行扩展 | 存在多个维度的继承体系时 | |
| 门面 | 为子系统中一组接口定义一个更高层的接口,使得子系统更加容易使用 | 解决接口复用性(细粒度)与接口易用性(粗粒度)的矛盾 | |
| 组合 | 将对象组合成树形结构以表示部分-整体的层次结构,统一单个对和组合对象的处理逻辑 | 满足部分与整体这种树形结构 | |
| 享元 | 运用共享技术有效地支持大量细粒度的对象 | 当系统存在大量的对象,这些对象的很多字段取值范围固定 | | 行为型 | 观察者 | 多个观察者监听同一主题对象,当主题对象状态发生变化时通知所有观察者,使它们能够自动更新自己 | 解耦事件创建者与接收者 | |
| 模板 | 定义一个操作中算法的骨架,将某些步骤实现延迟到子类中 | 解决复用与扩展问题 | |
| 策略 | 定义一组算法类,将每个算法分别封装起来,使得它们可以互相替换 | 消除各种 if-else 分支判断 解耦策略的定义、创建、使用 | |
| 状态 | 允许一个对象在其内部状态改变的时候改变其行为 | 分离对象的状态与行为 | |
| 职责链 | 将一组对象连成一条链,请求沿着该链传递,直到某个对象能够处理它为止 | 解耦请求的发送者与接收者 | |
| 迭代器 | 提供一种方法顺序访问一个集合对象的各个元素,但不暴露该对象的内部表示 | 解耦集合对象的内部表示与遍历访问 | |
| 访问者 | 封装一些作用于某种数据结构中各元素的操作,在不改变数据结构的前提下,定义作用于这些元素的新操作。 | 分离对象的数据结构与行为 | |
| 备忘录 | 在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态 | 用于对象的备份与恢复 | |
| 命令 | 将不同的请求封装成对应的命令对象,对命令的执行进行控制且对使用方透明 | 用于控制命令的执行,比如异步、延迟、排队、撤销、存储与撤销 | |
| 解释器 | 为某个语言定义它的语法表示,并定义一个解释器来处理这个语法 | 用于编译器、规则引擎、正则表达式等特定场景 | |
| 中介 | 定义一个单独的中介对象,来封装一组对象之间的交互,避免对象之间的直接交互 | 使各个对象不需要显式地相互引用,从而使其耦合松散 |
代码分层
模块结构说明
- server_main:配置层,负责整个项目的 module 管理,maven 配置管理、资源管理等;
- server_application:应用接入层,承接外部流量入口,例如:RPC 接口实现、消息处理、定时任务等;不要在此包含业务逻辑;
- server_biz:核心业务层,用例服务、领域实体、领域事件等
- server_irepository:资源接口层,负责资源接口的暴露
- server_repository:资源层,负责资源的 proxy 访问,统一外部资源访问,隔离变化。注意:这里强调的是弱业务性,强数据性;
- server_common:公共层,vo、工具等
命名规范
一个好的命名应该要满足以下两个约束:
- 准确描述所做得事情
- 格式符合通用的惯例
如果你觉得一个类或方法难以命名的时候,可能是其承载的功能太多了,需要进一步拆分。
约定俗称的惯例
场景 | 强约束 | 示例 |
---|---|---|
项目名 | 全部小写,多个单词用中划线分隔‘-’ | spring-cloud |
包名 | 全部小写 | com.alibaba.fastjson |
类名/接口名 | 单词首字母大写 | ParserConfig,DefaultFieldDeserializer |
变量名 | 首字母小写,多个单词组成时,除首个单词,其他单词首字母都要大写 | password, userName |
常量名 | 全部大写,多个单词,用’_’分隔 | CACHE_EXPIRED_TIME |
方法 | 同变量 | read(), readObject(), getById() |
类命名
类名使用大驼峰命名形式,类命通常使用名词或名词短语。接口名除了用名词和名词短语以外,还可以使用形容词或形容词短语,如 Cloneable,Callable 等,表示实现该接口的类有某种功能或能力。
场景 | 约束 | 示例 |
---|---|---|
抽象类 | Abstract 或者 Base 开头 | BaseUserService |
枚举类 | Enum 作为后缀 | GenderEnum |
工具类 | Utils 作为后缀 | StringUtils |
异常类 | Exception 结尾 | RuntimeException |
接口实现类 | 接口名+ Impl | UserServiceImpl |
设计模式相关类 | Builder,Factory 等 | 当使用到设计模式时,需要使用对应的设计模式作为后缀,如 ThreadFactory |
处理特定功能的类 | Handler,Predicate, Validator | 表示处理器,校验器,断言,这些类工厂还有配套的方法名如 handle,predicate,validate |
特定层级的类 | Controller,Service,ServiceImpl,Dao 后缀 | UserController, UserServiceImpl,UserDao |
特定层级的值对象 | Ao, Param, Vo,Config, Message | Param 调用入参;Ao 为 thrift 返回结果;Vo 通用值对象; Config 配置类;Message 为 MQ 消息 |
测试类 | Test 结尾 | UserServiceTest, 表示用来测试 UserService 类的 |
方法命名
方法命名采用小驼峰的形式,首字小写,往后的每个单词首字母都要大写。和类名不同的是,方法命名一般为动词或动词短语,与参数或参数名共同组成动宾短语,即动词 + 名词。一个好的函数名一般能通过名字直接获知该函数实现什么样的功能。
场景 | 约束 | 示例 |
---|---|---|
返回真伪值 | is/can/has/needs/should | isValid/canRemove |
用于检查 | ensure/validate | ensureCapacity/validateInputs |
按需执行 | IfNeeded/try/OrDefault/OrElse | drawIfNeeded/tryCreate/getOrDefault |
数据相关 | get/search/save/update/batchSave/ batchUpdate/saveOrUpdateselect /insert/update/delete | getUserById/searchUsersByCreateTime |
生命周期 | initialize/pause/stop/destroy | initialize/pause/onPause/stop/onStop |
常用动词对 | split/join、inject/extract、bind/seperate、 increase/decrease、lanch/run、observe/listen、build/publish、 encode/decode、submit/commit、push/pull、enter/exit、 expand/collapse、encode/decode |
|
重构技巧
提炼方法
多个方法代码重复、方法中代码过长或者方法中的语句不在一个抽象层级。方法是代码复用的最小粒度,方法过长不利于复用,可读性低,提炼方法往往是重构工作的第一步。
意图导向编程:把处理某件事的流程和具体做事的实现方式分开。
- 把一个问题分解为一系列功能性步骤,并假定这些功能步骤已经实现
- 我们只需把把各个函数组织在一起即可解决这一问题
- 在组织好整个功能后,我们在分别实现各个方法函数
/
1、交易信息开始于一串标准ASCII字符串。
2、这个信息字符串必须转换成一个字符串的数组,数组存放的此次交易的领域语言中所包含的词汇元素(token)。
3、每一个词汇必须标准化。
4、包含超过150个词汇元素的交易,应该采用不同于小型交易的方式(不同的算法)来提交,以提高效率。
5、如果提交成功,API返回”true”;失败,则返回”false”。
/
public class Transaction {
public Boolean commit(String command) {
Boolean result = true;
String[] tokens = tokenize(command);
normalizeTokens(tokens);
if (isALargeTransaction(tokens)) {
result = processLargeTransaction(tokens);
} else {
result = processSmallTransaction(tokens);
}
return** result;
}
}
以函数对象取代函数
将函数放进一个单独对象中,如此一来局部变量就变成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。
引入参数对象
移除对参数的赋值
public int discount(int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
if (quantity > 100) inputVal -= 1;
if (yearToDate > 10000) inputVal -= 4;
return inputVal;
}
public int discount(int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
if (quantity > 100) result -= 1;
if (yearToDate > 10000) result -= 4;
return result;
}
将查询与修改分离
任何有返回值的方法,都不应该有副作用
- 不要在 convert 中调用写操作,避免副作用
- 常见的例外:将查询结果缓存到本地
移除不必要临时变量
临时变量仅使用一次或者取值逻辑成本很低的情况下引入解释性变量
将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途
if ((platform.toUpperCase().indexOf(“MAC”) > -1)
&& (browser.toUpperCase().indexOf(“IE”) > -1) && wasInitialized() && resize > 0) {
// do something
}
final boolean isMacOs = platform.toUpperCase().indexOf(“MAC”) > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf(“IE”) > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// do something
}
使用卫语句替代嵌套条件判断
把复杂的条件表达式拆分成多个条件表达式,减少嵌套。嵌套了好几层的 if - then-else 语句,转换为多个 if 语句
//未使用卫语句
public void getHello(int type) {
if (type == 1) {
return;
} else {
if (type == 2) {
return;
} else {
if (type == 3) {
return;
} else {
setHello();
}
}
}
}
//使用卫语句
public void getHello(int type) {
if (type == 1) {
return;
}
if (type == 2) {
return;
}
if (type == 3) {
return;
}
setHello();
}
使用多态替代条件判断断
当存在这样一类条件表达式,它根据对象类型的不同选择不同的行为。可以将这种表达式的每个分支放进一个子类内的复写函数中,然后将原始函数声明为抽象函数。
public int calculate(int a, int b, String operator) {
int result = Integer.MIN_VALUE;
**if** ("add".equals(operator)) {<br /> result = a + b;<br /> } **else** **if** ("multiply".equals(operator)) {<br /> result = a * b;<br /> } **else** **if** ("divide".equals(operator)) {<br /> result = a / b;<br /> } **else** **if** ("subtract".equals(operator)) {<br /> result = a - b;<br /> }<br /> **return** result;<br />}
当出现大量类型检查和判断时,if else(或 switch)语句的体积会比较臃肿,这无疑降低了代码的可读性。 另外,if else(或 switch)本身就是一个“变化点”,当需要扩展新的类型时,我们不得不追加 if else(或 switch)语句块,以及相应的逻辑,这无疑降低了程序的可扩展性,也违反了面向对象的开闭原则。
基于这种场景,我们可以考虑使用“多态”来代替冗长的条件判断,将 if else(或 switch)中的“变化点”封装到子类中。这样,就不需要使用 if else(或 switch)语句了,取而代之的是子类多态的实例,从而使得提高代码的可读性和可扩展性。很多设计模式使用都是这种套路,比如策略模式、状态模式。
public interface Operation {
int apply(int a, int b);
}
public class Addition implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
public class OperatorFactory {
private final static Map
static {
operationMap.put(“add”, new Addition());
operationMap.put(“divide”, new Division());
// more operators
}
**public** **static** Operation **getOperation**(String operator) {<br /> **return** operationMap.get(operator);<br /> }<br />}
public int calculate(int a, int b, String operator) {
if (OperatorFactory .getOperation == null) {
throw new IllegalArgumentException(“Invalid Operator”);
}
return OperatorFactory .getOperation(operator).apply(a, b);
}
使用异常替代返回错误码
非正常业务状态的处理,使用抛出异常的方式代替返回错误码
- 不要使用异常处理用于正常的业务流程控制
- 异常处理的性能成本非常高
- 尽量使用标准异常
- 避免在 finally 语句块中抛出异常
- 如果同时抛出两个异常,则第一个异常的调用栈会丢失
- finally 块中应只做关闭资源这类的事情
//使用错误码
public boolean withdraw(int amount) {
if (balance < amount) {
return false;
} else {
balance -= amount;
return true;
}
}
//使用异常
public void withdraw(int amount) {
if (amount > balance) {
throw new IllegalArgumentException(“amount too large”);
}
balance -= amount;
}
引入断言
某一段代码需要对程序状态做出某种假设,以断言明确表现这种假设。
- 不要滥用断言,不要使用它来检查“应该为真”的条件,只使用它来检查“一定必须为真”的条件
- 如果断言所指示的约束条件不能满足,代码是否仍能正常运行?如果可以就去掉断言
引入 Null 对象或特殊对象
当使用一个方法返回的对象时,而这个对象可能为空,这个时候需要对这个对象进行操作前,需要进行判空,否则就会报空指针。当这种判断频繁的出现在各处代码之中,就会影响代码的美观程度和可读性,甚至增加 Bug 的几率。
空引用的问题在 Java 中无法避免,但可以通过代码编程技巧(引入空对象)来改善这一问题。
//空对象的例子
public class OperatorFactory {
static MapoperationMap = new HashMap<>();
static {
operationMap.put(“add”, new Addition());
operationMap.put(“divide”, new Division());
// more operators
}
public static OptionalgetOperation(String operator) {
return Optional.ofNullable(operationMap.get(operator));
}
}
public int calculate(int a, int b, String operator) {
Operation targetOperation = OperatorFactory.getOperation(operator)
.orElseThrow(() -> new IllegalArgumentException(“Invalid Operator”));
return targetOperation.apply(a, b);
}
//特殊对象的例子
public class InvalidOp implements Operation {
@Override
public int apply(int a, int b) {
throw new IllegalArgumentException(“Invalid Operator”);
}
}
提炼类
根据单一职责原则,一个类应该有明确的责任边界。但在实际工作中,类会不断的扩展。当给某个类添加一项新责任时,你会觉得不值得分离出一个单独的类。于是,随着责任不断增加,这个类包含了大量的数据和函数,逻辑复杂不易理解。
此时你需要考虑将哪些部分分离到一个单独的类中,可以依据高内聚低耦合的原则。如果某些数据和方法总是一起出现,或者某些数据经常同时变化,这就表明它们应该放到一个类中。另一种信号是类的子类化方式:如果你发现子类化只影响类的部分特性,或者类的特性需要以不同方式来子类化,这就意味着你需要分解原来的类。
//原始类
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
**public** String **getName**() {<br /> **return** name;<br /> }
**public** String **getTelephoneNumber**() {<br /> **return** ("(" + officeAreaCode + ")" + officeNumber);<br /> }
**public** String **getOfficeAreaCode**() {<br /> **return** officeAreaCode;<br /> }
**public** **void** **setOfficeAreaCode**(String arg) {<br /> officeAreaCode = arg;<br /> }
**public** String **getOfficeNumber**() {<br /> **return** officeNumber;<br /> }
**public** **void** **setOfficeNumber**(String arg) {<br /> officeNumber = arg;<br /> }<br />}
//新提炼的类(以对象替换数据值)
public class TelephoneNumber {
private String areaCode;
private String number;
**public** String **getTelephnoeNumber**() {<br /> **return** ("(" + getAreaCode() + ")" + number);<br /> }
String **getAreaCode**() {<br /> **return** areaCode;<br /> }
**void** **setAreaCode**(String arg) {<br /> areaCode = arg;<br /> }
String **getNumber**() {<br /> **return** number;<br /> }
**void** **setNumber**(String arg) {<br /> number = arg;<br /> }<br />}
组合优先于继承
继承使实现代码重用的有力手段,但这并非总是完成这项工作的最佳工具,使用不当会导致软件变得很脆弱。与方法调用不同的是,继承打破了封装性。子类依赖于其父类中特定功能的实现细节,如果父类的实现随着发行版本的不同而变化,子类可能会遭到破坏,即使他的代码完全没有改变。
举例说明,假设有一个程序使用 HashSet,为了调优该程序的性能,需要统计 HashSet 自从它创建以来添加了多少个元素。为了提供该功能,我们编写一个 HashSet 的变体。
// Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
**public** **InstrumentedHashSet**() { }
**public** **InstrumentedHashSet**(**int** initCap, **float** loadFactor) {<br /> **super**(initCap, loadFactor);<br /> }
@Override<br /> **public** **boolean** **add**(E e) {<br /> addCount++;<br /> **return** **super**.add(e);<br /> }
@Override<br /> **public** **boolean** **addAll**(Collection<? extends E> c) {<br /> addCount += c.size();<br /> **return** **super**.addAll(c);<br /> }
**public** **int** **getAddCount**() {<br /> **return** addCount;<br /> }<br />}
通过在新的类中增加一个私有域,它引用现有类的一个实例,这种设计被称为组合,因为现有的类变成了新类的一个组件。这样得到的类将会非常稳固,它不依赖现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。许多设计模式使用就是这种套路,比如代理模式、装饰者模式
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
private final Set
public ForwardingSet(Set
@Override<br /> **public** **int** **size**() { **return** s.size(); }<br /> @Override<br /> **public** **boolean** **isEmpty**() { **return** s.isEmpty(); }<br /> @Override<br /> **public** **boolean** **contains**(Object o) { **return** s.contains(o); }<br /> @Override<br /> **public** Iterator<E> **iterator**() { **return** s.iterator(); }<br /> @Override<br /> **public** Object[] toArray() { **return** s.toArray(); }<br /> @Override<br /> **public** <T> T[] toArray(T[] a) { **return** s.toArray(a); }<br /> @Override<br /> **public** **boolean** **add**(E e) { **return** s.add(e); }<br /> @Override<br /> **public** **boolean** **remove**(Object o) { **return** s.remove(o); }<br /> @Override<br /> **public** **boolean** **containsAll**(Collection<?> c) { **return** s.containsAll(c); }<br /> @Override<br /> **public** **boolean** **addAll**(Collection<? extends E> c) { **return** s.addAll(c); }<br /> @Override<br /> **public** **boolean** **retainAll**(Collection<?> c) { **return** s.retainAll(c); }<br /> @Override<br /> **public** **boolean** **removeAll**(Collection<?> c) { **return** s.removeAll(c); }<br /> @Override<br /> **public** **void** **clear**() { s.clear(); }<br />}
// Wrappter class - uses composition in place of inheritance
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
private int addCount = 0;
**public** **InstrumentedHashSet1**(Set<E> s) {<br /> **super**(s);<br /> }
@Override<br /> **public** **boolean** **add**(E e) {<br /> addCount++;<br /> **return** **super**.add(e);<br /> }
@Override<br /> **public** **boolean** **addAll**(Collection<? extends E> c) {<br /> addCount += c.size();<br /> **return** **super**.addAll(c);<br /> }
**public** **int** **getAddCount**() {<br /> **return** addCount;<br /> }<br />}
继承与组合如何取舍
- 只有当子类真正是父类的子类型时,才适合继承。对于两个类 A 和 B,只有两者之间确实存在“is-a”关系的时候,类 B 才应该继承 A;
- 在包的内部使用继承是非常安全的,子类和父类的实现都处在同一个程序员的控制之下;
- 对于专门为了继承而设计并且具有很好的文档说明的类来说,使用继承也是非常安全的;
-
接口优于抽象类
Java 提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。自从 Java8 为接口增加缺省方法(default method),这两种机制都允许为实例方法提供实现。主要区别在于,为了实现由抽象类定义的类型,类必须称为抽象类的一个子类。因为 Java 只允许单继承,所以用抽象类作为类型定义受到了限制。
接口相比于抽象类的优势: 现有的类可以很容易被更新,以实现新的接口。
- 接口是定义混合类型(比如 Comparable)的理想选择。
- 接口允许构造非层次结构的类型框架。
接口虽然提供了缺省方法,但接口仍有有以下局限性:
- 接口的变量修饰符只能是 public static final 的
- 接口的方法修饰符只能是 public 的
- 接口不存在构造函数,也不存在 this
- 可以给现有接口增加缺省方法,但不能确保这些方法在之前存在的实现中都能良好运行。
- 因为这些默认方法是被注入到现有实现中的,它们的实现者并不知道,也没有许可
接口缺省方法的设计目的和优势在于:
- 为了接口的演化
- Java 8 之前我们知道,一个接口的所有方法其子类必须实现(当然,这个子类不是一个抽象类),但是 java 8 之后接口的默认方法可以选择不实现,如上的操作是可以通过编译期编译的。这样就避免了由 Java 7 升级到 Java 8 时项目编译报错了。Java8 在核心集合接口中增加了许多新的缺省方法,主要是为了便于使用 lambda。
- 可以减少第三方工具类的创建
- 例如在 List 等集合接口中都有一些默认方法,List 接口中默认提供 replaceAll(UnaryOperator)、sort(Comparator)、、spliterator()等默认方法,这些方法在接口内部创建,避免了为了这些方法而专门去创建相应的工具类。
- 可以避免创建基类
- 在 Java 8 之前我们可能需要创建一个基类来实现代码复用,而默认方法的出现,可以不必要去创建基类。
- 由于接口的局限性和设计目的的不同,接口并不能完全替换抽象类。但是通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。 接口负责定义类型,或许还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的大部分工作。这就是模板方法(Template Method)设计模式。
接口 Protocol:定义了 RPC 协议层两个主要的方法,export 暴露服务和 refer 引用服务
抽象类 AbstractProtocol:封装了暴露服务之后的 Exporter 和引用服务之后的 Invoker 实例,并实现了服务销毁的逻辑
具体实现类 XxxProtocol:实现 export 暴露服务和 refer 引用服务具体逻辑
优先考虑泛型
声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口。泛型类和接口统称为泛型(generic type)。泛型从 Java 5 引入,提供了编译时类型安全检测机制。泛型的本质是参数化类型,通过一个参数来表示所操作的数据类型,并且可以限制这个参数的类型范围。泛型的好处就是编译期类型检测,避免类型转换。
// 比较三个值并返回最大值
public static
T max = x;
// 假设x是初始最大值
if ( y.compareTo( max ) > 0 ) {
max = y; //y 更大
} if ( z.compareTo( max ) > 0 ) {
max = z; // 现在 z 更大
} return max; // 返回最大对象
}
public static void main( String args[] ) {
System.out.printf( “%d, %d 和 %d 中最大的数为 %d\n\n”, 3, 4, 5, maximum( 3, 4, 5 ));
System.out.printf( “%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n\n”, 6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ));
System.out.printf( “%s, %s 和 %s 中最大的数为 %s\n”,”pear”, “apple”, “orange”, maximum( “pear”, “apple”, “orange” ) );
}
不要使用原生态类型
由于为了保持 Java 代码的兼容性,支持和原生态类型转换,并使用擦除机制实现的泛型。但是使用原生态类型就会失去泛型的优势,会受到编译器警告。
要尽可能地消除每一个非受检警告
每一条警告都表示可能在运行时抛出 ClassCastException 异常。要尽最大的努力去消除这些警告。如果无法消除但是可以证明引起警告的代码是安全的,就可以在尽可能小的范围中,使用@SuppressWarnings(“unchecked”)注解来禁止警告,但是要把禁止的原因记录下来。
利用有限制通配符来提升 API 的灵活性
参数化类型不支持协变的,即对于任何两个不同的类型 Type1 和 Type2 而言,List 既不是 List 的子类型,也不是它的超类。为了解决这个问题,提高灵活性,Java 提供了一种特殊的参数化类型,称作有限制的通配符类型,即 List<? extends E>和 List<? super E>。使用原则是 producer-extends,consumer-super(PECS)。如果即是生产者,又是消费者,就没有必要使用通配符了。
还有一种特殊的无限制通配符 List<?>,表示某种类型但不确定。常用作泛型的引用,不可向其添加除 Null 以外的任何对象。
//List<? extends E>
// Number 可以认为 是Number 的 “子类”
List<? extends Number> numberArray = new ArrayList
// Integer 是 Number 的子类
List<? extends Number> numberArray = new ArrayList
// Double 是 Number 的子类
List<? extends Number> numberArray = new ArrayList
//List<? super E>
// Integer 可以认为是 Integer 的 “父类”
List<? super Integer> array = new ArrayList
// Number 是 Integer 的 父类
List<? super Integer> array = new ArrayList
// Object 是 Integer 的 父类
List<? super Integer> array = new ArrayList