文章目录
-
- 新的需求
- 生成继承体系用于计算
- 总结与思考
- 系列文章
在上一篇文章 《重构:改善既有代码的设计(第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++版)(二)