- 1 Introduction
- 2 Duplicated Code 重复代码
- 3 Long Method 过长方法
- 4 Large Class 体积庞大的类
- 5 Long Parameter List 过长的参数列表
- 6 Divergent Change 扩散的变更
- 7 Shotgun Surgery 霰弹式手术
- 8 Feature Envy 特性羡慕
- 9 Data Clumps 数据扎堆
- 10 Primitive Obsession 原始类型困扰
- 11 Switch Statements Switch语句
- 12 Parallel Inheritance Hierarchies 并列的继承层级
- 13 Lazy Class
- 14 Speculative Generality
- 15 Temporary Field 临时字段
- 16 Message Chains 消息链
- 17 Middle Man 中间人
- 18 Inappropriate Intimacy 不正当亲密关系
- 19 Alternative Classes with Different Interfaces
- 20 Incomplete Library Class
- 21 Data Class 数据类
- 22 Refused Bequest 拒绝遗产
- 23 Comment
1 Introduction
Refactoring, https://refactoring.com/ by Martin Fowler, Kent Beck
- High Cohesion 高内聚
- Low Coupling 低耦合
- 代码的本质是给人阅读
- 一个修改需要修改10个类20个方法
- 内聚度不够高,一个功能被拆分到了太多的类
- 耦合度很高,一个功能的执行,涉及到太多的类
- 维护即变更,难以理解,容易出错 => 代码质量
- 从生理角度看,太多的关联关系,修改时需要更多的大脑思考活动,意味着更高的能量消耗,更低的效率
- 10个类,1对1对1… 最多9个关系,多对多对多…最多10! = 3628800
2 Duplicated Code 重复代码
2.1 现象
- 相似的代码
- 相似的代码结构
- 似曾眼熟即为相似 ```typescript // number.ts function sortNumberDesc(numbers: number[]) {} function sortNumberAsc(numbers: number[]) {}
// string.ts function sortStringDesc(strings: string[]) {} function sortStringAsc(strings: string[]) {}
// date.ts function sortDateDesc(dates: string[]) {} function sortDateAsc(dates: string[]) {}
<a name="H7Pid"></a>
## 2.2 解法
- Extract Method 抽取公共方法
- 仅抽取相同的业务逻辑,不要用if-else来处理不同部分
- 不同部分保留在原处,数据作为公共方法的参数传入
- Pull Up Field 提升字段
![image.png](https://cdn.nlark.com/yuque/0/2021/png/162086/1625748320735-ae6ccaf3-4ec5-48b0-ad2b-090137d93132.png#align=left&display=inline&height=156&id=Iz8vW&margin=%5Bobject%20Object%5D&name=image.png&originHeight=156&originWidth=500&size=12534&status=done&style=none&width=500)
- Form Template Method 形成**模板方法**:
- 使用继承来消除重复代码
- 把相似的业务逻辑抽取到父类
- 把不同的业务逻辑变成抽象方法,留给子类实现
```typescript
class Site {
}
class ResidentialSite extends Site {
getSellAmount() {
const base = this.area * this.rate * 0.5; // 基本售价
const tax = base * Site.TAX_RATE * 0.5; // 税金
const commission = base * ResidentialSite.COMMISSION_RATE; // 交易佣金
return base + tax + commission;
}
}
class CommerceSite extends Site {
getSellAmount() {
const base = this.area * this.rate;
const tax = base * Site.TAX_RATE;
return base + tax;
}
}
class Site {
getSellAmount(): number {
// 基本售价 + 税金 + 交易佣金
return this.getBaseAmount() + this.getTaxAmount() + this.getCommissionAmount();
}
abstract getBaseAmount(): number;
abstract getTaxAmount(): number;
abstract getCommissionAmount(): number;
}
class ResidentialSite extends Site {
getBaseAmount(): number {
return this.area * this.rate * 0.5;
}
getTaxAmount(): number {
return this.getBaseAmount() * Site.TAX_RATE * 0.5;
}
getCommissionAmount(): number {
return this.getBaseAmount() * ResidentialSite.COMMISSION_RATE;
}
}
class CommerceSite extends Site {
getBaseAmount(): number {
return this.area * this.rate;
}
getTaxAmount(): number {
return this.getBaseAmount() * Site.TAX_RATE;
}
getCommissionAmount(): number {
return 0;
}
}
Refer: https://gitlab.shoplazza.site/shoplaza/frontend/checkout/sunfish/blob/master/src/pageAction.ts#L43
- Substitute Algorithm 替换算法
Extract Class 抽取类
- 把相同代码抽取到一个class中去 ```typescript class Persion { officeAreaCode: number; officeNumber: number; homeAreaCode: number; homeNumber: number
getOfficeTelephoneNumer(): string; getHomeTelephoneNumber(): string; }
class Person { officeTelephoneNumber: TelephoneNumber; homeTelephoneNumber: TelephoneNumber; getOfficeTelephoneNumer(): string; getHomeTelephoneNumber(): string; }
- Extract Function 抽取函数
```typescript
// number.ts
function sortNumberDesc(numbers: number[]) {}
function sortNumberAsc(numbers: number[]) {}
// string.ts
function sortStringDesc(strings: string[]) {}
function sortStringAsc(strings: string[]) {}
// date.ts
function sortDateDesc(dates: string[]) {}
function sortDateAsc(dates: string[]) {}
function sort(items: T[], comparator: (a: T, b: T) => 0 | 1 | -1): T[]{}
3 Long Method 过长方法
3.1 现象
-
3.2 解法
Extract Method 抽取多个方法
- Replace Temp with Query: 如果有大量中间变量,使用方法调用替换中间变量
```typescript if (basePrice() > 1000) return basePrice() 0.95; else return basePrice() 0.98;const basePrice = quantity * itemPrice;
if (basePrice > 1000)
return basePrice * 0.95;
else
return basePrice * 0.98;
- Replace Temp with Query: 如果有大量中间变量,使用方法调用替换中间变量
function basePrice() { return quantity * itemPrice; }
- 如果需要传递大量参数到各个方法
- Introduce Parameter Object 使用参数对象
```typescript
function fn(a, b, c, d, e, f){}
function fn({ a, b, c, d, e, f}){}
- Preserve Whole Object 保留整个对象
const low = daysTempRange().getLow();
const high = daysTempRange().getHigh();
const timezone = daysTempRange().getTimezon();
withinPlan = plan.withinRange(low, high, timezone);
withinPlan = plan.withinRange(daysTempRange());
- Decompose Conditional 拆解条件语句
// 计算取暖费
if (date.before (SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * _winterRate + _winterServiceCharge;
} else {
charge = quantity * _summerRate;
}
if (notSummer(date)) {
charge = winterCharge(quantity);
} else {
charge = summerCharge (quantity);
}
4 Large Class 体积庞大的类
- 通常违反SRP原则,承担了太多各种各样的功能职责
- Extract Class 抽取类:优先考虑,组合优于继承
- Extract Subclass 抽取子类
`
5 Long Parameter List 过长的参数列表
- Introduce Parameter Object 使用参数对象
- Preserve Whole Object 保留整个对象
- Replace Parameter with Method 使用方法替换参数
6 Divergent Change 扩散的变更
- 为一个类添加功能时,需要同时修改该类的多个方法
- 通常需要把一个类的职责拆分到多个类中
7 Shotgun Surgery 霰弹式手术
- 做出某种变更时(如支持另一种数据库,支持新的文件格式等),需要对大量的类做出大量的细微变更
- Move Method & Move Field:把分布在各处的变更集中到一个类中,如果没有这样的类,可以新创建一个
- Inline Class(和Extract Class相反):把一个类的所有功能转移到另外一个类中,然后删除它
8 Feature Envy 特性羡慕
- A类的一个方法似乎应该放在B类中
- 该方法大量调用了B类的getter方法获取数据进行计算
- Move Method:转移该方法到B类中
- Extract Method:有时候只需要抽取一部分代码作为一个新方法转移到B类中
9 Data Clumps 数据扎堆
- 一些数据项经常一起出现,有时一起出现在类中,有时一起出现在方法签名中
- Extract Class:抽取一个公共类
- Introduce Parameter Object & Preserve Whole Object:处理方法签名
- 可能需要进一步拆分更多的公共类
- 如果移除某些字段,其他字段组成的类业务语义完整,那么意味着需要进一步拆分
10 Primitive Obsession 原始类型困扰
- Primitive Type vs Reference Type
- 新手通常不愿意创建简单的类
- Replace Data Value with Object:有明确的业务语义时,即使一个字段也可以创建类
- Replace Type Code with Class
- Replace Type Code with Subclasses:如果需要通过type code执行条件语句,利用多态来移除条件语句
- Replace Type Code with State/Strategy:组合优于继承
11 Switch Statements Switch语句
- OOP代码中,一般很少出现Switch语句,因为添加新分支时,需要修改多处
Replace Conditional with Polymorphism: 通常使用多态来替换Switch语句(包括相似的if-else语句) ```typescript class HTMLElement { validate() { switch(this.tagName) {
case 'DIV':
// ...
case 'BUTTON':
// ...
...
} }
render() { switch(this.tagName) {
case 'DIV':
// ...
case 'BUTTON':
// ...
...
} } }
const elements = createElement(htmlCode); elements.forEach(e => e.render());
```typescript
abstract class HTMLElement {
abstract validate();
abstract render();
}
class HTMLDIVElement extends HTMLElement {
readonly tagName: string = 'DIV';
validate() {
// ...
}
render() {
// ...
}
}
class HTMLButtonElement extends HTMLElement {
readonly tagName: string = 'BUTTON';
validate() {
// ...
}
render() {
// ...
}
}
const elements = createElement(htmlCode);
elements.forEach(e => e.render());
- Replace Parameter with Explicit Methods:如果条件语句影响较小,抽取多个子类是过度设计
function setValue (name: string, value: number): void {
if (name.equals("height"))
height = value;
if (name.equals("width"))
width = value;
Assert.shouldNeverReachHere();
}
12 Parallel Inheritance Hierarchies 并列的继承层级
- 创建一个子类A,就必须创建另外一个子类B
- Move Method & Move Field:把两个并列的层级合并起来
13 Lazy Class
- 过度设计,只有一个子类的父类
14 Speculative Generality
- 过度设计:提前设计实现好父类子类,抽象类等
15 Temporary Field 临时字段
- 使用对象的实例字段来存储一些中间变量
- 对象的实例字段反映实例的状态
16 Message Chains 消息链
16.1 现象
const state = objA.getObjB().getObjC().getObjD().getObjE().getState();
const state = objA.getState();
class A {
getState() {
return objB.getState();
}
}
class B {
getState() {
return objC.getState();
}
}
// ....
- Extract Method: 获得State的代码逻辑可以抽出一个公共方法,直接调用
17 Middle Man 中间人
- 对象的主要特性之一是封装,封装常常意味着委托
- 如果一个类一半以上的方法实现都委托给了其他类,通常它只是一个中间
- 把调用委托对象方法的代码转移到中间人方法的调用者中去
- 注意Facade模式:又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口
- Remove Middle Man
class MiddleMan {
doA() {
return a.do();
}
doB() {
return b.doB();
}
doC() {
return a.doC();
}
}
// client.js
middleMan.doA();
middleMan.doB();
middleman.doC();
class MiddleMan {
doA() {
return a.do();
}
doB() {
return b.doB();
}
doC() {
return c.doC();
}
}
// client.js
a.do();
b.do();
c.do();
18 Inappropriate Intimacy 不正当亲密关系
- 一个类过于关注另一个类的私有代码,类和类之间的协议和契约,应该由且仅由共有接口和协议指定:
Hack-into - Move method & Move field & Extract Class:可能存在职责分配不合理
- Change Bidirectional Association to Unidirectional
class Order {
private customer: Customer;
Customer getCustomer() {
return this.customer;
}
setCustomer (Customer arg): void {
if (this.customer != null) this.customer.friendOrders().remove(this);
this.customer = arg;
if (this.customer != null) this.customer.friendOrders().add(this);
}
getDiscountedPrice(): number {
return getGrossPrice() * (1 - this.customer.getDiscount());
}
}
class Customer {
private orders: Set = new Set();
addOrder(Order arg): void {
arg.setCustomer(this);
this.orders.add(arg);
}
friendOrders(): Set {
/** should only be used by Order */
return this.orders;
}
getPriceFor(Order order) {
Assert.isTrue(this.orders.contains(order));
return order.getDiscountedPrice();
}
}
class Order {
getDiscountedPrice(customer: Customer): number {
return getGrossPrice() * (1 - customer.getDiscount());
}
}
class Customer {
private orders: Set = new Set();
addOrder(Order arg): void {
this.orders.add(arg);
}
getPriceFor(Order order) {
Assert.isTrue(this.orders.contains(order));
return order.getDiscountedPrice(this);
}
}
19 Alternative Classes with Different Interfaces
20 Incomplete Library Class
21 Data Class 数据类
- 贫血模型:领域对象只有状态以及getter/setter方法
- 充血模型:将大多数业务逻辑和持久化放在领域对象中
- Encapsulate Field => Encapsulate Collection => Remove Setting Method => Extract Method & Move Method => Hide Method
- Data classes像小孩子一样慢慢长大
22 Refused Bequest 拒绝遗产
- 子类逐渐开始拒绝接纳父类的方法、数据乃至接口
- Push Down Method & Push Down Field:把父类中用不到的字段和方法放到一个兄弟类中,已有的子类去继承这个兄弟类
- Replace Inheritance with Delegation
23 Comment
- 使用Comment来解释
- Extract Method & Rename Method:需要特别解释的代码
- Introduce Assertion:检查和申请参数和系统状态
Tip: When you feel the need to write a comment, first try to refactor the code so that any comment becomes superfluous.
- 写注释的最佳时机
- 不知道该做什么、怎么做等不确定的内容
- 为什么代码要这么写
- TODO list