文章目录
- 用Creation Method替换构造函数
- 用Factory封装类
- 用Factory Method引入多态创建
- 系列文章
用Creation Method替换构造函数
当类中有多个构造函数,使用过程中很容易遇到不知道调用哪一个,此时用能够说明意图的返回对象实例的Creation Method替换构造函数
考虑如下的类Loan,C++要求其构造函数的名字必须与它所属的类相同。当构造函数慢慢变多,问题也会越来越明显,当希望实例化一个Loan时,程序员不得不研究需要传入什么参数,琢磨这些构造函数代码,已选择应该调用哪一个构造函数。构造函数本身无法有效和高效地表达意图。越多的构造函数不仅容易造成选择错误,还会减缓开发速度。此外,随着迭代,许多构造函数不再使用但仍留在代码中,大多数情况下没有人会去管这些死掉的构造函数。你是否与我一样嗅到了一丝代码的坏味道呢,没错,死构造函数会使类膨胀。
在这里,作者提出以Creation Method来解决这些问题。所谓Creation Method,就是类中的一个静态或非静态的负责实例化类的新实例的方法。
Creation Method 和 Factory Method
很多人都了解或知道一点“Factory Method”,其来源于《设计模式》DP一书中的一个创建型模式。但并非所有的创建对象的方法都是工厂模式。作者所提Creation Method来指代创建类的实例的静态方法或非静态方法。即所有Factory Method都是Creation Method,但反之则不尽然。
回到上面的类Loan,其可以用来表示贷款方式,这里有三种分别为定期贷款(term loan),循环贷款(revoler),以及循环信用定期贷款(revolving credit term loan,RCTL)。
很多同学或许会跟我一样,在面对这样的情况下很容易想到通过将Loan设计为一个抽象超类,用子类标识各种贷款。所以我希望在对这一系列构造函数进行重构之前,通过以下解释先达成一些共识:
- 区分不同贷款的知识计算资金(capital),收益(income)和期限(duration)等数值。与其为了支持定义贷款(term loan)而创建三个子类,不如通过strategy模式设置3个不同的Strategty类。
- 不同种类贷款切换时,修改Loan实例中的几个字段要比Loan不同子类实例中切换容易的多。
有了共识后我们要对Loan类中的所有构造函数进行重构,先来看Loan类的构造函数:
class Loan {
public:
Loan(double commitment, int riskRating, Date* maturity) {
Loan(commitment, 0.00, riskRating, maturity, nullptr);
}
Loan(double commitment, int riskRating, Date* maturity, Date* expiry) {
Loan(commitment, 0.00, riskRating, maturity, expiry);
}
Loan(double commitment, double outstanding, int riskRating, Date* maturity, Date* expiry) {
Loan(nullptr, commitment, outstanding, riskRating, maturity, expiry);
}
Loan(CapitalStrategy* capitalStrategy, double commitment, int riskRating, Date* maturity, Date* expiry) {
Loan(capitalStrategy, commitment, 0.00, riskRating, maturity, expiry);
}
Loan(CapitalStrategy *capitalStrategy, double commitment, double outstanding, int riskRating, Date *maturity, Date *expiry) {
_commitment = commitment;
_outstanding = outstanding;
_riskRating = riskRating;
_capitalStrategy.reset(capitalStrategy);
_maturity.reset(maturity);
_expiry.reset(expiry);
if (!_capitalStrategy.get()) {
if (!_expiry.get()) {
_capitalStrategy = make_shared<CapitalStrategy>(CapitalStrategyTermLoan());
} else if (!_maturity.get()) {
_capitalStrategy = make_shared<CapitalStrategy>(CapitalStrategyRevolver());
} else {
_capitalStrategy = make_shared<CapitalStrategy>(CapitalStrategyRCTL());
}
}
};
}
在动手重构之前,我还是会忍不住提醒你记住重构成功的秘籍——小步快跑,而保证跑的的方向对的手段是构建测试用例:
class CapitalCalculateTest {
public:
CapitalCalculateTest() {
...
Loan termLoan = new Loan(commitment, riskRating, maturity);
...
}
};
接下来应用Creation Method替换构造函数进行重构:
- 提炼函数:
class CapitalCalculateTest {
public:
CapitalCalculateTest() {
...
Loan termLoan = new CreateTermLoan(commitment, riskRating, maturity);
...
}
static Loan CreateTermLoan(double commitment, int riskRating, Date* maturity) {
return new Loan(commitment, riskRating, maturity);
}
};
- 搬移函数:将CreateTermLoan搬移到Loan类中
class Loan {
public:
static Loan* CreateTermLoan(double commitment, int riskRating, Date* maturity) {
return new Loan(commitment, riskRating, maturity);
}
}
class CapitalCalculateTest {
public:
CapitalCalculateTest() {
...
Loan termLoan = Loan.CreateTermLoan(commitment, riskRating, maturity);
...
}
};
- 方法内联化:CreateTermLoan方法现在是该构造函数的唯一调用者,将另一个链接的构造函数内联化
class Loan {
public:
Loan(double commitment, int riskRating, Date* maturity) {
Loan(commitment, 0.00, riskRating, maturity, nullptr);
};
static Loan* CreateTermLoan(double commitment, int riskRating, Date* maturity) {
return new LoanLoan(commitment, 0.00, riskRating, maturity, nullptr);
}
};
观察上述步骤,最终生成了CreateTermLoan方法,而当程序员想生成Loan实例时,遇到这样的Loan类也不会再产生迷惑了。同样的对其他构造函数也采用类似步骤,生成如下方法:
class Loan {
public:
Loan createTermLoan(double commitment, int riskRating, Date* maturity);
Loan createTermLoan(CapitalStrategy* capitalStrategy, double commitment, double outstanding, int riskRating, Date* maturity);
Loan createRevolver(double commitment, double outstanding, int riskRating, Date* expiry);
Loan createRevolver(CapitalStrategy* capitalStrategy, double commitment, double outstanding, int riskRating, Date* expiry);
Loan createRCTL(double commitment, double outstanding, int riskRating, Date* maturity, Date* expiry);
Loan createRCTL(CapitalStrategy* capitalStrategy, double commitment, double outstanding, int riskRating, Date* maturity, Date* expiry);
private:
Loan(CapitalStrategy* capitalStrategy, double commitment, double outstanding, int riskRating, Date* maturity, Date* expiry);
}
可以看到我将唯一一个公共构造函数置为了私有。而真正用于生成Loan实例的方法正是我们重构后的Creation Method。观察下面这个类,是不是要比重构之前清楚很多,当你想构造一个Loan实例时,不再会为选择哪个构造函数而犯难。
- 优缺点
- 比构造函数能够更好地表达所创建的实例的种类。
- 避免了构造函数的局限,比如两个构造函数的参数数目和类型不能相同。
- 更容易发现无用的创建代码
- 缺点:
- 创建方式是非标准的:有些类用new实例化,而有些类用Creation Method实例化。
用Factory封装类
直接实例化处在同一类结构中、实现同一接口的多个类。把类的构造函数声明为非公共的,并通过Factory来创建它们的实例。当调用者需要确切知道一些类的存在,直接实例化这些类的能力是必须切有用的。但是当它们不再需要这的信息呢?
有这么一些类,它们都处在同一个包结构中、都实现了同一个接口,且并不会经常发生改变。在这种情况下,通过一个“Factory”类就可以把这些类与调用者隔离起来,只需要赋予Factory类创建和返回实现了类的实例的能力即可。
示例:下面的代码基于一段实现对象-关系数据库映射的代码,他用来从关系型数据库中读取对象并把对象写入关系型数据库。各个类用于吧数据库中的属性映射到对象的实例变量中。
class AttributeDescriptor {
Protect:
AttributeDescriptor() {
cout << "AttributeDescriptor" <<endl;
}
};
class BooleanDescriptor : public AttributeDescriptor {
public:
BooleanDescriptor() {
cout << "BooleanDescriptor" << endl;
}
};
class DefaultDescriptor : public AttributeDescriptor {
public:
DefaultDescriptor() {
cout <<"DefaultDescriptor" << endl;
}
};
class ReferenceDescriptor : public AttributeDescriptor {
public:
ReferenceDescriptor() {
cout << "ReferenceDescriptor" << endl;
}
};
重构后的代码通过超类AttributeDescriptor 访问其子类,同时保证AttributeDescriptor 类的接口能够获得子类的对象。此外,告诉其他程序员AttributeDescriptor 的子类并不打算公开,客户代码通过统一的接口与子类的实例交互,防止客户代码直接实例化AttributeDescriptor 的子类。
这么做带来许多好处,首先,确保客户代码使用超类的通用接口与类交互——“面向接口编程,而不是面向实现[DP]”;其次,通过隐藏那些被外部可见的类。再次,通过Factory类中的意图导向的Creation Method(ForBoolean,ForString,ForDate),简化了不同种类实例的创建。
Creation Method,就是类中的一个静态或非静态的负责实例化类的新实例的方法。
class AttributeDescriptor {
public:
static AttributeDescriptor* ForBoolean();
static AttributeDescriptor* ForDate();
static AttributeDescriptor* ForString();
protected:
AttributeDescriptor() {
cout << "AttributeDescriptor" <<endl;
}
};
class BooleanDescriptor : public AttributeDescriptor {
public:
BooleanDescriptor() {
cout << "BooleanDescriptor" << endl;
}
};
class DefaultDescriptor : public AttributeDescriptor {
public:
DefaultDescriptor() {
cout <<"DefaultDescriptor" << endl;
}
};
class ReferenceDescriptor : public AttributeDescriptor {
public:
ReferenceDescriptor() {
cout << "ReferenceDescriptor" << endl;
}
};
AttributeDescriptor* AttributeDescriptor::ForBoolean() {
return new BooleanDescriptor();
}
AttributeDescriptor* AttributeDescriptor::ForDate() {
return new DefaultDescriptor();
}
AttributeDescriptor* AttributeDescriptor::ForString () {
return new ReferenceDescriptor();
}
- 优点
- 通过意图导向的Creation Method简化了不同种类实例的创建
- 隐藏不需要公开的类
- 严格执行“面向接口编程,而不是面向实现[DP]”
- 缺点
- 当需要创建新种类的实例时,必须新建/更新Creation Method,对变化不灵活。
用Factory Method引入多态创建
为了形成Creation Method,类必须实现一个静态或非静态的方法来初始化并返回一个对象。另一方面,如果想形成一个Factory Method模式[DP],需要如下事务:
- 用来表示Factory Method实现者可能实例化并返回的类的集合的类型(由接口、抽象类或类定义)。
- 实现这一类型的类集合。
- 实现Factory Method的类,他们在本地决定实例化、初始化并返回哪些类。
Factory Method是面向对象编程中最常用的模式,提供了多态传教对象的方法。通常的情况是,一个抽象类要么声明一个Factory Method并强制子类重写它,要么实现一个默认的Factory Method并允许子类继承或重写这个默认实现。
使用Factory Method进行重构主要用于如下两种情况:
- 当兄弟子类实现了出对象创建步骤外都很相似的方法时。
- 当超类和子类实现了除对象创建步骤外都很相似的方式时。
我们还是以在实践《重构》示例(C++版)中的例子来举例,把关键代码挪到这儿来,账单类Bill是主要目的是GetBill产生账单,其他totalVolumeCredits或totalAmount为计算账单中的一些关键信息。
现在观察如下代码,调用GetBill会产生一个文本格式的账单并输出,我们希望在此基础上提供能力生成一个HTML版本的账单,最简单的方式就是将Bill复制一份,将部分改写为HTML版本。很明显这么做存在大量重复代码,在面临日后变化的时候会越来越膨胀(XML版本、json版本)。
class Customer {
public:
Customer(string _custom) {
name = _custom;
}
void AddPerformance(const Performance& perf) {
performances.emplace_back(perf);
}
string GetName() { return name; }
string statement() {
double totalAmount = 0;
int volumeCredits = 0;
string result = "Statement for " + name + "\n";
for (auto& perf : performances) {
double thisAmount = 0;
switch (perf.GetPlayId().GetPerfCode()) {
case PlayCode::TRAGEDY: {
thisAmount = 40000;
if (perf.GetAudience() > 30) {
thisAmount += 1000 * (perf.GetAudience() - 30);
}
break;
}
case PlayCode::COMEDY: {
thisAmount = 30000;
if (perf.GetAudience() > 20) {
thisAmount += 10000 + 500 * (perf.GetAudience() - 20);
}
thisAmount += 300 * perf.GetAudience();
break;
}
default:
cout << "unknown type " << perf.GetPlayId().GetPerfCode() << endl;
}
// add volume credits
volumeCredits += max(perf.GetAudience() - 30, 0);
// add extra credit for every ten comedy attendees
if (perf.GetPlayId().GetPerfCode() == PlayCode::COMEDY) volumeCredits += (perf.GetAudience() / 5);
result += "\t" + perf.GetPlayId().GetPerfName() + "\t" + to_string(thisAmount / 100) +
"(" + to_string(perf.GetAudience()) + " seats)\n";
totalAmount += thisAmount;
}
result += "Amount owed is " + to_string(totalAmount / 100) + "\n";
result += "You earned " + to_string(volumeCredits) + "credits\n";
return result;
}
private:
vector<Performance> performances;
string name;
};
我们先进行提取函数、搬移函数、函数上移等操作将上述代码中的计算逻辑与打印逻辑分离开来(Template Method重构),这一部分不是我们的重点,如果想了解具体重构请参考实践《重构》示例(C++版),下面才是我们要真正动刀的代码。
class Bill {
public:
Bill (string _customName) : name(_customName) {}
void AddPerformance(const Performance& perf) {
performances.push_back(perf);
}
string GetName() { return name; }
string GetBill() {
bill = "Statement for " + GetName() + "\n";
for (auto& perf : performances) { // 计算每场演出的名字、价格、观众人数。
bill += "\t" + perf.GetPlay().GetPerfName() + "\t" + to_string(perf.GetAmount() / 100) +
"(" + to_string(perf.GetAudience()) + " seats)\n";
}
bill += "Amount owed is " + to_string(totalAmount() / 100) + "\n";
bill += "You earned " + to_string(totalVolumeCredits()) + "credits\n";
return bill;
}
private:
int totalVolumeCredits() {...} // 计算所有演出的观众积分
int totalAmount() {...} //计算所有演出的费用
private:
vector<Performance> performances;
string name;
};
我们首先要提炼超类——AbstractBill(Factory Method:Ceator[DP])中包含一个抽象方法 GetBill,我们将其的实现交给子类TextBill 和HTMLBill,实现这一抽象方法的每个子类就都成了Factory Method:ConcreateCeator[DP]。重构后:
class AbstractBill {
public:
void AddPerformance(const Performance& perf) {
performances.push_back(perf);
}
string GetName() { return name; }
virtual string GetBill() = 0;
protected:
int totalVolumeCredits() {...}
int totalAmount() {...}
protected:
vector<Performance> performances;
string name;
};
class TextBill : public AbstractBill {
public:
TextBill(string _name) : AbstractBill(_name) {
cout << "The Text GetBill!\n";
}
string GetBill() override {
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;
}
};
class HTMLBill : public AbstractBill {
public:
HTMLBill(string _name) : AbstractBill(_name) {
cout << "The HTML GetBill!\n";
}
string GetBill() override {
string result = "<h1>Statement for " + GetName() + "</h1>\n";
result += "<table>\n";
result += "<tr><th>play</th><th>seats</th><th>cost</th></tr>\n";
for (auto& perf : performances) {
result += "<tr><td>" + perf.GetPlay().GetPerfName() + "</td><td>" + to_string(perf.GetAmount() / 100) +
"</td><td>" + to_string(perf.GetAudience()) + "</td><tr>\n";
}
result += "</table>\n";
result += "<p>Amount owed is <em>" + to_string(totalAmount() / 100) + "</em></p>\n";
result += "<p>You earned <em>" + to_string(totalVolumeCredits()) + "</em>credits</p>\n";
return result;
}
};
调用:
AbstractBill* it = new HTMLBill("john");
cout << it->GetBill() << endl;
思考:使用Factory Method真的比直接调用new 或 Creation Method简单嘛?答案显而易见。但是,使用Factory Method(进而与Template Method配合)的代码往往比在类中复制方法来应对变化要简单。当然所有的事情不可能是完美的,Factory Method亦然。观察上述代码中,我们必须向子类的实现中传递_name参数,而这个参数也许是一些子类并不需要的。
- 优点
- 减少因创建自定义对象而产生的重复代码。
- 有效地表达了对象创建发生的位置,以及如何重写对象的创建
- 强制Factory Method使用的类必须实现统一的类型
- 缺点
- 可能会向Factory Method的一些实现者传递不必要的参数。
系列文章
- 实践《重构与模式》(C++)——开篇(一)
更多推荐
实践《重构与模式》(C++)——Creation Method + Factory + Factory Method (二)
发布评论