天天看點

實踐《重構:改善既有代碼的設計(第2版)》示例(C++版)(二)

文章目錄

    • 新的需求
    • 生成繼承體系用于計算
    • 總結與思考
    • 系列文章

在上一篇文章 《重構:改善既有代碼的設計(第2版)》示例(C++版)(一)中,我們對一個有關于劇目演出的代碼,并且花費了大量的動作将其重構,在重構的過程中保持重構-測試-送出這種小步快跑的模式,最終将原來大泥團式的代碼拆分成結構比較清晰,易于閱讀的代碼。

但是在上篇文章末尾,留下下來一個新的需求問題需要解決,即劇團需要支援更多類型的劇種,以及支援它們各自的價格計算和觀衆量積分計算。

新的需求

時代在發展,劇團也要改革進步,未來會引進更多的劇種,他們的結算方式不盡相同。為了應對未來将要到來的變化,我們需要做一些改變。但是由于未來引入什麼劇種,那麼其結算方式也都是未定的。但有一點可以确定的是,一旦有新的劇種出現,我們就得添加新的結算方式。

按照目前的結構,我隻需要在計算函數裡面添加分支邏輯即可。AmountForPerf函數清楚展現了,戲劇類型在計算分支的選擇上起着關鍵的作用。但這樣的分支邏輯很容易随着代碼堆積而腐化。其實在看到switch出現時候就應該保持有防止代碼腐化的警惕之心。

最好不要在另一個對象的屬性基礎上運用switch語句。如果不得不使用,也應該在對象自己的資料上使用,而不是在别人的資料上使用。我們嘗試抽象出一個抽象類IPlay,它有成員函數GetAmount。為了消滅switch,我們需要為每一個原來的枚舉類型實作一個子類,在每個子類中實作自己的計算方法。下面是具體的UML圖。

實踐《重構:改善既有代碼的設計(第2版)》示例(C++版)(二)

生成繼承體系用于計算

千萬不要心急,不如我們再往前多走一點點。原有的設計存在一個小問題:一個劇目是可以在其生命周期内修改自己的計算費用的算法的。當劇團期望劇目根據淡季旺季修改其結算方式時候,我們并不能在每個劇種生命周期内修改自己結算方式。

為解決這個問題,這時候我們就需要設計模式了——State(狀态)模式。至于說具體應該是State(狀态)模式還是Strategy(政策)模式,那就是另一個話題了。我們來看UML圖:

實踐《重構:改善既有代碼的設計(第2版)》示例(C++版)(二)

要為程式引入結構、顯示地表達出“計算邏輯的差異是由類型代碼确定”有許多途徑,不過最自然的解決辦法還是使用面向對象世界裡的一個經典特性——類型多态。以多态取代條件表達式也是常見的重構手法之一。在具體實施之前,我的設想是建立一個繼承體系,它有兩個子類,分别對應着喜劇和悲劇,再将其獨有的計算體系搬移進子類的具體實作中去。這裡增加了間接層——PerformanceCalculator充當計算費用的電腦。我們可以對他進行子類化動作,就可以修改計算算法了。

class PerformanceCalculator {
public:
    virtual int GetAmount(int audience) = 0;
    virtual int GetVolumeCredits(int audience) {
        int result = 0;
        result += max(audience - 30, 0);
        return result;
    }
};

class TragedyCalculator : public PerformanceCalculator {
public:
    int GetAmount(int audience) override {
        int result = 40000;
        if (audience > 30) {
            result += 1000 * (audience - 30);
        }
        return result;
    }
};
class CemodyCalculator : public PerformanceCalculator {
public:
    int GetAmount(int audience) override {
        int result = 30000;
        if (audience > 20) {
            result += 10000 + 500 * (audience - 20);
        }
        result += 300 * audience;
        return result;
    }
    int GetVolumeCredits(int audience) override{
        int result = 0;
        result += max(audience - 30, 0);
        result += (audience / 5);
        return result;
    }
};
           

此外,在類Play的構造函數中,增加設值函數和取值函數(在這我們還是直接用了類型代碼的變量)

class Play {
public:
    ...
    Play(const string& _playName, int _playCode) {
        name = _playName;
        SetPerfCode(_playCode);
    }
    void SetPerfCode(int _playCode) {
        switch (_playCode) {
            case PlayCode::TRAGEDY :
                calculator.reset(new TragedyCalculator());
                break;
            case PlayCode::COMEDY :
                calculator.reset(new ComedyCalculator());
                break;
        }
    }

    int GetAmount(int audience) {
        int result = 0;
        if (calculator.get()) {
            result = calculator->GetAmount(audience);
        }
        return result;
    }
private:
    string name;
    shared_ptr<PerformanceCalculator> calculator = nullptr;
};
           

再為類Performance增加函數用于調用結算方式并将觀衆人數作為參數傳入。

class Performance {
public:
    ...
    int GetAmount() {
        return GetPlay().GetAmount(GetAudience());
    }
    int GetVolumeCredits() {
        return GetPlay().GetVolumeCredits(GetAudience());
    }

private:
    Play playId;
    int audience;
};
           

至此,我們的UML圖變成了

實踐《重構:改善既有代碼的設計(第2版)》示例(C++版)(二)

終于來到了最後階段,有了前期鋪路,實作這一部分已經變得不再困難。再對最後的Statement()進行修改适配

class Customer {
public:
    Customer(string _custom) {
        name = _custom;
    }

    void AddPerformance(const Performance& perf) {
        performances.push_back(perf);
    }
    string GetName() { return name; }

    string Statement() {
        string result = "Statement for " + GetName() + "\n";
        for (auto& perf : performances) {
             result += "\t" + perf.GetPlay().GetPerfName() + "\t" + to_string(perf.GetAmount() / 100) +
                "(" + to_string(perf.GetAudience()) +  " seats)\n";
        }

        result += "Amount owed is " + to_string(totalAmount() / 100) + "\n";
        result += "You earned " + to_string(totalVolumeCredits()) + "credits\n";
        return result;
    }

private:
    int totalVolumeCredits() {
        int result = 0;
        for (auto& perf : performances) {
            // add volume credits
            result += perf.GetVolumeCredits();
        }
        return result;
    }

    int totalAmount() {
        double result = 0;
        for (auto& perf : performances) {
            result += perf.GetAmount();
        }
        return result;
    }

private:
    vector<Performance> performances;
    string name;
};
           

别忘了測試,每次進行微小重構時,都要進行測試,保證正确性。

int main() {
    Play play1{"Hamlet", PlayCode::TRAGEDY};
    Play play2{"As You Like It", PlayCode::COMEDY};
    Play play3{"Othello", PlayCode::TRAGEDY};

    Performance perf1{play1, 55};
    Performance perf2{play2, 35};
    Performance perf3{play3, 40};

    Customer customer("BigCo");
    customer.AddPerformance(perf1);
    customer.AddPerformance(perf2);
    customer.AddPerformance(perf3);

    cout << customer.Statement() << endl;
    return 0;
}
           

在上述的重構中,我們通過使用state(狀态)模式将原有的代碼進行了重構,當然是保證測試正确性的條件下。現在我們的程式無論是引入新的劇種還是為目前劇目修改計算方式,我們都可以非常容易的應對了。

總結與思考

通過兩篇文章,介紹了《重構》(第二版)一書中的第一個示例,在重構的過程中有三個比較重要的節點:1)将原函數分解成一組小的嵌套的函數、引用拆分階段分離計算邏輯與格式化邏輯,以及為電腦引入多态性來處理計算邏輯。處理之後對代碼添加了更多的結構,更友善的了解和表達代碼。

那麼問題來了:那麼重構是為了什麼呢?

在回答這個問題前我們先考慮下什麼是好的代碼。Martin Fowler認為代碼的好壞不僅是關乎個人品味,而且是有客觀标準的。

好代碼的檢驗标準就是人們是否能輕而易舉地修改它。

好代碼應該直截了當:有人需要修改代碼時,他們應能輕易找到修改點,應該能快速做出更改,而不易引入其他錯誤。優秀的軟體工程師應該保持“代碼的潔癖”,維護軟體的架構,輸出漂亮優美的代碼。然而事實上,在面臨進度的要求下,太多人會選擇妥協(不管出于什麼原因)——犧牲代碼品質而追求進度。尤其是在大型的團隊中,《人月神話》中提到那個焦油坑同樣适用于代碼品質和軟體架構,往往在不斷妥協中積重難返。

而一個健康的代碼庫能夠最大限度地提升我們的生産力,支援我們更快、更低成本地為使用者添加新特性。為了保持代碼庫的健康,就需要時刻留意現狀與理想之間的差距,然後通過重構不斷接近這個理想。如果說設計模式是為了提高軟體結構提供一系列解決方案,即模式是你想要到達的目的地,那麼重構則是從其他地方抵達這個目的地的條條道路。還是Martin Fowler那句話總結的到位:

傻瓜都能寫出計算機可以了解的代碼。唯有能寫出人類容易了解的代碼的,才是優秀的程式員。

系列文章

  • 《重構:改善既有代碼的設計(第2版)》示例(C++版)(一)
  • 《重構:改善既有代碼的設計(第2版)》示例(C++版)(二)