文章目錄
-
- 新的需求
- 生成繼承體系用于計算
- 總結與思考
- 系列文章
在上一篇文章 《重構:改善既有代碼的設計(第2版)》示例(C++版)(一)中,我們對一個有關于劇目演出的代碼,并且花費了大量的動作将其重構,在重構的過程中保持重構-測試-送出這種小步快跑的模式,最終将原來大泥團式的代碼拆分成結構比較清晰,易于閱讀的代碼。
但是在上篇文章末尾,留下下來一個新的需求問題需要解決,即劇團需要支援更多類型的劇種,以及支援它們各自的價格計算和觀衆量積分計算。
新的需求
時代在發展,劇團也要改革進步,未來會引進更多的劇種,他們的結算方式不盡相同。為了應對未來将要到來的變化,我們需要做一些改變。但是由于未來引入什麼劇種,那麼其結算方式也都是未定的。但有一點可以确定的是,一旦有新的劇種出現,我們就得添加新的結算方式。
按照目前的結構,我隻需要在計算函數裡面添加分支邏輯即可。AmountForPerf函數清楚展現了,戲劇類型在計算分支的選擇上起着關鍵的作用。但這樣的分支邏輯很容易随着代碼堆積而腐化。其實在看到switch出現時候就應該保持有防止代碼腐化的警惕之心。
最好不要在另一個對象的屬性基礎上運用switch語句。如果不得不使用,也應該在對象自己的資料上使用,而不是在别人的資料上使用。我們嘗試抽象出一個抽象類IPlay,它有成員函數GetAmount。為了消滅switch,我們需要為每一個原來的枚舉類型實作一個子類,在每個子類中實作自己的計算方法。下面是具體的UML圖。

生成繼承體系用于計算
千萬不要心急,不如我們再往前多走一點點。原有的設計存在一個小問題:一個劇目是可以在其生命周期内修改自己的計算費用的算法的。當劇團期望劇目根據淡季旺季修改其結算方式時候,我們并不能在每個劇種生命周期内修改自己結算方式。
為解決這個問題,這時候我們就需要設計模式了——State(狀态)模式。至于說具體應該是State(狀态)模式還是Strategy(政策)模式,那就是另一個話題了。我們來看UML圖:
要為程式引入結構、顯示地表達出“計算邏輯的差異是由類型代碼确定”有許多途徑,不過最自然的解決辦法還是使用面向對象世界裡的一個經典特性——類型多态。以多态取代條件表達式也是常見的重構手法之一。在具體實施之前,我的設想是建立一個繼承體系,它有兩個子類,分别對應着喜劇和悲劇,再将其獨有的計算體系搬移進子類的具體實作中去。這裡增加了間接層——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圖變成了
終于來到了最後階段,有了前期鋪路,實作這一部分已經變得不再困難。再對最後的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++版)(二)