代码重构内容整理完成

This commit is contained in:
Estom
2021-04-11 22:12:16 +08:00
parent bf5e2c34f1
commit 49325d3bdd
27 changed files with 1497 additions and 20 deletions

View File

View File

@@ -17,6 +17,8 @@
- [ ] 设计模式有道云笔记源网址gitee设计模式库
- [ ] effective 系列
- [ ] 系列视频(知乎上收藏的内容)
- [ ] 狂神java、linux系列
- [ ] C++ coding系列
- [ ] 看大佬写C++。(chermo)
- [ ] 问题专项解决
- Java四周

View File

@@ -3,6 +3,7 @@
- [x] 设计模式复习
- [ ] vscode C++集成开发环境
- [ ] cmake/vs 项目重构
- [ ] 项目重构计划书完成。(等有时间再搞)
- [ ] 制定四月份论文阅读计划

View File

@@ -92,8 +92,7 @@
### **对象作为参数**
一些模式引入总是被用作参数的对象。例如一个 Visitor 对象是一个多态的 Accept
操作的参数。
一些模式引入总是被用作参数的对象。例如一个 Visitor 对象是一个多态的 Accept操作的参数。
一些模式定义一些可作为令牌到处传递的对象。例如 Command 代表一个请求Memento
代表一个对象在某个时刻的内部状态。

View File

@@ -0,0 +1,22 @@
# 代码重构的常用方法 (C++实现)
## 0 概述
### 重构定义
重构是在软件开发中改善已有代码的一种方法通过代码重构可以改进软件的设计、使得软件更容易理解、有利于Bug的发现以及提高后续开发效率。Martin Fowler在《重构改善既有代码的设计》一书中对“重构”提供了两种形式的定义
1. 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
2. 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
### 重构时机
对于何时重构Martin Fowler认为不能为重构而重构应该在 **“当你想做别的事情,而重构可以帮助你把那些事情做好”** 时,才去进行重构。并给出了四个常见的重构时机:
1. 事不过三,三则重构;
2. 添加功能时重构;
3. 修补错误时重构;
4. 复审代码时重构。
对于何时不应该重构Martin Fowler认为如果“既有代码是在太混乱重构它还不如重新写一个简单”时就应该重写而不是重构。重写的一个清晰的信号是现有代码根本不能正常运作。
重构既是一件脑力活,也是一件体力活。特别是在软件中的“代码坏味道”积累到一定量时,就需要花费大量的时间和精力才能完成对代码的重构。因此,需要加强对“代码坏味道”的嗅觉,在发现“代码坏味道”时及时地去重构,通过不断的微重构来阻止代码架构的腐化,从而避免走上代码重写的艰难之路。
本文主要对《重构》一书中提到的一些常见的“代码坏味道”和对应重构手法的总结。

View File

@@ -0,0 +1,151 @@
# 1 代码坏味道
## 1 Duplicated Code重复代码
### 现象
1. 同一个类的两个函数含有相同的表达式。
2. 两个互为兄弟的子类内含相同的表达式(可能其中对应的某些函数以相同顺序执行类似的操作,但在各个操作的细节上有所不同)。
### 重构手法
1. Extract Method
2. Extract Method->Pull Up Method (->Form Template Method)
## 2 Long Method过长函数
### 现象
1. 一个函数所承担的事情太多,导致代码行数过长(可能有临时变量存在)。
### 重构手法
2. Extract Method (->Replace Temp with Query)
## 3 Large Class过大的类
### 现象
1. 一个类做了太多的事情,不符合单一职责原则,导致代码过多。
### 重构手法
1. Extract Class
## 4 Long Parameter List过长参数列
### 现象
总体表现为一个函数的参数列表太长,可能伴随着以下两种现象:
1. 向已有的对象发出一条请求就可以取代一个参数。
2. 某些参数数据缺乏合理的对象归属。
### 重构手法
1. Replace Parameter with Explicit Method
2. Introduce Parameter Object
## 5 Divergent Change发散式变化
### 现象
1. 某个类经常因为不同的原因在不同的方向上发生变化。
## 重构手法
1. Extract Class
## 6 Shotgun Surgery霰弹式修改
### 现象
1. 每遇到某种变化,都必须在许多不同的类内作出许多小修改。
### 重构手法
1. Move Method->Move Field
## 7 Feature Envy依恋情结
### 现象
1. 函数对某个类的兴趣高过对所驻类的兴趣。
2. 函数中的一部分对某个类的兴趣高过对所驻类的兴趣。
### 重构手法
1. Move Method
2. Extract Method->Move Method
## 8 Data Clumps数据泥团
### 现象
1. 多个类中有这相同的字段。
2. 多个函数的签名中有着相同的字段。
### 重构手法
1. Extract Class
2. Extract Class->Introduce Parameter Object
## 9 Primitive Obsession基本类型偏执
### 现象
总体表现为以基本类型表示一些具有业务性质的概念,又分成两种情况:
1. 基本类型都是独立的出现
2. 几个基本都行总是一起出现(可能出现在函数参数列表中)
### 重构手法
1. Replace Data Value with Object
2. Extract Class (->Introduce Parameter Object)
## 10 Switch Statementsswitch惊悚现身
### 现象
代码中出现switch表达式分以下两种场景
1. 根据类型码来选择不同的行为。
2. 只是单一函数中有些选择事例可能选择条件之一是NULL
### 重构手法
1. Extract Method->Move Method->Replace Conditional with Polymorphism
2. Replace Parameter with Explicit Method (->Introduce Null Object)
## 11 Parallel Inheritance Hierarchies平行继承体系
### 现象
1. 每当为某个类增加一个子类也必须为另一个类相应增加一个子类Shotgun Surgery的特殊情况
### 重构手法
1. Move Method->Move Field
## 12 Lazy Class冗赘类
### 现象
某个类在重构后变得冗余,或者这个类是为未来变化服务的,当前并没有用。可能属于以下两种场景:
1. 属于继承体系
2. 属于组合体系
### 重构手法
1. Collapse Hierarchy
2. Inline Class
## 13 Speculative Generality夸夸其谈未来性
### 现象
某个类是为未来变化服务的,当前并没有用。可能属于以下两种场景:
1. 属于继承体系
2. 属于组合体系
### 重构手法
1. Collapse Hierarchy
2. Inline Class
## 14 Temporary Field令人迷惑的临时字段
### 现象
1. 在一个类中,某个字段只在特定的情况下才有用,其余情况下容易令人迷惑。
### 重构手法
1. Extract Class (将该字段以及使用到它的代码提取到一个新的类中,使得代码高内聚)
## 15 Message Chains过度耦合的消息链
### 现象
1. 向一个对象请求另一个对象,然后在向后者请求另一个对象,…
### 重构手法
2. Hide Delegate
## 16 Middle Man中间人
### 现象
1. 某个类接口有一半的函数都委托给其他类。
### 重构手法
1. Remove Middle Man
## 17 Inappropriate Intimacy不适当的亲密关系
### 现象
1. 两个类过于亲密花费太多时间去探究彼此的private成分。
### 重构手法
有以下两种重构思路:
1. Move Method->Move Field
2. Extract Class (将两者共同点提炼到一个新的类中)
## 18 Data Class纯数据类
### 现象
1. 某些类拥有一些字段以及对应的getter/setter函数除此之外一无长处。这些类通常被其他类过分琐碎的操控者。
2. 只有getter/setter函数没有业务行为函数的类就是DDD中提到的贫血模型。在软件开发过程中除非框架要求应该杜绝贫血模型的出现。
### 重构手法
1. Remove Setting Method对那些不该被其他类修改的字段使用->Move Method把在其他类上的调用行为搬移到数据类中
## 19 Comments过多的注释
### 现象
1. 如果一段代码有着长长的注释,那么就会发现,这些注释之所以存在是因为代码很糟糕,注释本身并不是一种坏味道。
### 重构手法
1. Extract Method

View File

@@ -0,0 +1,210 @@
## 2 重新组织函数
## 1 Extract Method提炼函数
### 概念
Extract Method应该是最常用的重构手法了当遇到一个过长的代码或者需要添加注释才能让人理解其用途的代码时就可以运用Extract Method将代码提炼到一个函数上。该方法的一个重点时函数命名只有给函数起个适当的名字时他们才能真正起作用。对于是否使用该重构手法代码长度不是问题关键在于函数名称和函数本体之间的语义距离。
如果提炼可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。
### 重构实例1
```C++
// 重构前
void PrintOwing(double amount)
{
PrintBanner();
// print details
cout << "name: " << m_name << endl;
cout << "amount: " << amount << endl;
}
```
```C++
// 重构后
void PrintOwing(double amount)
{
PrintBanner();
PrintDetails(amount);
}
void PrintDetails(double amount)
{
cout << "name: " << m_name << endl;
cout << "amount: " << amount << endl;
}
```
当重构的代码中带有临时变量时可以将局部变量作为提炼函数的入参参见重构示例1。如果涉及到对临时变量的再赋值可能还需要返回处理结果参见重构示例2
### 重构实例2
```C
// 重构前
void PrintOwing(double amount)
{
PrintBanner();
double outstanding = amount * 1.2;
for (auto& order : m_orders) {
outstanding += order.Amount();
}
PrintDetails(outstanding);
}
```
```C
// 重构后
void PrintOwing(double amount)
{
PrintBanner();
double outstanding = amount * 1.2;
outstanding = CalOutstanding(outstanding);
PrintDetails(outstanding);
}
double CalOutstanding(double val)
{
double result = val;
for (auto& order : m_orders) {
result += order.Amount();
}
return result;
}
```
在临时变量过多的情况下可以采用Replace Temp with Query减少临时变量。
## 2 Replace Temp with Query以查询取代临时变量
由于临时变量只能在所属函数内可见因此会驱使你写出更长的函数。如果把临时变量替换为一个查询函数那么同一个类中的所有函数都将可以获得这份信息。除此之外Replace Temp with Query还可以使代码可维护性更好如果临时变量的计算方式改变了只需修改查询函数即可。
### 重构示例3
```C
// 重构前
double basePrice = m_quantity + m_itemPrice;
if (basePrice > 1000)
{
return basePrice * 0.95;
} else
{
return basePrice * 0.98;
}
```
```C
// 重构后
if (BasePrice() > 1000)
{
return BasePrice() * 0.95;
} else
{
return BasePrice() * 0.98;
}
double BasePrice()
{
return m_quantity + m_itemPrice;
}
```
## 3 Introduce Explaining Variable引入解释性变量
Introduce Explaining Variable也是一个很常用的重构手法如果表达式非常复杂而难以阅读引入临时变量可以将表达式分解成容易管理的形式可读性更强。该手法在条件逻辑中特别有价值——将每个条件子句提炼出来以一个良好命名的临时变量来解释对应条件子句的意义。
### 重构示例4
```C
// 重构前
if ((platform.find("MAC") != platform.end()) &&
(browser.find("IE") != browser.end()) &&
IsInit() && resize > 0) {
// do something
}
```
```C
// 重构后
bool isMacOs = platform.find("MAC") != platform.end();
bool isIEBrowser = browser.find("IE") != browser.end();
bool isResized = resize > 0;
if (isMacOs && isIEBrowser && IsInit() && isResized) {
// do something
}
```
## 4 Split Temporary Variable分解临时变量
如果有某个临时变量被赋值超过一次则可以使用Split Temporary Variable进行重构针对每一次赋值创造一个独立、对应的临时变量。
### 重构示例5
```C
// 重构前
double temp = 2 * (m_height + m_width);
cout << temp << endl;
temp = m_height * m_width;
cout << temp << endl;
// 重构后
double perimeter = 2 * (m_height + m_width);
cout << perimeter << endl;
double area = m_height * m_width;
cout << area << endl;
```
在软件开发过程中应该避免使用temp/tmp作为变量名。
## 5 Replace Method with Method Object以函数对象取代函数
对于一个大型的函数如果有太多的临时变量导致无法采用Extract Method而采用Replace Temp with Query
又产生过多的查询函数时就可以采用Replace Method with Method Object对函数进行重构。
将函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解成多个小型函数。
### 重构示例6
```C
// 重构前
class Account {
...
int GetGamma(int inputVal, int quantity, int yearToDate)
{
int importantVal1 = (inputVal * quantity) + Delta();
int importantVal2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantVal1) > 100) {
importantVal2 -= 20;
}
int importantVal3 = importantVal2 * 7;
...
return importantVal3 - 2 * importantVal1;
}
}
```
```C
// 重构后
class Account {
// ...
int GetGamma(int inputVal, int quantity, int yearToDate)
{
Gamma gamma(this, inputVal, quantity, yearToDate);
return gamma.Compute();
}
}
class Gamma {
public:
Gamma(Account* account, int inputVal, int quantity, int yearToDate);
int Compute()
{
importantVal1 = (inputVal * quantity) + account->Delta();
importantVal2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantVal1) > 100) {
importantVal2 -= 20;
}
importantVal3 = importantVal2 * 7;
// ...
return importantVal3 - 2 * importantVal1;
}
private:
Account* account;
int inputVal;
int quantity;
int yearToDate;
int importantVal1;
int importantVal2;
int importantVal3;
}
```

View File

@@ -0,0 +1,212 @@
# 3 在对象之间搬移特性
## 1 Move Method搬移函数
如果某个函数使用另一个对象的次数比使用自己所驻对像的次数多就可以使用Move Method将函数搬移到另一个对象。当函数中用到了原来对象的属性时搬移后可以把原来对象this作为入参。
### 重构示例7
```C
// 重构前
class Account {
public:
double OverdraftCharge()
{
if (m_type->IsPremium()) {
double result = 10;
if (m_dayOverdrawn > 7) {
result += (m_dayOverdrawn - 7) * 0.85;
}
return result;
}
return m_dayOverdrawn * 1.75;
}
double BankCharge()
{
double result = 4.5;
if (m_dayOverdrawn > 0) {
result += OverdraftCharge();
}
return result;
}
private:
AccountType* m_type;
int m_dayOverdrawn;
}
```
```C
// 重构后
class Account {
public:
double BankCharge()
{
double result = 4.5;
if (m_dayOverdrawn > 0) {
result += m_type->OverdraftCharge(this);
}
return result;
}
private:
AccountType* m_type;
int m_dayOverdrawn;
}
class AccountType {
public:
double OverdraftCharge(Account* account)
{
if (IsPremium()) {
double result = 10;
if (account->GetDayOverdrawn() > 7) {
result += (account->GetDayOverdrawn() - 7) * 0.85;
}
return result;
}
return account->GetDayOverdrawn() * 1.75;
}
}
```
## 2 Move Field搬移字段
与Move Method类似对于一个字段如果在其所驻类之外的另一个类中有更多函数使用了它就可以使用Move Field将该字段搬移到另一个对象中。需要注意的是搬移之后需要在新的类中提供该字段的访问函数否则原对象就无法访问到该字段了。
## 3 Extract Class提炼类
当一个类所承担的责任太多时就可以使用Extract Class手法根据单一职责原则将类的一些函数和字段抽离出来封装成一个新的类。
### 重构示例8
```C
// 重构前
class Person {
public:
string GetName()
{
return m_name;
}
string GetTelephoneNumber()
{
return "(" + m_officeAreaCode + ") " + m_officeNumber;
}
string GetOfficeAreaCode()
{
return m_officeAreaCode;
}
string GetOfficeNumber()
{
return m_officeNumber;
}
private:
string m_name;
string m_officeAreaCode;
string m_officeNumber;
}
```
```C
// 重构后
class Person {
public:
string GetName()
{
return m_name;
}
string GetTelephoneNumber()
{
return m_officeTelephone->GetTelephoneNumner();
}
TelephoneNumber* GetOfficeTelephone()
{
return m_officeTelephone;
}
private:
string m_name;
TelephoneNumber* m_officeTelephone;
}
class TelephoneNumber {
public:
string GetTelephoneNumner()
{
return "(" + m_areaCode + ") " + m_number;
}
string GetAreaCode()
{
return m_areaCode;
}
string GetNumber()
{
return m_number;
}
private:
string m_areaCode;
string m_number;
}
```
## 4 Inline Class将类内联化
Inline Class与Extract Class相反如果某个类所做的事情太少以至于不值得作为一个类存在则将这个类的所有属性/函数搬移到另一个类中,然后删除原来的类。
## 5 Hide Delegate隐藏“委托关系”
Hide Delegate可以防止信息泄漏避免模块之间的耦合。如果某个Client先通过服务对象A的字段得到另一个对象B然后调用对象B的函数那么Client就必须知晓这一层的委托关系。这时就产生了信息的泄漏对象A将对象B泄漏给了Client。如果委托关系变了Client也必须跟着改变这样模块之间的耦合就太多了。
在服务类上建立Client所需的所有函数可以隐藏委托关系。
![](image/2021-04-11-21-59-29.png)
### 重构示例9
```C
// 重构前
class Person {
public:
Department* GetDepartment()
{
return m_department;
}
private:
Department* m_department;
}
class Department {
public:
Person* GetManager()
{
return m_manager;
}
private:
Person* m_manager;
}
// 如果需要知道Person* john的经理是谁必须先取得Department对象
manager = john->GetDepartment()->GetManager();
```
```C
// 重构后
class Person {
public:
Person* GetManager()
{
return m_department->GetManager();
}
private:
Department* m_department;
}
class Department {
public:
Person* GetManager()
{
return m_manager;
}
private:
Person* m_manager;
}
// 现在可以通过Person* john直接知道经理了
manager = john->GetManager();
```
## 6 Remove Middle Man移除中间人
该重构手法与Hide Delegate刚好相反如果某个类做了过多的简单委托动作则把该类移除让客户端直接调用受托类。

View File

@@ -0,0 +1,201 @@
# 4 重新组织数据
## 1 Self Encapsulate Field自封装字段
直接访问一个字段后面如果与该字段的耦合关系逐渐变得笨拙时可以使用Self Encapsulate Field手法进行重构为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
### 重构示例10
```
// 重构前
class Range {
public:
bool Includes(int arg)
{
return arg >= m_low && arg <= m_high;
}
private:
int m_low;
int m_high;
}
```
```
// 重构后
class Range {
public:
bool Includes(int arg)
{
return arg >= GetLow() && arg <= GetHigh();
}
private:
int GetLow()
{
return m_low;
}
int GetHigh()
{
return m_high;
}
private:
int m_low;
int m_high;
}
```
应该更多的赋予对象业务行为函数而不是过多的使用getter/setter函数否则对象就会变成领域驱动设计里面所提到的贫血模型。
## 2 Replace Data Value with Object以对象取代数据值
软件开发初期你往往会以简单的基本类型来表示某一概念随着开发的迭代这些概念不再是简单的基本类型就能表示的这时就需要使用Replace Data Value with Object进行重构封装一个新的对象来取代原有的基本类型数据值。比如重构示例8中刚开始使用一个字符串来表示“电话号码”的概念但随后就会发现电话号码需要“格式化”、“抽取区号”之类的特殊行为这些就需要将“电话号码”封装成一个对象了。
![](image/2021-04-11-22-02-22.png)
> 更好的方法是在设计阶段对通用语言进行领域建模,并赋予对象业务行为函数,这样可以使复杂的系统更加清晰。
## 3 Replace Type Code with Subclasses以子类取代类型码
如果一个类中有一个不可变的类型码而且它会影响到类的行为这时就可以以子类来取代这个类型码。一般来说这种情况的标志就是出现switch或if/else结构它们检查类型码的值并根据不同的值执行不同的动作。
Replace Type Code with Subclasses的好处在于它把“对不同行为的了解”从类用户转移到了类自身。如果需要加入新的行为变化只需添加一个子类即可。如果没有多态机制就必须找到所有的条件表达式并逐一修改它们。
![](image/2021-04-11-22-02-10.png)
### 重构示例11
```C
// 重构前
class Employee {
public:
Employee(int type) : m_type(type) {}
int GetType()
{
return m_type;
}
public:
static constexpr int ENGINEER = 0;
static constexpr int SALESMAN = 1;
static constexpr int MANAGER = 2;
private:
int m_type;
}
```
```C
// 重构后
class Employee {
public:
static Employee* Create(int type)
{
switch (type) {
case ENGINEER:
return new Engineer;
case SALESMAN:
return new Salesman;
case MANAGER:
return new Manager;
default:
return nullptr;
}
}
virtual int GetType() = 0;
private:
Employee() = default;
public:
static constexpr int ENGINEER = 0;
static constexpr int SALESMAN = 1;
static constexpr int MANAGER = 2;
}
class Engineer : public Employee {
public:
int GetType() override
{
return Employee::ENGINEER;
}
}
```
## 4 Replace Type Code with State/Strategy以State/Strategy取代类型码
如果一个类中有一个不可变的类型码而且它会影响到类的行为但是无法通过继承手法消除它时就可以使用Replace Type Code with State/Strategy进行重构。如果是通过重构来简化一个算法则Strategy模式比较合适如果是打算搬移与状态相关的数据而且把新建对象视为一种变迁状态则State状态比较合适。
![](image/2021-04-11-22-01-58.png)
### 重构示例12
```
// 重构前
class Employee {
public:
Employee(int type) : m_type(type) {}
int GetType()
{
return m_type;
}
int PayAmount()
{
switch (m_type) {
case ENGINEER:
return m_monthlySalary;
case SALESMAN:
return m_monthlySalary + m_commission;
case MANAGER:
return m_monthlySalary + m_bonus;
default:
return -1;
}
}
public:
static constexpr int ENGINEER = 0;
static constexpr int SALESMAN = 1;
static constexpr int MANAGER = 2;
private:
int m_type;
}
```
```C
// 重构后
class Employee {
public:
Employee(int type) : m_type(EmployeeType::ValueOf(type)) {}
int GetType()
{
return m_type->GetTypeCode();
}
int PayAmount()
{
switch (GetType()) {
case EmployeeType::ENGINEER:
return m_monthlySalary;
case EmployeeType::SALESMAN:
return m_monthlySalary + m_commission;
case EmployeeType::MANAGER:
return m_monthlySalary + m_bonus;
default:
return -1;
}
}
private:
EmployeeType* m_type;
}
class EmployeeType {
public:
static EmployeeType* ValueOf(int code)
{
switch (code) {
case ENGINEER:
return new Engineer;
case SALESMAN:
return new Salesman;
case MANAGER:
return new Manager;
default:
return nullptr;
}
}
virtual int GetTypeCode() = 0;
public:
static constexpr int ENGINEER = 0;
static constexpr int SALESMAN = 1;
static constexpr int MANAGER = 2;
}
class Engineer : public EmployeeType {
public:
int GetTypeCode() override
{
return EmployeeType::ENGINEER;
}
}
```

View File

@@ -0,0 +1,287 @@
# 5 简化条件表达式
## 1 Decompose Conditional分解条件表达式
复杂的条件逻辑会降低代码的可读性通过从if/else if/else三个段落中分别提炼出独立的函数根据每一段落的用途命名函数从而更清晰地表达自己的意图。
### 重构示例13
```
// 重构前
if (date.Before(SUMMER_START) || date.After(SUMMER_END)) {
charge = quantity * m_winterRate + m_winterServiceCharge;
} else {
charge = quantity * m_summerRate;
}
```
```C
// 重构后
if (NotSummer(date)) {
charge = WinterCharge(quantity);
} else {
charge = SummerCharge(quantity);
}
bool NotSummer(Date date)
{
return date.Before(SUMMER_START) || date.After(SUMMER_END)
}
int WinterCharge(quantity)
{
return quantity * m_winterRate + m_winterServiceCharge;
}
int SummerCharge(quantity)
{
return quantity * m_summerRate;;
}
```
## 2 Consolidate Conditional Expression合并条件表达式
有时候一系列的条件分支都得到相同的结果可以用Consolidate Conditional Expression手法将这些条件分支合为一个条件表达式并提炼成一个独立的函数。
### 重构示例14
```
// 重构前
double DisabilityAmount()
{
if (m_seniority < 2) {
return 0;
}
if (m_monthsDisabled > 12) {
return 0;
}
if (m_isPartTime) {
return 0;
}
// ...
}
```
```C
// 重构后
double DisabilityAmount()
{
if (IsNotEligibleForDisability()) {
return 0;
}
// ...
}
bool IsNotEligibleForDisability()
{
return m_seniority < 2 || m_monthsDisabled > 12 || m_isPartTime;
}
```
## 3 Consolidate Duplicate Conditional Fragments合并重复的条件片段
如果在条件表达式的每个分支上有着相同的一段代码可以使用Consolidate Duplicate Conditional Fragments将这段重复代码搬移到条件表达式之外。
### 重构示例15
// 重构前
ifIsSpecialDeal()) {
total = price * 0.95;
Send();
} else {
total = price * 0.98;
Send();
}
```
```C
// 重构后
ifIsSpecialDeal()) {
total = price * 0.95;
} else {
total = price * 0.98;
}
Send();
```
## 4 Remove Control Flag (移除控制标记)
在一系列的条件表达式中常常存在一个用于判断何时停止条件检查的控制标志。这源于结构化编程原则的“每个子程序只能有一个入口和一个出口”但这样也降低了代码的可读性。可以通过break/continue/return来替换掉控制标志提升代码可读性。
### 重构示例16
```
// 重构前
void CheckSecurity(vector<string>& peoples)
{
bool isFound = false;
for (auto& people : peoples) {
if (!isFound) {
if (people == "Don") {
SendAlert();
isFound = true;
}
if (people == "John") {
SendAlert();
isFound = true;
}
}
}
}
```
```
// 重构后
void CheckSecurity(vector<string>& peoples)
{
for (auto& people : peoples) {
if (people == "Don") {
SendAlert();
break;
}
if (people == "John") {
SendAlert();
break;
}
}
}
```
## 5 Replace Nested Conditional with Guard Clauses以卫语句取代嵌套条件表达式
嵌套的if/else语句式造成代码可读性差的罪魁祸首之一它让人难以看清正常的执行路径。这时可以通过使用卫语句表现所有特殊情况最常见的就是对条件进行反转来消除嵌套的条件表达式提高代码可读性。
### 重构示例17
```
// 重构前
double GetAdjustedCapital()
{
double result = 0.0;
if (m_capital > 0.0) {
if (m_intRate > 0.0 && m_duration > 0.0) {
result = (m_income / m_duration) * ADJ_FACTOR;
}
}
return result;
}
```
```C
// 重构后
double GetAdjustedCapital()
{
if (m_capital <= 0.0) {
return 0.0;
}
if (m_intRate <= 0.0 || m_duration <= 0.0) {
return 0.0;
}
return (m_income / m_duration) * ADJ_FACTOR;
}
```
## 6 Replace Conditional with Polymorphism以多态取代条件表达式
该手法有点类似于Replace Type Code with Subclasses如果有个条件表达式根据类型码的不同而选择不同的行为。这时可以通过Replace Conditional with Polymorphism将这个条件表达式的每一个分支放进一个子类内的覆写函数中然后将原市函数声明为抽象函数。继续以重构示例12中的代码示例为例子我们采用Move Method将PayAmount()函数迁移到EmployeeType并以多态来取代在其中的switch语句。
### 重构示例18
```
// 重构前
class Employee {
public:
Employee(int type) : m_type(EmployeeType::ValueOf(type)) {}
int GetType()
{
return m_type->GetTypeCode();
}
int PayAmount()
{
switch (GetType()) {
case EmployeeType::ENGINEER:
return m_monthlySalary;
case EmployeeType::SALESMAN:
return m_monthlySalary + m_commission;
case EmployeeType::MANAGER:
return m_monthlySalary + m_bonus;
default:
return -1;
}
}
private:
EmployeeType* m_type;
}
class EmployeeType {
public:
static EmployeeType* ValueOf(int code)
{
switch (code) {
case ENGINEER:
return new Engineer;
case SALESMAN:
return new Salesman;
case MANAGER:
return new Manager;
default:
return nullptr;
}
}
virtual int GetTypeCode() = 0;
public:
static constexpr int ENGINEER = 0;
static constexpr int SALESMAN = 1;
static constexpr int MANAGER = 2;
}
class Engineer : public EmployeeType {
public:
int GetTypeCode() override
{
return EmployeeType::ENGINEER;
}
}
```
```C
// 重构后
class Employee {
public:
Employee(int type) : m_type(EmployeeType::ValueOf(type)) {}
int GetType()
{
return m_type->GetTypeCode();
}
int PayAmount()
{
return m_type->PayAmount();
}
private:
EmployeeType* m_type;
}
class EmployeeType {
public:
static EmployeeType* ValueOf(int code)
{
switch (code) {
case ENGINEER:
return new Engineer;
case SALESMAN:
return new Salesman;
case MANAGER:
return new Manager;
default:
return nullptr;
}
}
virtual int GetTypeCode() = 0;
virtual int PayAmount() = 0;
public:
static constexpr int ENGINEER = 0;
static constexpr int SALESMAN = 1;
static constexpr int MANAGER = 2;
}
class Engineer : public EmployeeType {
public:
int GetTypeCode() override
{
return EmployeeType::ENGINEER;
}
int PayAmount() override
{
return m_monthlySalary;
}
}
```
## 7 Introduce Null Object引入Null对象
引入Null对象主要是为了消除随处可见的判空逻辑通过新建一个Null对象并在原来返回Null的地方改成返回新建的Null对象。
![](image/2021-04-11-22-05-41.png)
> Java 8中新增了一个Optional接口相对于新建一个Null对象更推荐使用Optional除了可以表示Null对象的语义之外它还提供了很多很强大的功能。C++14中也新增了std::optional提供了类似的功能。

View File

@@ -0,0 +1,126 @@
# 6 简化函数调用
## 1 Introduce Parameter Object引入参数对象
在代码中可能有一组参数总是一起被传递到好几个函数中这样的一组参数就是所谓的Data Clumps数据泥团。最常见的就是指代一个时间范围的startTime/endTime。可以通过Introduce Parameter Object手法以一个对象取代这些参数。
### 重构示例19
```
// 重构前
class Account {
public:
double GetFlowBetween(Date& startTime, Date& endTime)
{
double result = 0.0;
for (auto& entry : m_entries) {
if (entry.GetDate() == startTime || entry.GetDate() == endTime ||
(entry.GetDate().After(startTime) && entry.GetDate().Before(endTime)) {
result += entry.GetValue();
}
}
return result;
}
}
```
```C
// 重构后
class Account {
public:
double GetFlowBetween(DateRange& dateRange)
{
double result = 0.0;
for (auto& entry : m_entries) {
if (dateRange.Includes(entry.GetDate())) {
result += entry.GetValue();
}
}
return result;
}
}
class DateRange {
public:
bool Includes(Date& date)
{
return date == m_startTime || date == m_endTime ||
(date.After(m_startTime) && date.Before(m_endTime));
}
private:
Date m_startTime;
Date m_endTime;
}
```
## 2 Replace Constructor with Factory Method以工厂函数取代构造函数
如果希望在创建对象时不仅仅是做简单的构建动作,可以将构造函数替换为静态工厂函数,并将原来的构造函数设为私有。静态工厂函数不仅在语义上更加符合人的思维,使代码可读性更强,它还能降低对象与对象使用者之间的耦合。比如,后续想要把对象改成单例模式,只需修改一下静态工厂方法,对象的使用者无感知。
Java 8新增的接口都采用了静态工厂函数用于创建对象比如Optional接口、新的时间接口等。
## 3 Replace Parameter with Explicit Method以明确函数取代参数
如果某个参数有多种可能的值而函数内又以条件表达式检查这些参数值并根据不同参数值作出不同的行为那么就可以使用Replace Parameter with Explicit Method进行重构了。该手法是提供了不同的函数给调用者使用避免出现条件表达式。
### 重构示例20
```
// 重构前
static Employee* Create(int type)
{
switch (type) {
case ENGINEER:
return new Engineer;
case SALESMAN:
return new Salesman;
case MANAGER:
return new Manager;
default:
return nullptr;
}
}
```
```C
// 重构后
static Employee* CreateEngineer()
{
return new Engineer;
}
static Employee* CreateSalesman()
{
return new Salesman;
}
static Employee* CreateManager()
{
return new Manager;
}
```
## 4 Remove Setting Method移除设置函数
某些类中的某些字段应该在初始化时就确认值后续都不会在变化。如果这样的字段拥有setter函数就应该把它去掉然后把字段设置为const/final。
过多无用的getter/setter函数是造成贫血模型的罪恶源头
### 重构示例21
```
// 重构前
class Account {
public:
Account(string id)
{
SetId(id);
}
void SetId(string id)
{
m_id = id;
}
private:
string m_id;
}
```
```C
// 重构后
class Account {
public:
Account(string id) m_id(id) {}
private:
const string m_id;
}
```

View File

@@ -0,0 +1,97 @@
# 7 处理继承关系
## 1 Pull Up Field字段上移
如果两个子类有相同的字段,则将该字段上移至超类中。
![](image/2021-04-11-22-10-08.png)
## 2 Pull Up Method函数上移
如果两个子类有相同的函数而且产生完全相同的结果,则将该函数上移至超类。
![](image/2021-04-11-22-10-17.png)
## 3 Extract Superclass提炼超类
如果两个类有相似特性,可以为这两个类建立一个超类,将相同特性移至超类。如果继承不合适,可以使用[Extract Class](#Extract Class)来提取重复代码。
![](image/2021-04-11-22-10-29.png)
## 4 Form Template Method塑造模板函数
该重构的手法其实就是设计模式中的模板模式,如果有一些子类,其中对应的某些函数以相同顺序执行类似的操作,但在各个操作的细节上有所不同。可以将这些操作分别放到独立的函数中,替换在原函数中原有的操作代码,并上移至超类。
### 重构示例22
```
// 重构前
class Site {
public:
virtual double GetBillableAmount() = 0;
// ...
}
class ResidentialSite : public Site {
public:
double GetBillableAmount() override
{
double base = m_units * m_rate;
double tax = base * Site::TAX_RATE;
return base + tax;
}
// ...
}
class LifelineSite : public Site {
public:
double GetBillableAmount() override
{
double base = m_units * m_rate * 0.5;
double tax = base * Site::TAX_RATE * 0.2;
return base + tax;
}
// ...
}
```
```C++
// 重构后
class Site {
public:
virtual double GetBaseAmount() = 0;
virtual double GetTaxAmount() = 0;
double GetBillableAmount()
{
return GetBaseAmount() + GetTaxAmount();
}
// ...
}
class ResidentialSite : public Site {
public:
double GetBaseAmount() override
{
return m_units * m_rate;
}
double GetTaxAmount() override
{
return GetBaseAmount() * Site::TAX_RATE;
}
// ...
}
class LifelineSite : public Site {
public:
double GetBaseAmount() override
{
return m_units * m_rate * 0.5;
}
double GetTaxAmount() override
{
return GetBaseAmount() * Site::TAX_RATE * 0.2;
}
// ...
}
```
## 5 Replace Inheritance with Delegation以委托取代继承
有时候继承会使代码变得复杂特别是继承层次很深时这时通过组合来取代继承用has-a取代is-a更加合适。如果某个类只使用了超类接口中的一部分或者根本就不需要继承而来的数据就可以使用Replace Inheritance with Delegation进行重构了。
![](image/2021-04-11-22-11-01.png)
## 6 Collapse Hierarchy折叠继承体系
如果超类和子类实现的功能没有太大的差别则使用Collapse Hierarchy将它们合并成一个类。

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,18 +0,0 @@
# SimuRosot项目重构规划书
## 1 重构的目标
- 增强可读性(**自动生成项目的说明文档**
- 增强可扩展性,降低模块的耦合和代码的冗余。面向对象、接口。
- 增强可修改性,方便代码进行修改和扩展。
## 2 重构的流程
- [ ] 重读设计模式和架构设计。使用更好的架构和模块设计方案。(给出一个**架构设计说明书**)。
- [ ] 了解重构工具和重构的方法。熟练掌握C++项目重构过程。
- [ ] 熟读源代码,对架构设计说明书进行增删修改。完成最终的架构设计说明书。
- [ ] 使用vs对**项目**架构进行重构。
> 方案1主要还是重新设计项目把项目内的其他代码赋值黏贴出来。让它成为一个新的能够运行的小项目。
>
> 方案2重新设计项目然后在原有的代码上进行重构。

View File

@@ -0,0 +1,134 @@
# 重构
> 参考文献
> * [https://www.cnblogs.com/ranjiewen/p/5912259.html](https://www.cnblogs.com/ranjiewen/p/5912259.html)
> * [https://blog.csdn.net/ruanrunxue/article/details/102945431](https://blog.csdn.net/ruanrunxue/article/details/102945431)
## 1 什么是重构?
### 重构的定义
Martin Fowler在《重构改善既有代码的设计》一书中给出了重构的两个定义.
* 第一个是名词形式:Refactoring: 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本.
* 第二个是动词形式:Refactor: 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构.
### 重构的目标
重构的目标是什么? 重构的目标绝不是将代码从别人的taste改成自己的taste也不是将代码从一种坏味道改到另一种坏味道!Matin Fowler利用上面两个定义指出了重构的目标:
1. 不改变软件可观察行为
2. 提高软件可理解性
3. 降低软件修改成本
而对于上述目标,我们再深入一点分析,发现其实已经有更经典的定义. 那就是Kent Beck的简单设计四原则:
1. Pass All Test: 通过全部测试;
2. No Duplication: 没有重复(DRY)
3. Reveals Intent: 程序表达意图,易于理解
4. Has no superfluous parts: 没有冗余或者YAGNI原则
上述四条的重要程度依次降低.到目前为止,简单设计四原则是对”什么是好的软件设计”最好的定义!
## 2 从哪里开始?
### 重构的原因
对于重构的目标达成一致后,我们回到起点:什么样的软件需要重构? 以及什么时候进行重构?
对于第一个问题,由于我们重构的目标是使软件满足简单设计四原则,那么任何违反简单设计四原则的代码都应该是我们重构的目标.例如1)代码很容易出现bug导致测试失败! 或者 2)代码存在知识重复使得不易修改! 或者 3)代码写的晦涩非常难以理解! 或者 4)代码存在过度设计,存在冗余导致复杂!
现实中可能有一堆的代码问题等待我们解决,而时间、成本、人力是有限的,所以我们需要从最有价值,最没有争议的部分开始重构. 由于简单设计四原则的重要程度是依次降低的,对于四条原则的判定从上往下也是逐渐主观化,所以我们选择重构的代码的优先级顺序也是按照它们破坏简单四原则的顺序依次降低! 如果一坨代码存在很多重复,另外一坨代码不易理解,那么我们优先选择去解决重复代码的问题,因为按照简单四原则消除重复更重要,也更容易被客观评价.
在《重构》一书中Martin为了避免引起所谓编程美学的含混争辩总结了代码的22条坏味道. 在实践中我们一般都是从某一代码坏味道着手重构的,但是对于优先重构哪个坏味道,我们遵守上面描述的原则.
### 重构的时机
对于进行重构的时机Matin给出:
1. 重复地做某一件事情的时候 (三次法则)
1. 添加新功能的时候
2. 修改Bug的时候
3. Code Review的时候
事实上在我的工作过程中,重构是随时随地进行的. 尤其对于采用演进式设计方法论,重构和代码开发是紧密结合难以分割的,甚至很多时候只有依托重构才能完成代码的开发.
### 重构的手法
明白了起点和目标,下来最重要的就是掌握完成这一过程的手段! 而重构的手法则是带领我们正确到达目标的工具.
很多人认为学习重构只要掌握背后的思想就足够了,其详细繁琐的操作手法并不重要.于是乎现实中我们看到很多人在实际操作重构的过程中章法全无,一旦开始半天停不下来,代码很多时候处于不可编译或者测试不能通过的状态,有时改的出错了很难再使代码回到初始状态,只能推倒重来! 实际上重构是一项非常实践性的技术,能够正确合理地使用重构操作,安全地,小步地,高效地完成代码修改,是评价重构能力的核心标准.
那么什么才是正确的重构手法?
Martin对重构的第二个定义中提到使用一系列的重构手法但是对这一系列的重构手法却没有概括.
而William Opdyke在他的论文”Refactoring Objected-Oriented Frameworks”里面对重构给出了如下定义:
重构行为保持Behavior Preservation的程序重建和程序变换.
在论文里面将重构手法定义为一些程序重建或者程序变换的操作,这些操作满足行为保持(Behavior Preservation)的要求. 论文里面对行为保持的定义如下:
Behavior Preservation For the same set of input valuesthe resulting set of output values should be the same before and after the refactoring.
也就是说存在一系列代码变换的操作,应用这些操作之后,在相同的输入条件下,软件的输出不会发生变化. 我们把满足上述要求的代码操作称之为代码等价变换操作. 在William Opdyke的论文中针对C++提出了26种低层次的代码等价变换操作(例如: 重命名变量,为函数增加一个参数,删除一个不被引用的类…). 按照一定设计好的顺序组合上述低层次的代码等价变换操作,我们可以完成一次安全的代码重构,保证代码重构前后的行为保持要求.
这里代码等价变换的过程. 类似于初等数学中的多项式变换.例如对于如下公式变化:
![](image/2021-04-11-20-50-57.png)
每一步我们运用一次多项式等价变换公式,一步一步地对多项式进行化简,每次变换前后多项式保持等价关系.
在多项式化简的这个例子中,承载简化过程的是已经被数学证明过的多项式等价变换的公式. 同理承载重构的则是被证明过的一个个代表代码等价变换操作的重构手法.
另外,由于完成一项重构需要使用一系列的重构手法,这些手法的使用顺序也是至关重要的!
我们学习重构,就是要来学习每种场景下所使用的小步安全的重构手法及其使用顺序,并不断加以练习! 能够灵活而流畅地使用一系列重构手法完成一项重构,是衡量重构能力的一个非常重要的指标.
而本文后面的一个重点就是对常用的重构手法以及运用顺序进行提炼,降低大家的学习难度.
最后,既然重构中使用的是安全小步的代码等价变换手法,为什么我们还需要测试? 首先是因为我们是人,我们总会犯错! 另外由于编程语言的复杂性导致所谓的等价变换是受上下文约束的例如在C++中为一个存在继承关系的类的成员方法重命名,有可能导致新的方法名和它某一父类中有默认实现的虚方法重名,而即使编译器也不能发现该错误.
### 高效地重构
虽然我们了解了如何/何时开始,目标,以及重构的手法,但是如果我们有了下面这些因素的辅助,会让我们更加安全和高效.
1. 覆盖良好高效的自动化测试
2. 合适的IDE最好提供基本的自动化重构菜单
3. 良好的工程设置
4. 高效的构建环境
5. 良好的编码习惯
对于上面这些不同语言面临的现状不同针对C++语言我们后面会专门总结.
### 哪些不是重构?
针对上面的讨论,我们站在严格的重构定义上来看看下面这些反模式:
“我把bug重构掉了!”
“Debug一下刚才的重构那里出错了”
“昨晚重构出来的Bug到现在还没有查出来”
“先把代码重构好,再看测试为啥不过”
“我把软件架构由集中式重构成分布式了”
想想上面的场景哪里存在问题?
在实际的开发过程中,我们还经常面临另外一种场景,那就是对某一已经开发完成的软件模块进行整体重构. 在这样的过程中,虽然也存在频繁地使用重构手法对原有模块代码进行修改,但是更多的是进行大量的架构和设计方案上的修改.为了与我们要讨论的重构进行区分对于这样的过程我们称其为reengineering(软件重建).
软件重建一般是站在之前开发、测试的基础上伴随着对软件要解决的问题和解决方式本身有了更深入的理解通过修改软件把这些学习成果反映到软件的结构中去使得软件可以更好、更精炼的解决业务问题。站在DDD领域驱动设计的角度软件重建一般是对领域模型的进一步精练使得软件更加贴合业务的本质虽然成功的软件重建往往能对组织带来较大的收益但是由于软件重建的开销普遍较大而软件开发又是一项商业活动所以需要对软件重建谨慎评估其成本收益比以及过程风险后才能决定是否启动。而本文中的重构技术则只是一项日常编码中频繁使用的安全、高效的代码修改技术被普遍认为是现代软件开发技术中必备的一项基本技能是演进式软件设计或者软件重建目标达成的一项必要手段
### 关于本文
我们总结一下,重构有三个要点,见下图:
![](image/2021-04-11-20-52-27.png)
你要有一个敏感的鼻子,能够嗅出代码中的坏味道; 一般只要发现不符合简单设计四原则的Code就是我们需要重构的目标对象. 而Martin总结的22条代码坏味道给我们一个很好的实践起点.
你要知道重构的目标,就是让代码逐渐靠近简单设计四原则.
需要掌握小的安全的重构手法,以及在不同场景下合理的使用顺序,以便安全高效地承载重构目标的达成.
由于重构手法和实施顺序是学习重构的关键,所以本文后面会重点讲述这个主题. 另外,在实践中如何高效和安全的进行重构,和具体使用的编程语言及其开发、构建、测试环境关系也很密切.本文最后会针对C++语言总结这方面相关问题.

View File

@@ -0,0 +1,53 @@
# SimuRosot项目重构规划书
## 1 重构的目标
- 增强可读性(**自动生成项目的说明文档**
- 增强可扩展性,降低模块的耦合和代码的冗余。面向对象、接口。
- 增强可修改性,方便代码进行修改和扩展。
## 2 重构的流程
- [ ] 重读设计模式和架构设计。使用更好的架构和模块设计方案。(给出一个**架构设计说明书**)。
- [ ] 了解重构工具和重构的方法。熟练掌握C++项目重构过程。
- [ ] 熟读源代码,对架构设计说明书进行增删修改。完成最终的架构设计说明书。
- [ ] 使用vs对**项目**架构进行重构。
> 方案1主要还是重新设计项目把项目内的其他代码赋值黏贴出来。让它成为一个新的能够运行的小项目。
>
> 方案2重新设计项目然后在原有的代码上进行重构。
## 3 重构的原则
1. 分层设计。上层对下层依赖。层内部不允许依赖。
2. 控制反转。使用register机制将对象注册到core中计算执行。
3. 决策树。减少strategy过程中if-else的逻辑。
4. 高内聚低耦合。减少类之间的依赖,增强类内部的函数依赖。
5. 文档生成与规范化。
6. 运行时数据依赖与静态数据依赖,进行分离。比如历史数据应该由单独的模块进行管理。
## 4 重构的设计
### 纵向模块
1. 策略层
1. Strategy模块
2. Result模块
2. 击球动作层
3. 跑位动作层
4. 分析层
1. BallPrediction模块
5. 数据层
1. Log模块数据持久化、
2. Compute模块动态数据计算用来表示当前的状态、
3. Court模块静态数据常量用来保存场地信息、
4. History模块历史数据记录用来表示历史状态、
### 横向模块
1. 依赖注入层
2. 核心层
1. 接口封装模块
2. 初始化
3. 策略执行
4. 策略控制
5. 清理