由于工作中常用c++的原因,在看《Refactoring — Improving the Design of Exsiting Code》这本书时,将java的例子写成了c++程序,略做总结,以深理解。
重构的习惯与思想,跟语言无关。但c++是一个难掌控的语言,记下这些优化改进,希望上进的c++ programmer们,能实现日积跬步,终至千里的梦想。
一、待优化的程序
程序描述:
(1)我们将优化的是一个影片出租用的小程序,该程序会记录每个顾客的消费金额并打印出来。
(2)程序输入为:顾客租的影片及对应的租期;程序的处理为:根据顾客租用影片时间及影片类型,计算费用;输出:打印消费单。
(3)影片有三种类型:普通影片、儿童影片及新上映影片。
(4)另外,模仿时下潮流,程序还提供了积分制度,为常客计算积分,积分会根据影片是否为新上映影片而不同。
程序代码如下:
// 影片类
class Movie
{
public:
enum MovieType {
REGULAR = 0, CHILDREN, NEW_REALESE
};
Movie(const string &title, int type)
{
m_title = title;
m_type = type;
}
int GetType()
{
return m_type;
}
int SetType(int type)
{
m_type = type;
}
string GetTitle()
{
return m_title;
}
private:
// 影片名字
string m_title;
// 影片类型
int m_type;
};
影片类是个纯数据类。
// 租赁类
class Rental
{
public:
Rental(Movie *movie, int days_rented)
{
m_movie = movie;
m_days_rented = days_rented;
}
int GetDaysRented() const
{
return m_days_rented;
}
Movie * GetMovie() const
{
return m_movie;
}
private:
// 租赁的影片对象
Movie *m_movie;
// 租赁的天数
int m_days_rented;
};
Rental类表示某个顾客租了一部影片。
// 顾客类
class Customer
{
public:
Customer(const string &name)
{
m_name = name;
}
~Customer()
{
CleanUp();
}
void AddRental(Rental *rental)
{
m_rentals.push_back(rental);
}
string GetName() const
{
return m_name;
}
string GenerateReports();
private:
void CleanUp()
{
vector::iterator it = m_rentals.begin();
for (; it != m_rentals.end(); ++it)
{
delete (*it);
}
m_rentals.clear();
}
template
string NumToString(T num)
{
stringstream s_stream;
s_stream << num;
return s_stream.str();
}
private:
// 顾客的名字
string m_name;
// 顾客租赁的所有影片
vector m_rentals;
};
Customer类用来表示一个顾客。GenerateReports()函数用来生成某个顾客租赁影片的详细报表。
这三个类的UML类图描述如下:
GenerateReports()函数的完整代码如下:
string Customer::GenerateReports()
{
// 租赁消费总额
double total_amount = 0;
// 积分
int renter_points = 0;
// 返回的字符串形式的报表
string result = "Retal Record for " + GetName() + "\n";
vector::const_iterator it = m_rentals.begin();
for (; it != m_rentals.end(); ++it)
{
Rental *a_rental = *it;
double amount = 0;
// 计算每个租赁的消费额
switch (a_rental->GetMovie()->GetType())
{
case Movie::REGULAR:
amount += 2;
if (a_rental->GetDaysRented() > 2)
{
amount += (a_rental->GetDaysRented() - 2) * 1.5;
}
break;
case Movie::NEW_REALESE:
amount += a_rental->GetDaysRented() * 3;
break;
case Movie::CHILDREN:
amount += 1.5;
if (a_rental->GetDaysRented() > 3)
{
amount += (a_rental->GetDaysRented() - 3) * 1.5;
}
break;
default:
break;
}
// 添加积分
++renter_points;
// 对两天以上的新片给以奖励积分
if (a_rental->GetMovie()->GetType() == Movie::NEW_REALESE
&& a_rental->GetDaysRented() > 1)
{
++renter_points;
}
// 将本次租赁信息添加到results中
result += "\t" + a_rental->GetMovie()->GetTitle() + "\t"
+ NumToString(amount) + "\n";
total_amount += amount;
}
// 添加总消费额及积分
result += "Amount is " + NumToString(total_amount) + "\n";
result += "You earned " + NumToString(renter_points) + " frequent renter points.";
return result;
}
GenerateReports函数的交互过程如下:
二、代码的评价
Martin Fowler 对上述代码做了如下评价:
(1)上述代码不符合面向对象精神。
(2)对于一个独立的小程序,快速而简单的设计一个程序并没有错;但如果上述代码是复杂系统中的一小部分,这段代码令人堪忧。
(3)Customer类里那个长长的GenerateReports函数承担了太多职责,做了很多本该由其他类完成的工作。
(4)编译器不会在乎上述的丑陋代码,它能正常完成工作,但是,当我们打算修改系统的时候,就涉及到了人,人会在乎上述代码的丑陋,因为人会夹杂美学上的判断。
(5)差劲的系统是很难修改的,因为很难找到修改点,如果很难找到修改点,程序员就很可能犯错,引入bug。
另外,如果需求变化了,上述代码会怎样?
(1)在web时代,如果用户希望输出HTML报表呢?
你会发现,在打印HTML报表的函数中,你根本无法重用GenerateReports函数的任何行为。
你唯一可做的是,编写一个全新的GenerateHtmlReports函数,重复GenerateReports的行为,当然,这种改变好像也不怎么费力,也许你很快就完成了!
(2)如果计费标准发生了变化,譬如租用新影片的计费发生变化,该怎么办呢?
这个时候,你必须同时修改GenerateHtmlReports()和GenerateReports(),而且要保证对两个函数修改的一致性。
当后续需求变化还需要修改时,ctrl+c和ctrl+v的带来的问题就来了,假若你编写的是一个永不修改程序也罢,但如果程序的生命周期很长,就可能会维护,会面临修改,那么复制和粘贴就会造成潜在的威胁。
(3)很快,需求变更又来了,用户决定改变影片的分类规则,但是还没有决定如何修改。
不管分类规则如何变化,总会影响计费规则和积分规则,就必须对GenerateReports的函数做出修改。
随着规则越来越复杂,两个函数的一致性越来越难保证,不犯错的机会将会越来越少。
三、重构
也许,你倾向于尽量少的修改程序,因为这段代码还可以工作,它并没有挂掉,但是,虽然它没有挂掉,它伤了程序员的心。它使程序员的生活变得难过,因为你发现,当你需要满足客户的修改需求时,你越来越难完成客户的需求......
相信很多程序员都有这样的经历,这个时候,就是需要重构技术登场了!