实践《重构与模式》(C++)——Creation Method + Factory + Factory Method (二)

编程入门 行业动态 更新时间:2024-10-15 16:22:13

文章目录

    • 用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替换构造函数进行重构:

  1. 提炼函数
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);
    }
};
  1. 搬移函数:将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);
        ...
    }
};
  1. 方法内联化: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 (二)

本文发布于:2023-06-13 17:58:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1387006.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:重构   模式   Creation   Method   Factory

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!