天天看点

实践《重构:改善既有代码的设计(第2版)》示例(C++版)(二)

文章目录

    • 新的需求
    • 生成继承体系用于计算
    • 总结与思考
    • 系列文章

在上一篇文章 《重构:改善既有代码的设计(第2版)》示例(C++版)(一)中,我们对一个有关于剧目演出的代码,并且花费了大量的动作将其重构,在重构的过程中保持重构-测试-提交这种小步快跑的模式,最终将原来大泥团式的代码拆分成结构比较清晰,易于阅读的代码。

但是在上篇文章末尾,留下下来一个新的需求问题需要解决,即剧团需要支持更多类型的剧种,以及支持它们各自的价格计算和观众量积分计算。

新的需求

时代在发展,剧团也要改革进步,未来会引进更多的剧种,他们的结算方式不尽相同。为了应对未来将要到来的变化,我们需要做一些改变。但是由于未来引入什么剧种,那么其结算方式也都是未定的。但有一点可以确定的是,一旦有新的剧种出现,我们就得添加新的结算方式。

按照当前的结构,我只需要在计算函数里面添加分支逻辑即可。AmountForPerf函数清楚体现了,戏剧类型在计算分支的选择上起着关键的作用。但这样的分支逻辑很容易随着代码堆积而腐化。其实在看到switch出现时候就应该保持有防止代码腐化的警惕之心。

最好不要在另一个对象的属性基础上运用switch语句。如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。我们尝试抽象出一个抽象类IPlay,它有成员函数GetAmount。为了消灭switch,我们需要为每一个原来的枚举类型实现一个子类,在每个子类中实现自己的计算方法。下面是具体的UML图。

实践《重构:改善既有代码的设计(第2版)》示例(C++版)(二)

生成继承体系用于计算

千万不要心急,不如我们再往前多走一点点。原有的设计存在一个小问题:一个剧目是可以在其生命周期内修改自己的计算费用的算法的。当剧团期望剧目根据淡季旺季修改其结算方式时候,我们并不能在每个剧种生命周期内修改自己结算方式。

为解决这个问题,这时候我们就需要设计模式了——State(状态)模式。至于说具体应该是State(状态)模式还是Strategy(策略)模式,那就是另一个话题了。我们来看UML图:

实践《重构:改善既有代码的设计(第2版)》示例(C++版)(二)

要为程序引入结构、显示地表达出“计算逻辑的差异是由类型代码确定”有许多途径,不过最自然的解决办法还是使用面向对象世界里的一个经典特性——类型多态。以多态取代条件表达式也是常见的重构手法之一。在具体实施之前,我的设想是建立一个继承体系,它有两个子类,分别对应着喜剧和悲剧,再将其独有的计算体系搬移进子类的具体实现中去。这里增加了间接层——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图变成了

实践《重构:改善既有代码的设计(第2版)》示例(C++版)(二)

终于来到了最后阶段,有了前期铺路,实现这一部分已经变得不再困难。再对最后的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++版)(二)