首页 > 解决方案 > OOP 课程设计:两个数字计算器

问题描述

问题是使用 OOP 编写两个数字计算器(+、-、*、/)。基于直觉(我还没学过设计模式),我写了这样的代码:

class Calculator {  
  public:
    double calculate(double a, char op, double b) {
        switch (op) {
        case '+':
            ...   
        }
   }

但是,我的朋友说它没有遵循,OCP然后他向我展示了以下代码:

class Operator {
    virtual double eval(double a, double b) = 0;
}

class Add : public Operator {
    double eval(double a, double b) { 
        return a + b;
    }
}
...
class Calculator {
  public:
    Calculator() {
        Operator* add = new Add();
        Operator* sub = new Sub();
        Operator* mul = new Mul();
        Operator* div = new Div();
        cal = {{'+', add}, {'-', sub}, {'*', mul}, {'/', div}};
    }

    double calculate(double a, char op, double b) {
        return cal.at(op)->eval(a, b);
    }

    ~Calculator() {
        for (auto i : cal) {
            delete i.second;
        }
    }

    private:
        bool isEqual(double a, double b) const {
            return fabs(a-b) < std::numeric_limits<double>::epsilon();
        }

        std::unordered_map<char, Operator*> cal;
};

嗯,它确实使用了 OOP 的四个基础,但它真的很好吗?我对第二个代码有以下疑问:

  1. 它真的遵循OCP吗?(因为我认为Calculator如果我们添加一个新的运算符,我们仍然需要修改代码)
  2. 引入太多的类不重要吗?
    例如,如果我写一个 tiny git,可能会有很多参数,比如initcommit等等。我是否应该更愿意为可能的命令引入这么多类,而不是只在第一个代码中使用 switch 表达式?
  3. 它是问题的完美解决方案吗?如果不是,那么完美的设计是什么?

标签: c++oopdesign-patterns

解决方案


没有一个完美的设计。您需要根据您期望更频繁地更改的内容以及语言为您提供的保证来做出权衡。这实际上是一个古老的辩论,如果您查找“模式匹配与子类型多态性”,您可以找到非常全面的文章。

您可能更喜欢第一种方法(函数式风格),如果您希望有很多switch语句为这些运算符赋予新的含义,这些含义不一定属于运算符本身(在 FP 中,行为并不真正属于类型)。想想这样的事情:

double identityValue(char op) { switch (op) .... }
bool resultOverflows(double a, char op, N double b) { switch (op) ... }

但是,当您添加新Operator类型时,您将不得不更改所有这些方法,并且您会希望语言提供模式匹配之类的功能,或者至少在编译时检查是否case耗尽。

使用多态方法,行为与类型保持一致,因此您的运算符将提供类似的方法resultOverflows(double a, double b)identityValue()等等。添加新的 Operator 子类型不会影响任何使用它们的现有代码(即无需在任何地方添加新的case,因为您没有switch任何地方)。

但是,当您添加新行为(另一种方法,例如identityValue(),如果您仅在项目后期才发现需要它)时,您将不得不更改所有现有的Operator子类型。


对于您的用例,出于以下几个原因,我会采用多态方法:

  • 这是使用 C++ 等 OOP 语言进行编程的自然方式(将数据和行为保持在一起)
  • C++ 中没有对模式匹配的真正支持,因此从长远来看,伪功能方法很难维护
  • 即使在对函数式方法有更好支持的语言中(比如 Scala,甚至 Rust),你也会经常发现将行为附加到类型上更自然,尤其是当它合理地属于那里时,我认为我的情况就是这样。上面的例子

推荐阅读