auto commit
This commit is contained in:
200
notes/重构.md
200
notes/重构.md
@ -125,53 +125,115 @@
|
||||
|
||||
包括三个类:Movie、Rental 和 Customer,Rental 包含租赁的 Movie 以及天数。
|
||||
|
||||
<div align="center"> <img src="../pics//25d6d3d4-4726-47b1-a9cb-3316d1ff5dd5.png"/> </div><br>
|
||||
<div align="center"> <img src="../pics//c2f0c8e2-da66-498c-a38f-e1176abee29e.png"/> </div><br>
|
||||
|
||||
最开始的实现是把所有的计费代码都放在 Customer 类中。
|
||||
|
||||
可以发现,该代码没有使用 Customer 类中的任何信息,更多的是使用 Rental 类的信息,因此第一个可以重构的点就是把具体计费的代码移到 Rental 类中,然后 Customer 类的 getTotalCharge() 方法只需要调用 Rental 类中的计费方法即可。
|
||||
|
||||
```java
|
||||
class Customer...
|
||||
double getTotalCharge() {
|
||||
while (rentals.hasMoreElements()) {
|
||||
double thisAmount = 0;
|
||||
Rental each = (Rental) rentals.nextElement();
|
||||
switch (each.getMovie().getPriceCode()) {
|
||||
case Movie.REGULAR:
|
||||
thisAmount += 2;
|
||||
if (each.getDaysRented() > 2)
|
||||
thisAmount += (each.getDaysRented() - 2) * 1.5;
|
||||
break;
|
||||
case Movie.NEW_RELEASE:
|
||||
thisAmount += each.getDaysRented() * 3;
|
||||
break;
|
||||
case Movie.CHILDRENS:
|
||||
thisAmount += 1.5;
|
||||
if (each.getDaysRented() > 3)
|
||||
thisAmount += (each.getDaysRented() - 3) * 1.5;
|
||||
break;
|
||||
public class Customer {
|
||||
|
||||
private List<Rental> rentals = new ArrayList<>();
|
||||
|
||||
public void addRental(Rental rental) {
|
||||
rentals.add(rental);
|
||||
}
|
||||
|
||||
public double getTotalCharge() {
|
||||
double totalCharge = 0.0;
|
||||
for (Rental rental : rentals) {
|
||||
switch (rental.getMovie().getMovieType()) {
|
||||
case Movie.Type1:
|
||||
totalCharge += rental.getDaysRented();
|
||||
break;
|
||||
case Movie.Type2:
|
||||
totalCharge += rental.getDaysRented() * 2;
|
||||
break;
|
||||
case Movie.Type3:
|
||||
totalCharge += 1.5;
|
||||
totalCharge += rental.getDaysRented() * 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return totalCharge;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```java
|
||||
public class Rental {
|
||||
private int daysRented;
|
||||
|
||||
private Movie movie;
|
||||
|
||||
public Rental(int daysRented, Movie movie) {
|
||||
this.daysRented = daysRented;
|
||||
this.movie = movie;
|
||||
}
|
||||
|
||||
public Movie getMovie() {
|
||||
return movie;
|
||||
}
|
||||
|
||||
public int getDaysRented() {
|
||||
return daysRented;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用 switch 的准则是:只能在对象自己的数据上使用,而不能在另一个对象的数据基础上使用。解释如下:switch 使用的数据通常是一组相关的数据,例如上面的代码使用了 Movie 的多种类别数据。当这组类别的数据发生改变时,例如上面的代码中增加 Movie 的类别或者修改一种 Movie 类别的计费方法,就需要修改 switch 代码。如果允许违反了准则,就会有多个地方的 switch 使用了这部分的数据,那么需要打开所有的 switch 代码进行修改。
|
||||
```java
|
||||
public class Movie {
|
||||
|
||||
public static final int Type1 = 0, Type2 = 1, Type3 = 2;
|
||||
|
||||
private int type;
|
||||
|
||||
public Movie(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getMovieType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class App {
|
||||
public static void main(String[] args) {
|
||||
Customer customer = new Customer();
|
||||
Rental rental1 = new Rental(1, new Movie(Movie.Type1));
|
||||
Rental rental2 = new Rental(2, new Movie(Movie.Type2));
|
||||
customer.addRental(rental1);
|
||||
customer.addRental(rental2);
|
||||
System.out.println(customer.getTotalCharge());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
5
|
||||
```
|
||||
|
||||
使用 switch 的准则是:只使用 switch 所在类的数据。解释如下:switch 使用的数据通常是一组相关的数据,例如 getTotalCharge() 代码使用了 Movie 的多种类别数据。当这组类别的数据发生改变时,例如增加 Movie 的类别或者修改一种 Movie 类别的计费方法,就需要修改 switch 代码。如果违反了准则,就会有多个地方的 switch 使用了这部分的数据,那么这些 swtich 都需要进行修改,这些代码可能遍布在各个地方,修改工作往往会很难进行。上面的实现违反了这一准则,因此需要重构。
|
||||
|
||||
以下是继承 Movie 的多态解决方案,这种方案可以解决上述的 switch 问题,因为每种电影类别的计费方式都被放到了对应 Movie 子类中,当变化发生时,只需要去修改对应子类中的代码即可。
|
||||
|
||||
<div align="center"> <img src="../pics//76b48b4c-8999-4967-893b-832602e73285.png"/> </div><br>
|
||||
<div align="center"> <img src="../pics//41026c79-dfc1-40f7-85ae-062910fd272b.png"/> </div><br>
|
||||
|
||||
但是由于 Movie 可以在其生命周期内修改自己的类别,一个对象却不能在生命周期内修改自己所属的类,因此这种方案不可行。可以使用策略模式来解决这个问题(原书写的是使用状态模式,但是这里应该为策略模式,具体可以参考设计模式内容)。
|
||||
但是我们需要允许一部影片可以在运行过程中改变其所属的分类,但是上述的继承方案却不可行,因为一个对象所属的类在编译过程就确定了。
|
||||
|
||||
下图中,Price 有多种实现,Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。
|
||||
为了解决上述的问题,需要使用策略模式。引入 Price 类,它有多种实现。Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。
|
||||
|
||||
<div align="center"> <img src="../pics//2a842a14-e4ab-4f37-83fa-f82c206fe426.png"/> </div><br>
|
||||
<div align="center"> <img src="../pics//8c0b3ae1-1087-46f4-8637-8d46b4ae659c.png"/> </div><br>
|
||||
|
||||
重构后整体的类图和时序图如下:
|
||||
|
||||
<div align="center"> <img src="../pics//9d549816-60b7-4899-9877-23b01503ab13.png"/> </div><br>
|
||||
<div align="center"> <img src="../pics//5b910141-08b6-442d-a4bc-a1608458c636.png"/> </div><br>
|
||||
|
||||
<div align="center"> <img src="../pics//2c8a7a87-1bf1-4d66-9ba9-225a1add0a51.png"/> </div><br>
|
||||
<div align="center"> <img src="../pics//3ca58a41-8794-49c1-992e-de5d579a50d1.png"/> </div><br>
|
||||
|
||||
# 二、重构原则
|
||||
|
||||
@ -197,7 +259,7 @@ double getTotalCharge() {
|
||||
- 允许逻辑共享
|
||||
- 分开解释意图和实现
|
||||
- 隔离变化
|
||||
- 封装条件逻辑。
|
||||
- 封装条件逻辑
|
||||
|
||||
重构可以理解为在适当的位置插入间接层以及在不需要时移除间接层。
|
||||
|
||||
@ -227,14 +289,12 @@ double getTotalCharge() {
|
||||
|
||||
在编写代码时,不用对性能过多关注,只有在最后性能优化阶段再考虑性能问题。
|
||||
|
||||
应当只关注关键代码的性能,因为只有一小部分的代码是关键代码。
|
||||
应当只关注关键代码的性能,并且只有一小部分的代码是关键代码。
|
||||
|
||||
# 三、代码的坏味道
|
||||
|
||||
本章主要介绍一些不好的代码,也就是说这些代码应该被重构。
|
||||
|
||||
文中提到的具体重构原则可以先忽略。
|
||||
|
||||
## 1. 重复代码
|
||||
|
||||
> Duplicated Code
|
||||
@ -273,7 +333,7 @@ Extract Method 会把很多参数和临时变量都当做参数,可以用 Repl
|
||||
|
||||
太长的参数列表往往会造成前后不一致,不易使用。
|
||||
|
||||
面向对象程序中,函数所需要的数据通常内在宿主类中找到。
|
||||
面向对象程序中,函数所需要的数据通常能在宿主类中找到。
|
||||
|
||||
## 5. 发散式变化
|
||||
|
||||
@ -287,7 +347,7 @@ Extract Method 会把很多参数和临时变量都当做参数,可以用 Repl
|
||||
|
||||
> Shotgun Surgery
|
||||
|
||||
一个变化引起多个类修改;
|
||||
一个变化引起多个类修改。
|
||||
|
||||
使用 Move Method 和 Move Field 把所有需要修改的代码放到同一个类中。
|
||||
|
||||
@ -452,18 +512,20 @@ return anOrder.basePrice() > 1000;
|
||||
|
||||
> Replace Temp with Query
|
||||
|
||||
以临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中,将所有对临时变量的引用点替换为对新函数的调用。Replace Temp with Query 往往是 Extract Method 之前必不可少的一个步骤,因为局部变量会使代码难以提炼。
|
||||
以临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中,将所有对临时变量的引用点替换为对新函数的调用。
|
||||
|
||||
Replace Temp with Query 往往是 Extract Method 之前必不可少的一个步骤,因为局部变量会使代码难以提炼。
|
||||
|
||||
```java
|
||||
double basePrice = quantity * itemPrice;
|
||||
if(basePrice > 1000)
|
||||
if (basePrice > 1000)
|
||||
return basePrice * 0.95;
|
||||
else
|
||||
return basePrice * 0.98;
|
||||
```
|
||||
|
||||
```java
|
||||
if(basePrice() > 1000)
|
||||
if (basePrice() > 1000)
|
||||
return basePrice() * 0.95;
|
||||
else
|
||||
return basePrice() * 0.98;
|
||||
@ -478,10 +540,10 @@ double basePrice(){
|
||||
|
||||
> Introduce Explaining Variable
|
||||
|
||||
将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
|
||||
将复杂表达式(或其中一部分)的结果放进一个临时变量, 以此变量名称来解释表达式用途。
|
||||
|
||||
```java
|
||||
if((platform.toUpperCase().indexOf("MAC") > -1) &&
|
||||
if ((platform.toUpperCase().indexOf("MAC") > -1) &&
|
||||
(browser.toUpperCase().indexOf("IE") > -1) &&
|
||||
wasInitialized() && resize > 0) {
|
||||
// do something
|
||||
@ -493,7 +555,7 @@ final boolean isMacOS = platform.toUpperCase().indexOf("MAC") > -1;
|
||||
final boolean isIEBrower = browser.toUpperCase().indexOf("IE") > -1;
|
||||
final boolean wasResized = resize > 0;
|
||||
|
||||
if(isMacOS && isIEBrower && wasInitialized() && wasResized) {
|
||||
if (isMacOS && isIEBrower && wasInitialized() && wasResized) {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
@ -513,14 +575,18 @@ if(isMacOS && isIEBrower && wasInitialized() && wasResized) {
|
||||
以一个临时变量取代对该参数的赋值。
|
||||
|
||||
```java
|
||||
int discount (int inputVal, int quentity, int yearToDate){
|
||||
int discount (int inputVal, int quentity, int yearToDate) {
|
||||
if (inputVal > 50) inputVal -= 2;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
int discount (int inputVal, int quentity, int yearToDate){
|
||||
int discount (int inputVal, int quentity, int yearToDate) {
|
||||
int result = inputVal;
|
||||
if (inputVal > 50) result -= 2;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 以函数对象取代函数
|
||||
@ -572,18 +638,18 @@ int discount (int inputVal, int quentity, int yearToDate){
|
||||
建立所需的函数,隐藏委托关系。
|
||||
|
||||
```java
|
||||
class Person{
|
||||
class Person {
|
||||
Department department;
|
||||
|
||||
public Department getDepartment(){
|
||||
public Department getDepartment() {
|
||||
return department;
|
||||
}
|
||||
}
|
||||
|
||||
class Department{
|
||||
class Department {
|
||||
private Person manager;
|
||||
|
||||
public Person getManager(){
|
||||
public Person getManager() {
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
@ -598,7 +664,7 @@ Person manager = john.getDepartment().getManager();
|
||||
通过为 Peron 建立一个函数来隐藏这种委托关系。
|
||||
|
||||
```java
|
||||
public Person getManager(){
|
||||
public Person getManager() {
|
||||
return department.getManager();
|
||||
}
|
||||
```
|
||||
@ -651,7 +717,7 @@ Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受
|
||||
|
||||
以 Change Value to Reference 相反。值对象有个非常重要的特性:它是不可变的,不可变表示如果要改变这个对象,必须用一个新的对象来替换旧对象,而不是修改旧对象。
|
||||
|
||||
需要为值对象实现 equals() 和 hashCode() 方法
|
||||
需要为值对象实现 equals() 和 hashCode() 方法。
|
||||
|
||||
## 5. 以对象取代数组
|
||||
|
||||
@ -667,7 +733,7 @@ Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受
|
||||
|
||||
一些领域数据置身于 GUI 控件中,而领域函数需要访问这些数据。
|
||||
|
||||
将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用以同步领域对象和 GUI 对象内的重复数据。
|
||||
将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用于同步领域对象和 GUI 对象内的重复数据。
|
||||
|
||||
<div align="center"> <img src="../pics//e024bd7e-fb4e-4239-9451-9a6227f50b00.jpg" width=""/> </div><br>
|
||||
|
||||
@ -680,10 +746,10 @@ Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受
|
||||
有两个类,分别为订单 Order 和客户 Customer,Order 引用了 Customer,Customer 也需要引用 Order 来查看其所有订单详情。
|
||||
|
||||
```java
|
||||
class Order{
|
||||
class Order {
|
||||
private Customer customer;
|
||||
public void setCustomer(Customer customer){
|
||||
if(this.customer != null)
|
||||
public void setCustomer(Customer customer) {
|
||||
if (this.customer != null)
|
||||
this.customer.removeOrder(this);
|
||||
this.customer = customer;
|
||||
this.customer.add(this);
|
||||
@ -691,12 +757,12 @@ class Order{
|
||||
}
|
||||
```
|
||||
```java
|
||||
class Curstomer{
|
||||
class Curstomer {
|
||||
private Set<Order> orders = new HashSet<>();
|
||||
public void removeOrder(Order order){
|
||||
public void removeOrder(Order order) {
|
||||
orders.remove(order);
|
||||
}
|
||||
public void addOrder(Order order){
|
||||
public void addOrder(Order order) {
|
||||
orders.add(order);
|
||||
}
|
||||
}
|
||||
@ -716,7 +782,7 @@ class Curstomer{
|
||||
|
||||
> Replace Magic Number with Symbolic Constant
|
||||
|
||||
创建一个常量,根据其意义为它命名,并将字面常量换位这个常量。
|
||||
创建一个常量,根据其意义为它命名,并将字面常量换为这个常量。
|
||||
|
||||
## 10. 封装字段
|
||||
|
||||
@ -777,15 +843,17 @@ public 字段应当改为 private,并提供相应的访问函数。
|
||||
对于一个复杂的条件语句,可以从 if、then、else 三个段落中分别提炼出独立函数。
|
||||
|
||||
```java
|
||||
if(data.befor(SUMMER_START) || data.after(SUMMER_END))
|
||||
if (data.befor(SUMMER_START) || data.after(SUMMER_END))
|
||||
charge = quantity * winterRate + winterServiceCharge;
|
||||
else charge = quantity * summerRate;
|
||||
else
|
||||
charge = quantity * summerRate;
|
||||
```
|
||||
|
||||
```java
|
||||
if(notSummer(date))
|
||||
if (notSummer(date))
|
||||
charge = winterCharge(quantity);
|
||||
else charge = summerCharge(quantity);
|
||||
else
|
||||
charge = summerCharge(quantity);
|
||||
```
|
||||
|
||||
## 2. 合并条件表达式
|
||||
@ -797,7 +865,7 @@ else charge = summerCharge(quantity);
|
||||
将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
|
||||
|
||||
```java
|
||||
double disabilityAmount(){
|
||||
double disabilityAmount() {
|
||||
if (seniority < 2) return 0;
|
||||
if (monthsDisabled > 12 ) return 0;
|
||||
if (isPartTime) return 0;
|
||||
@ -805,7 +873,7 @@ double disabilityAmount(){
|
||||
}
|
||||
```
|
||||
```java
|
||||
double disabilityAmount(){
|
||||
double disabilityAmount() {
|
||||
if (isNotEligibleForDisability()) return 0;
|
||||
// ...
|
||||
}
|
||||
@ -820,7 +888,7 @@ double disabilityAmount(){
|
||||
将这段重复代码搬移到条件表达式之外。
|
||||
|
||||
```java
|
||||
if (isSpecialDeal()){
|
||||
if (isSpecialDeal()) {
|
||||
total = price * 0.95;
|
||||
send();
|
||||
} else {
|
||||
@ -844,7 +912,7 @@ send();
|
||||
|
||||
在一系列布尔表达式中,某个变量带有“控制标记”的作用。
|
||||
|
||||
用 break 语 句或 return 语句来取代控制标记。
|
||||
用 break 语句或 return 语句来取代控制标记。
|
||||
|
||||
## 5. 以卫语句取代嵌套条件表达式
|
||||
|
||||
@ -1027,7 +1095,7 @@ void setWidth(int arg){
|
||||
```java
|
||||
int low = daysTempRange().getLow();
|
||||
int high = daysTempRange().getHigh();
|
||||
withinPlan = plan.withinRange(low,high);
|
||||
withinPlan = plan.withinRange(low, high);
|
||||
```
|
||||
|
||||
```java
|
||||
@ -1096,12 +1164,12 @@ double finalPrice = discountedPrice (basePrice);
|
||||
将向下转型动作移到函数中。
|
||||
|
||||
```java
|
||||
Object lastReading(){
|
||||
Object lastReading() {
|
||||
return readings.lastElement();
|
||||
}
|
||||
```
|
||||
```java
|
||||
Reading lastReading(){
|
||||
Reading lastReading() {
|
||||
return (Reading)readings.lastElement();
|
||||
}
|
||||
```
|
||||
|
Reference in New Issue
Block a user