代碼重構:類重構的 8 個小技巧
代碼重構:類重構的 8 個小技巧
在大多數 OOP 類型的編程語言和面向對象程序設計中,根據業務建模主要有以下幾個痛點 🤕:
- 對象不可能一開始就設計的合理,好用
- 起初就算設計精良,但隨着版本迭代,對象的職責也在發生變化
- 在迭代中,對象的職責往往會因為承擔過多職責,開始變的臃腫不堪(🙈聞到腐爛的味道了~)
那麼怎麼解決以上的問題?就要運用一些重構的技巧,來讓代碼結構保持整潔,從而讓後續的需求擴展更加穩定
1:合理的分配函數
說明:從 OOP 的角度來考慮,如果函數之間頻繁的調用,顯然適合放在一個對象當中
使用場景:在 A 對象內,看到它經常調用 B 類的函數,那麼你就有必要需要考慮把 B 類的函數搬過來了。
示例一
空說很難理解,我們先展示一段代碼,來展示說項重構的手法:
public class Account {
// 計算透支費用
double overdraftCharge() {
if (_type.isPremium()) {
double result = 10;
if (_daysOverdrawn > 7) {
result += (_daysOverdrawn - 7) * 0.85;
}
return result;
} else {
return _daysOverdrawn * 1.75;
}
}
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0) {
result += overdraftCharge();
}
return result;
}
// 編碼道德 758 條:私有變量應該放在類的底部
private AccountType _type;
private int _daysOverdrawn;
}
// 賬戶類型
class AccountType {
//... do something
}
在上面例子 🌰 中,我們看到 Account
顯然承擔一些不屬於它本身的職責,從 _type.isPremium()
的調用方式來看,overdraftCharge
不論從調用,還是命名來看,都更像是 AccountType
的職責,所以我們嘗試來搬遷它,最終代碼如下:
class AccountType {
// 從 Account 搬過來了
double overdraftCharge(Account account) {
if (isPremium()) {
double result = 10;
if (account.getDaysOverdrawn() > 7) {
result += (account.getDaysOverdrawn() - 7) * 0.85;
}
return result;
} else {
return account.getDaysOverdrawn() * 1.75;
}
}
// more ...
}
public class Account {
double bankCharge() {
double result = 4.5;
if (_daysOverdrawn > 0) {
// 還可以根據不同 Account 類型進行擴展
result += _type.overdraftCharge(this);
}
return result;
}
}
函數 overdraftCharge
搬家後,我們有幾個可見的好處如下:
- 可以根據不同 Account 類型,計算不同結果,程序更靈活,調用方無需知道計算細節
- 避免類似
_type.isPremium()
的函數調用出現,看上去更合理
總結
通過 示例一
我們可以得出總結:
- 如果一個對象有太多行為和另一個對象耦合,那麼就要考慮幫它搬家
- 只要是合理的分配函數,就可以使系統結構,對象本身的行為更加合理
2:合理分配字段
說明:這裡的思路和 合理的分配函數
非常的相似,只是主體由 函數
替換為的 字段
使用場景:當 A 類的某一個字段頻繁的被 B 類使用,那麼就要考慮把它搬遷放到 B 類中
示例一
這裡比較簡單,能理解上面的函數分配,也就能理解這裡,我們看一段簡單的示例就好,還是以剛才的 Account 類為例子:
public class Account {
// 結算日息
double interestForAmountDays (double amount,int days){
return _interestRate * amount * days / 365;
}
private AccountType _type;
// 利率 %
private int _interestRate;
}
// 賬戶類型
class AccountType {
// do something....
}
從示例上看,_interestRate
字段顯然更適合放在 AccountType
,我們做一次字段搬遷,搬完後代碼如下:
public class Account {
double interestForAmountDays (double amount,int days){
return _type.getInterestRate() * amount * days / 365;
}
private AccountType _type;
}
// 賬戶類型
class AccountType {
// 利率 %
private int _interestRate;
public int getInterestRate() {
return _interestRate;
}
}
主要做有 2 個好處如下:
- 只需引入
AccountType
即可,無需再重複引入_interestRate
字段 AccountType
可以根據不同的類型,設置不同的_interestRate
利率,代碼更靈活
總結
不管是搬遷函數,還是搬遷字段也好,它們都是在不斷重構類的職責和屬性,程序會跟隨需求不斷變化,沒有任何設計是可以保持一成不變的,所以這裡的重構方法,不需要等到特定的時間和特定的規劃再去進行,重構應該是融入在日常開發當中,隨時隨地都在進行的
3:拆解大類
說明:隨着需求越來越多,原來設計的對象承擔的職責也會不斷的增多(方法,屬性等……),如果不加以使用重構的手段來控制對象的邊界(職責,功能),那麼代碼最終就會變得過於複雜,難以閱讀和理解,最終演化成技術債,代碼腐爛,從而導致項目最終的失敗。
使用場景:當一個類變的過於龐大,並且承擔很多不屬於它的職責(通過類名來辨識)的時候,創建新類的分擔它的工作
示例一
這裡的 Person
承擔的過多的職責,我們把不屬於它職責範圍的函數抽離出來,從而保證對象上下文的清晰,拆解過程如下:
實際代碼如下:
public class Person {
private String name;
private String sex;
private String age;
private String officeAreaCode;
private String officeNumber;
//... 省略 get/set 代碼...
}
Person 的類圖看起來是這樣的:
顯然 Person
做了很多不屬於自己的事情(現實情況往往要慘的多),想要分解的 Person
的話,我們可以這樣做:
- 識別 Person 的職責,然後創建一個
TelePhoneNumber
對象進行分擔 - 將關聯字段和函數遷移到
TelePhoneNumber
類中 - 進行單元測試
當我們拆解後,新建的 TelePhoneNumber
類代碼如下:
public class TelePhoneNumber {
private String officeAreaCode;
private String officeNumber;
//... 省略 get/set 代碼...
}
這時候 Person
對象的職責就簡單和清晰很多了,對象結構如下:
TelePhoneNumber
對接結構圖如下:
總結
拆解大類,是常見的重構技術手段,其最終目的都是保證每個對象的大小,職責都趨向合理。就像我們工作中如果有一個人太忙,那麼就找一個人幫他分擔就好了。
4:合併小類
說明:這裡是和 拆解大類 邏輯完全相反的的技巧
說用場景:如果一個類沒有做太多的事情,就要考慮把它和相似的類合併在一起,這樣做的目的是:
- 儘可能保證和控制每個類的職責在一個合理的範圍之內
- 類過大就使用 拆解大類 的手法
- 類太小就使用 合併小類 的手法
示例一
我們還是用上面的 Person
和 TelePhoneNumber
類舉例,合併過程如下:
上圖可以看到 Person
在本身屬性很少的情況下,又拆分了 TelePhoneNumber
類,這屬於典型的過度拆分了。就需要使用合手法,將散亂在各地臨散的類進行合併。代碼如下:
class Person {
// Person 職責很少,沒必要拆解為 2 個類
private String name;
private String age;
// ...
}
class TelePhoneNumber {
private String phoneNumber;
// ...
}
我們把 Person
和 TelePhoneNumber
進行合併,然後可以移除 TelePhoneNumber
, Person
的最終代碼如下:
public class Person {
// Person 看上去更加合理了
private String name;
private String age;
private String phoneNumber;
// ... do some
}
總結
如果類很小,那麼就要考慮將它合併,從而讓臨近的類的職責更加合理
5:隱藏委託關係
說明:委託關係是指,必須通過 A 類才能調用另一個 B 類對象
使用場景:當只有個別函數需要通過關聯方式獲取的時候,使用隱藏委託模式,讓調用關係更加簡單
示例一
我們先看看委託模式的代碼,我們使用一個 Person
和 Department
類來舉例,代碼如下:
public class Person {
Department department;
// 獲取所屬部門
public Department getDepartment() {
return department;
}
}
class Department {
private String chargeCode;
private Person manage;
public Department(Person manage) {
this.manage = manage;
}
// 需要通過 Department 才能找到部門 Manage
public Person getManage() {
return manage;
}
}
以上代碼設計看上去沒有問題,但是當我想要獲取某一個 Person
對象的所屬經理的時候,我就需要先獲取 Person
的 Department
對象,然後在 Department
中才能調用 getManager()
函數,代碼看起來就會很彆扭,如下:
Person john = new Person();
// 委託模式:需要通過 Department 委託對象才能獲取 Person 想要的數據
Person manage = john.getDepartment().getManage();
這樣的類結構設計會存在以下幾個問題:
- 違背 OOP 的封裝原則,封裝的原則意味類儘可能的少對外的暴露信息
- 調用方需要去理解
Person
和Department
的依賴關係,才能拿到getManage()
信息 - 如果委託關係發生變化,那麼調用方也需要修改代碼
我們可以在 Person
中隱藏這層委託關係,從而讓 Person
可以直接獲取 getManage()
,我們在 Person
加入以下代碼:
public class Person {
Department department;
public Person getManage() {
return department.getManage();
}
}
這裡看到 Person 有兩處修改:
- 隱藏
department.getManage()
委託關係 - 移除
getDepartment()
函數
最終獲取 Person 的 getManage() 顯示更加直接,代碼如下:
// 然後改用新函數獲取 Person 的 Manage
Person manage = john.getManage();
總結
如果 只有少數函數 需要依賴委託關係獲取的時候,可以使用 隱藏委託關係 的重構手法來讓類關係和調用變的簡單。
6:移除中間人
說明:這是 隱藏委託關係 這個技巧的反例
使用場景:當有過多函數需要委託的時候,不建議繼續隱藏委託模式,直接讓調用方調用目標類,代碼反而更簡潔
示例一
我們上面的代碼通過在 Person
建立隱藏的委託模式,如下:
public Person getManage() {
return department.getManage();
}
通過這種方式來簡化對象關係的調用,但是再想想,在後續的需求迭代中,Department
也在不斷增加特性,如果 Department
每個新增的特性,都需要通過 Person
來進行委託的話,那麼代碼看起來就像這樣:
class Department {
private String chargeCode;
private Person manage;
// 部門不斷發展,新增不少角色
private Person captain;
private Person groupLeader;
// 省略獲取部門角色的方法....
}
所以每當 Department
增加角色,Person
都要修改代碼來繼續隱藏委託模式,Person
代碼如下:
class Person {
Department department;
public Person getManage() {
return department.getManage();
}
// 兼容 Department 新增的委託模式
public Person getCaptain() {
return department.getCaptain();
}
public Person getGroupLeader() {
return department.getGroupLeader();
}
}
所以 當有過多函數委託 的時候,倒不如移除 Person
這個簡單的中間人,讓對象直接調用 Department
,區別如下:
// 通過委託模式獲取
Person manage = john.getManage();
Person captain = john.getCaptain();
Person groupLeader = john.getGroupLeader();
// 移除中間人獲取
Person manage = john.getDepartment().getManage();
Person captain = john.getDepartment().getCaptain();
Person groupLeader = john.getDepartment().getGroupLeader();
這樣做的好處就是 Person
的代碼量大大減少,移除中間人後的 Person
類:
public class Person {
// 當委託工作變的非常重的時候,解除委託關係,可以讓 Person 獲得解放
Department department;
public Department getDepartment() {
return department;
}
}
總結
- 當需要委託的特性越多,隱藏委託模式就顯得沒有必要,直接調用提供人代碼會更簡單
- 如果只有簡單的委託特性,建議使用隱藏委託關係
7:擴展工具類
使用場景:當系統工具庫無法滿足你需求的時候,但是你又無法修改它(例如 Date
類),那麼你可以封裝和擴展它,來讓它具備你需要的新特性。
示例一
例如我們有一段處理時間的函數,Date
工具類似乎並沒有提供這樣的方法,我們自己實現了,代碼如下:
Date previousEnd = new Date();
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
以上 newStart
實現的方式有以下幾個問題:
- 表達式難以閱讀
- 無法復用
我們使用 擴展工具類 的方式,可以把程序重構為以下這樣:
Date previousEnd = new Date();
Date newStart = nextDay(previousEnd);
// 提煉一個函數,作為 Date 類的擴展函數方法
public static Date nextDay(Date arg) {
return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
總結
- 通過擴展工具類,為工具類增強更多的功能,從而滿足業務的需求
- 如果有可能(獲取修改工具類的權限),那麼可以考慮把擴展函數搬到工具類內部,讓更多人復用
8:增強工具類
使用場景:當你無法修改工具類(通常都無法修改),並且只有個別函數需要擴展的時候,那麼使用 擴展工具類 沒有任何問題,只要少量的代碼就可以滿足功能需求,但是這種擴展是一次性的,例如擴展的 nextDay()
函數,無法被其他類復用。所以我們需要用增強工具類來解決這個問題
示例一
我們還是使用上面的 nextDay()
擴展函數來舉例,假如這個函數會經常被用到,那麼我們就需要增強它,做法如下:
- 新建一個擴展類,然後繼承工具類(例如
Date
) - 在擴展類內實現擴展函數,例如
nextDay()
代碼如下:
public class StrongDate extends Date {
// 提煉一個函數,作為 Date 類的擴展函數方法
public static Date nextDay(Date arg) {
return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
// ... 這裡還可以做更多擴展
}
調用方使用方式:
Date previousEnd = new Date();
Date newStart = StrongDate.nextDay(previousEnd);
總結
- 工具類的擴展函數會經常被複用,建議使用 增強工具類 的方式重構顯然更加的合適