类内味道

可度量的味道

  1. 过长方法

我们遵循这样一条原则:每当感觉须要写注释来说明代码的时候。我们就把须要说明的东西写进一个独立的方法中,并以其意图(而非实现手法)命名。

  • 抽取方法 (解释能力、共享能力、选择能力)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    class Api {
    constructor() {
    this.name = 'yyy'
    this.age = 18
    }
    sayHello() {
    login()
    console.log(`name: ${this.name}`)
    console.log(`name: ${this.age}`)
    }
    login() {
    console.log('login')
    }
    }

    to

    class Api {
    constructor() {
    this.name = 'yyy'
    this.age = 18
    }
    sayHello() {
    login()
    speak()
    }
    login() {
    console.log('login')
    }
    speak() {
    console.log(`name: ${this.name}`)
    console.log(`name: ${this.age}`)
    }
    }
  • 将临时变量替换为查询

    我们常做的是临时变量作为参数传递给提炼出来的对象,但这样对提升代码可读性并没有帮助

正确的做法是

1
2
3
4
5
6
7
8
9
10
11
12
13
function sum() {
const basePrice = _quantity * _itemPrice
return basePrice > 1000 ? basePrice * 0.8 : basePrice * 0.9
}

to

function sum() {
return basePrice() > 1000 ? basePrice() * 0.8 : basePrice() *0.9
}
function basePrice() {
return _quantity * _itemPrice
}
  • 将方法替换为方法对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function gamma(inputVal, quantity, yearToDate) {
const importantValue1 = (inputVal * quantity) + delta();
const importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue1) > 100) {
importantValue2 -= 20;
}
return importantValue2 - 2 * importantValue1;
}

to

function gamma(inputVal, quantity, yearToDate) {
return new Gamma(inputVal, quantity, yearToDate).compute()
}

class Gamma{
constructor(inputVal, quantity, yearToDate) {
this.inputVal = inputVal
this.quantity = quantity
this.yearToDate = yearToDate
this.importantValue1 = (inputVal * quantity) + delta()
this.importantValue2 = (inputVal * quantity) + 100
}
compute() {
if ((this.yearToDate - this.importantValue1) > 100) {
this.importantValue2 -= 20;
}
return this.importantValue2 - 2 * this.importantValue1
}
}

现在我可以轻松地对compute()函数采取 Extract Method,不必担心引数(argument)传递

  • 分解条件式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    class Api {
    constructor() {
    this.name = 'yyy'
    this.age = 18
    }
    ask(msg) {
    if(msg === 'name' || msg === 'age') {
    console.log(`${msg}: ${this[msg]}`)
    } else {
    console.log('error-nodata')
    }
    }
    }

    to

    class Api {
    constructor() {
    this.name = 'yyy'
    this.age = 18
    }
    ask(msg) {
    if(this.isValidQuestion(msg)) {
    this.speak(speak)
    } else {
    this.nodata()
    }
    }
    isValidQuestion(msg) {
    return msg === 'name' || msg === 'age'
    }
    speak(msg) {
    console.log(`${msg}: ${this[msg]}`)
    }
    nodata() {
    console.log('error-nodata')
    }
    }
  1. 过大的类

说明这个类做太多事情。其内往往就会出现太多instance变量。一旦如此。Duplicated Code也就接踵而至了。

  • 抽取类

将「与电话号码相关」的行为分离到一个独立class中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Person{
constructor() {
this._name = ''
this._officeAreaCode = ''
this._officeNumber = ''
}
getName() {
return this._name;
}
getTelephoneNumber() {
return ("(" + this._officeAreaCode + ") " + this._officeNumber);
}
getOfficeAreaCode() {
return this._officeAreaCode;
}
setOfficeAreaCode(arg) {
this._officeAreaCode = arg;
}
getOfficeNumber() {
return this._officeNumber;
}
setOfficeNumber(arg) {
this._officeNumber = arg;
}
}

to

class Person{
constructor() {
this._name = ''

this._officeTelephone = new TelephoneNumber()
}
getName() {
return this._name;
}
getTelephoneNumber() {
return this._officeTelephone.getTelephoneNumber()
}
getOfficeTelephone() {
return this._officeTelephone
}
}

class TelephoneNumber {
constructor() {
this._areaCode = ''
this._number = ''
}
getTelephoneNumber() {
return ("(" + this._areaCode + ") " + this._number);
}

getAreaCode() {
return this._areaCode
}
setAreaCode(arg) {
this._areaCode = arg
}

getNumber() {
return this._number
}
setNumber(arg) {
this._number= arg
}
}
  • 抽取子类

    class 中的某些行为只被一部分实体用到,其他实体不需要它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class JobItem {
constructor(unitPrice, quantity, isLabor, employee) {
this._unitPrice = unitPrice;
this._quantity = quantity;
this._isLabor = isLabor;
this._employee = employee || new Employee();
}
getTotalPrice() {
return this.getUnitPrice() * this._quantity
}
getUnitPrice() {
return this._isLabor ? this._employee.getRate() : this._unitPrice
}
getQuantity() {
return this._quantity
}
getEmployee() {
return this._employee
}
}

class Employee {
constructor(rate) {
this._rate = rate
}
getRate() {
return this._rate
}
}

const kent = new Employee('rate')
const j1 = new JobItem (0, 5, true, kent)

to

class JobItem {
constructor(unitPrice, quantity) {
this._unitPrice = unitPrice;
this._quantity = quantity;
}
getTotalPrice() {
return this.getUnitPrice() * this._quantity
}
getUnitPrice() {
return this._unitPrice
}
getQuantity() {
return this._quantity
}
getEmployee() {
return this._employee
}
}

class Employee {
constructor(rate) {
this._rate = rate
}
getRate() {
return this._rate
}
}

class LaborItem extends JobItem {
constructor(quantity, employee) {
super(0, quantity)
this._employee = employee
}
getUnitPrice() {
return this._employee.getRate()
}
}

const kent = new Employee('rate')
const j1 = new LaborItem (5, kent)

console.log(j1.getQuantity())
  • 抽取接口

    implements关键字 js中没有

  • 将数据值替换为对象

    一开始你可能会用一个字符串来表示「电话号码」概念,但是随后你就会发现,电话号码需要「格式化」、「抽取区号」之类的特殊行为。如果这样的数据项只有一二个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Order {
constructor(customer) {
this._customer = customer
}
getCustomer() {
return this._customer
}
setCustomer(arg) {
this._customer = arg
}
}

to

class Order {
constructor(customer) {
this._customer = new Customer(customer)
}
getCustomer() {
return this._customer.getName()
}
setCustomer(arg) {
this._customer = new Customer(arg)
}
}

class Customer{
constructor(name) {
this._name = name
}
getName() {
return this._name
}
}
  1. 过长的参数表
  • 将参数替换为方法
  • 引入参数对象
  • 保持对象完整
  1. 过多的注释

    经常会有这种情况:你看到一段代码有着长长的注释。然后发现,这些注释之所以存在乃是由于代码非常糟糕。这种情况的发生次数之多。实在令人惊讶。

  • 抽取方法
  • 引入断言

不必要的复杂性

过分一般性

  • 折叠继承体系

    继承体系很容易变得过分复杂。如果继承并不能带来该有的价值,尝试将两个类合并起来。

  • 内联类

    你的某个class没有做太多事情(没有承担足够责任)。将class的所有特性搬移到另一个class中,然后移除原class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Person {
constructor() {
this._officeTelephone = new TelephoneNumber()
this.name = 'yyy'
}
getName() {
return this._name;
}
getTelephoneNumber() {
return this._officeTelephone.getTelephoneNumber();
}
getOfficeTelephone() {
return this._officeTelephone;
}
}
class TelephoneNumber {
constructor(number, areaCode) {
this._number = number
this._areaCode = areaCode
}
getTelephoneNumber() {
return ("(" + this._areaCode + ") " + this._number);
}
getAreaCode() {
return this._areaCode;
}
setAreaCode(arg) {
this._areaCode = arg;
}
getNumber() {
return this._number;
}
setNumber(arg) {
this._number = arg;
}
}
  • 移除参数
  • 重命名方法

重复

  1. 重复性代码

    坏味道的首当其冲是重复的代码Duplicated Code。假设你在一个以上的地点看到同样的程序结构,那么当可肯定:设法将它们合而为一,程序会变得更好。

  • 抽取方法
    同上

  • 抽取类
    同上

  • 上移方法

    有些函数,在各个subclass 中产生完全相同的结果。将该函数移至superclass。

  • 构建模板方法

    继承是「避免重复行为」的一个强大工具。无论何时,只要你看见两个subclasses 之中有类似的函数,就可以把它们提升到superclass 。但是如果这些函数并不完全相同呢?此时的你应该怎么办?我们仍有必要尽量避免重复,但又必须保持这些函 数之间的实质差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Customer {
constructor(rentals) {
this._rentals = rentals
this._name = 'yyy'
}
getName() {
return this._name
}
getTotalCharge() {

}
getTotalFrequentRenterPoints() {

}
statement() {
const rentals = this._rentals.elements();
let result = "Rental Record for " + getName() + "\n";
while (rentals.hasMoreElements()) {
const each = rentals.nextElement();
//show figures for this rental
result += "\t" + each.getMovie().getTitle() + "\t" +
String.valueOf(each.getCharge()) + "\n";
}
//add footer lines
result += "Amount owed is " + String.valueOf(getTotalCharge()) + "\n";
result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) +
" frequent renter points";
return result;
}
htmlStatement() {
const rentals = _rentals.elements();
let result = "<H1>Rentals for <EM>" + getName() + "</EM></H1><P>\n";
while (rentals.hasMoreElements()) {
const each = rentals.nextElement();
//show figures for each rental
result += each.getMovie().getTitle() + ": " +
String.valueOf(each.getCharge()) + "<BR>\n";
}
//add footer lines
result += "<P>You owe <EM>" + String.valueOf(getTotalCharge()) + "</EM><P>\n";
result += "On this rental you earned <EM>" +
String.valueOf(getTotalFrequentRenterPoints()) +
"</EM> frequent renter points<P>";
return result;
}
}

  1. 接口不同的相似类

    假设两个方法做同一件事,却有着不同的签名式。基本也是java的

  • 重命名方法
  • 搬移方法

条件逻辑

Switch惊悚现身

  • 将条件式替换为多态

它根据对象型别的不同而选择不同的行为。
将这个条件式的每个分支放进一个subclass 内的覆写函数中,然后将原始函数声明为抽象函数

  • 将类型码替换为子类
  • 将类型码替换为状态/策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
class Employee {
constructor() {
this.ENGINEER = 0
this.SALESMAN = 1
this.MANAGER = 2
}
setType(type) {
this._type = type;
}
getType() {
return this._type;
}
payAmount() {
switch (this.getType()) {
case this.ENGINEER: // 工程师
return _monthlySalary;
case this.SALESMAN: // 销售员
return _monthlySalary + _commission;
case this.MANAGER: // 经理
return _monthlySalary + _bonus;
default:
throw new RuntimeException("Incorrect Employee");
}
}
}

to

class Employee {
setType(arg) {
EmployeeType.newType(arg)
}
getType() {
return this._type;
}
payAmount() {
switch (this.getType()) {
case EmployeeType.ENGINEER: // 工程师
return _monthlySalary;
case EmployeeType.SALESMAN: // 销售员
return _monthlySalary + _commission;
case EmployeeType.MANAGER: // 经理
return _monthlySalary + _bonus;
default:
throw new RuntimeException("Incorrect Employee");
}
}
}

abstract class EmployeeType {
constructor() {
this.ENGINEER = 0
this.SALESMAN = 1
this.MANAGER = 2
}
getTypeCode(){}
newType(code) {
switch (code) {
case this.ENGINEER: // 工程师
return new Engineer();
case this.SALESMAN: // 销售员
return new Salesman();
case this.MANAGER: // 经理
return new Manager();
default:
throw new RuntimeException("Incorrect Employee");
}
}
}

class Engineer extends EmployeeType {
getTypeCode() {
return Employee.ENGINEER;
}
}
class Manager extends EmployeeType {
getTypeCode() {
return Employee.MANAGER;
}
}
class Salesman extends EmployeeType {
getTypeCode() {
return Employee.SALESMAN;
}
}

to

class Employee {
setType(arg) {
EmployeeType.newType(arg)
}
getType() {
return this._type;
}
payAmount() {
return this._type.payAmount(this);
}
}

abstract class EmployeeType {
constructor() {
this.ENGINEER = 0
this.SALESMAN = 1
this.MANAGER = 2
}
getTypeCode(){}
newType(code) {
switch (code) {
case this.ENGINEER: // 工程师
return new Engineer();
case this.SALESMAN: // 销售员
return new Salesman();
case this.MANAGER: // 经理
return new Manager();
default:
throw new RuntimeException("Incorrect Employee");
}
}
payAmount() {}
}

class Engineer extends EmployeeType {
payAmount(emp) {
return emp.getMonthlySalary();
}
getTypeCode() {
return Employee.ENGINEER;
}
}
class Manager extends EmployeeType {
payAmount(emp) {
return emp.getMonthlySalary() + emp.getCommission();
}
getTypeCode() {
return Employee.MANAGER;
}
}
class Salesman extends EmployeeType {
payAmount(emp) {
return emp.getMonthlySalary() + emp.getBonus();
}
getTypeCode() {
return Employee.SALESMAN;
}
}
  • 将参数替换为显式方法
  • 引入Null对象

    出现于条件语句 每一个条件都判断了是否为null

类间味道

数据

  1. 基本类型困扰

    单独存在的数值不易于理解,也不符合面向对象的思想。大多数开发初期基础类型可以满足需求,后期深入发现需要扩展

使数值尽量用类代替,就像java中的基本类型那样。

  • 将数据值替换为对象
  • 抽取类
  • 引入参数对象
  • 将数组替换为对象
  • 将类型码替换为类
  • 将类型码替换为子类
  • 将类型码替换为状态/策略
  1. 数据类

    所谓Data Class是指:它们拥有一些字段,以及用于訪问这些字段的方法,除此之外一无长物。

  • 搬移方法
  • 封装字段
  • 封装集合
  1. 数据泥团

    多个类中重复出现的字段,或多个函数(方法)中相同的入参
    重复参数多,影响阅读和理解。
    减少相同的字段及入参,缩短入参列,简化函数调用

  • 抽取类
  • 引入参数对象
  • 保持对象完整
  1. 临时字段

    有时你会看到这种对象:其内某个instance 变量仅为某种特定情势而设。这种代码让人不易理解,

  • 抽取类
  • 引入Null对象

继承

  1. 拒收的遗赠

    继承某个类的子类,并不需要父类的某些方法,属性或不需要实现父类实现的接口

  • 将继承替换为委托
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Vector {
size() {}
isEmpty() {}
isAndroid() {}
isIOS() {}
}

class MyStack extends Vector {
push(element) {
insertElementAt(element, 0);
}
pop() {
const result = firstElement();
removeElementAt(0);
return result;
}
}

to

class Vector {
size() {}
isEmpty() {}
isAndroid() {}
isIOS() {}
}

class MyStack {
constructor() {
this._vector = new Vector()
}
size() {
return this._vector.size()
}
push(element) {
insertElementAt(element, 0);
}
pop() {
const result = firstElement();
removeElementAt(0);
return result;
}
}
  1. 不当的紧密性

    继承(inheritance)往往造成过度亲密

  • 搬移方法
  • 搬移字段
  • 将双向关联改为单向
  • 将继承替换成委托

除非先有Customer对象,否则不会存在Order对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Order { // 订单
constructor() {
this._customer = new Customer() //译注:这是Order-to-Customer link也是本例的移除对象
}
getCustomer() {
return this._customer
}
setCustomer(arg) {
if (_customer != null) {
_customer.friendOrders().remove(this);
}
_customer = arg;
if (_customer != null) {
_customer.friendOrders().add(this);
}
}
}

class Customer { // 顾客
constructor() {
this._orders = new HashSet()
}
addOrder(arg) {
arg.setCustomer(this);
}
friendOrders() {
return this._orders
}
}
  • 隐藏委托
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person { // 人
constructor() {
this._department = new Department()
}
getDepartment() {
return this._department;
}
setDepartment(arg) {
this._department = arg;
}
}
class Department { // 部门
constructor() {
this._chargeCode = 12
this._manager = '_manager'
}
Department(manager) {
this._manager = manager;
}
getManager() { // 经理
return this._manager;
}
}
const john = new Person()

manager = john.getDepartment().getManager();

to

getManager() {
return this._department.getManager();
}

如果客户希望知道某人的经理是谁,他必须先取得Department对象

  1. 懒惰类

    如 果一个class的所得不值其身价,它就应该消失。项目中经常会出现这样的情况: 某个class原本对得起自己的身价,但重构使它身形缩水,不再做那么多工作

  • 内联类
    同上
  • 折叠继承体系

职责

  1. 依恋情结

    某个函数为了实现其功能,经常从另一个类中获取大量数据。比起自身所在的类来说,更加依赖于另一个类,代码结构混乱,类分功不明确

  • 搬移方法
  • 搬移字段
  • 抽取方法
  1. 过渡耦合的消息链

如果一个对象依赖另一个对象,另一个对象又依赖其他对象……
代码中调用链过长,代码耦合度高,造成代码扩展或修改困难

  • 隐藏委托
  1. 中间人

    类中的函数存在过度委托给其他对象的情况,委托函数过多时,减少委托,让调用者直接访问目标类进行操作

  • 移除中间人
  • 内联方法
  • 将委托替换为继承

协调变化

  1. 发散式改变

    由于代码的各种修改或扩展,每次都要修改某个类,
    可扩展性差,某个类干的事过多。
    针对某一外界变化的所有相应修改,都只发生在单一类中。这个类内的所有内容都应该反应此变化。

  • 抽取类
  1. 霰弹式修改

    牵一发而动全身,每次遇到某种变化,都必须在不同的类中做出小修改,
    代码散步各处,不利于扩展和阅读,增加代码修改难度及工作量,
    尽量使某类变化通过某个特定类来处理,避免修改过多类。提升代码可扩展性,减少不必要的工作量。

  • 搬移方法
  • 搬移字段
  • 内联类
  1. 并行继承体系

    每当为一个类增加子类时,必须也为另一个类相应增加子类。
    让其中一个继承体系的实例引用另一个继承体系的实例,减少平行继承的类。

  • 搬移方法
  • 搬移字段

库类

不完备的库类

封装好的类库中功能不能满足实际需求

  1. 引入外来方法
  2. 引入本地扩展