程式碼重構:類重構的 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);
總結
- 工具類的擴充套件函式會經常被複用,建議使用 增強工具類 的方式重構顯然更加的合適