程式碼重構:類重構的 8 個小技巧

小二十七發表於2021-10-12

程式碼重構:類重構的 8 個小技巧

在大多數 OOP 型別的程式語言和麵向物件程式設計中,根據業務建模主要有以下幾個痛點 ?:

  1. 物件不可能一開始就設計的合理,好用
  2. 起初就算設計精良,但隨著版本迭代,物件的職責也在發生變化
  3. 在迭代中,物件的職責往往會因為承擔過多職責,開始變的臃腫不堪(?聞到腐爛的味道了~)

那麼怎麼解決以上的問題?就要運用一些重構的技巧,來讓程式碼結構保持整潔,從而讓後續的需求擴充套件更加穩定

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 搬家後,我們有幾個可見的好處如下:

  1. 可以根據不同 Account 型別,計算不同結果,程式更靈活,呼叫方無需知道計算細節
  2. 避免類似 _type.isPremium() 的函式呼叫出現,看上去更合理

總結

通過 示例一 我們可以得出總結:

  1. 如果一個物件有太多行為和另一個物件耦合,那麼就要考慮幫它搬家
  2. 只要是合理的分配函式,就可以使系統結構,物件本身的行為更加合理

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 個好處如下:

  1. 只需引入 AccountType 即可,無需再重複引入 _interestRate 欄位
  2. AccountType 可以根據不同的型別,設定不同的 _interestRate 利率,程式碼更靈活

總結

不管是搬遷函式,還是搬遷欄位也好,它們都是在不斷重構類的職責和屬性,程式會跟隨需求不斷變化,沒有任何設計是可以保持一成不變的,所以這裡的重構方法,不需要等到特定的時間和特定的規劃再去進行,重構應該是融入在日常開發當中,隨時隨地都在進行的

3:拆解大類

說明:隨著需求越來越多,原來設計的物件承擔的職責也會不斷的增多(方法,屬性等……),如果不加以使用重構的手段來控制物件的邊界(職責,功能),那麼程式碼最終就會變得過於複雜,難以閱讀和理解,最終演化成技術債,程式碼腐爛,從而導致專案最終的失敗。
使用場景:當一個類變的過於龐大,並且承擔很多不屬於它的職責(通過類名來辨識)的時候,建立新類的分擔它的工作

示例一

這裡的 Person 承擔的過多的職責,我們把不屬於它職責範圍的函式抽離出來,從而保證物件上下文的清晰,拆解過程如下:
Person Class

實際程式碼如下:

public class Person {
    
    private String name;
    private String sex;
    private String age;
    private String officeAreaCode;
    private String officeNumber;

    //... 省略 get/set 程式碼...
}

Person 的類圖看起來是這樣的:
Person Class
顯然 Person 做了很多不屬於自己的事情(現實情況往往要慘的多),想要分解的 Person 的話,我們可以這樣做:

  1. 識別 Person 的職責,然後建立一個 TelePhoneNumber 物件進行分擔
  2. 將關聯欄位和函式遷移到 TelePhoneNumber 類中
  3. 進行單元測試

當我們拆解後,新建的 TelePhoneNumber 類程式碼如下:

public class TelePhoneNumber {

    private String officeAreaCode;
    private String officeNumber;

    //... 省略 get/set 程式碼...
}

這時候 Person 物件的職責就簡單和清晰很多了,物件結構如下:
Person Class
TelePhoneNumber 對接結構圖如下:
Person Class

總結

拆解大類,是常見的重構技術手段,其最終目的都是保證每個物件的大小,職責都趨向合理。就像我們工作中如果有一個人太忙,那麼就找一個人幫他分擔就好了。

4:合併小類

說明:這裡是和 拆解大類 邏輯完全相反的的技巧
說用場景:如果一個類沒有做太多的事情,就要考慮把它和相似的類合併在一起,這樣做的目的是:

  • 儘可能保證和控制每個類的職責在一個合理的範圍之內
  • 類過大就使用 拆解大類 的手法
  • 類太小就使用 合併小類 的手法

示例一

我們還是用上面的 PersonTelePhoneNumber 類舉例,合併過程如下:

Person Class

上圖可以看到 Person 在本身屬性很少的情況下,又拆分了 TelePhoneNumber 類,這屬於典型的過度拆分了。就需要使用合手法,將散亂在各地臨散的類進行合併。程式碼如下:

class Person {

    // Person 職責很少,沒必要拆解為 2 個類
    private String name;
    private String age;

    // ...
}

class TelePhoneNumber {

    private String phoneNumber;

    // ...
}

我們把 PersonTelePhoneNumber 進行合併,然後可以移除 TelePhoneNumberPerson 的最終程式碼如下:

public class Person {

    // Person 看上去更加合理了
    private String name;
    private String age;
    private String phoneNumber;

    // ... do some 
}

總結

如果類很小,那麼就要考慮將它合併,從而讓臨近的類的職責更加合理

5:隱藏委託關係

說明:委託關係是指,必須通過 A 類才能呼叫另一個 B 類物件
使用場景:當只有個別函式需要通過關聯方式獲取的時候,使用隱藏委託模式,讓呼叫關係更加簡單

示例一

我們先看看委託模式的程式碼,我們使用一個 PersonDepartment 類來舉例,程式碼如下:

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 物件的所屬經理的時候,我就需要先獲取 PersonDepartment 物件,然後在 Department 中才能呼叫 getManager() 函式,程式碼看起來就會很彆扭,如下:

Person john = new Person();
// 委託模式:需要通過 Department 委託物件才能獲取 Person 想要的資料
Person manage = john.getDepartment().getManage();

這樣的類結構設計會存在以下幾個問題:

  1. 違背 OOP 的封裝原則,封裝的原則意味類儘可能的少對外的暴露資訊
  2. 呼叫方需要去理解 PersonDepartment 的依賴關係,才能拿到 getManage() 資訊
  3. 如果委託關係發生變化,那麼呼叫方也需要修改程式碼

我們可以在 Person 中隱藏這層委託關係,從而讓 Person 可以直接獲取 getManage(),我們在 Person 加入以下程式碼:

public class Person {

    Department department;

    public Person getManage() {
        return department.getManage();
    }
}

這裡看到 Person 有兩處修改:

  1. 隱藏 department.getManage() 委託關係
  2. 移除 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() 擴充套件函式來舉例,假如這個函式會經常被用到,那麼我們就需要增強它,做法如下:

  1. 新建一個擴充套件類,然後繼承工具類(例如 Date
  2. 在擴充套件類內實現擴充套件函式,例如 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);

總結

  • 工具類的擴充套件函式會經常被複用,建議使用 增強工具類 的方式重構顯然更加的合適

相關文章