C++ 面向对象类设计:两个数字计算器

C++ 面向对象类设计:两个数字计算器,c++,oop,design-patterns,C++,Oop,Design Patterns,问题是使用OOP编写两个数字计算器(+、-、*、/)。基于直觉(我还没有学会设计模式),我编写了如下代码: 类计算器{ 公众: 双计算(双a、双字符运算、双b){ 开关(op){ 格“+”: ... } } 但是,我的朋友说它没有遵循OCP,然后他给我看了以下代码: 类运算符{ 虚拟双评估(双a,双b)=0; } 类添加:公共运算符{ 双值(双a,双b){ 返回a+b; } } ... 类计算器{ 公众: 计算器(){ 运算符*add=新添加(); 运算符*sub=新的sub(); 运算

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

类计算器{
公众:
双计算(双a、双字符运算、双b){
开关(op){
格“+”:
...   
}
}
但是,我的朋友说它没有遵循
OCP
,然后他给我看了以下代码:

类运算符{
虚拟双评估(双a,双b)=0;
}
类添加:公共运算符{
双值(双a,双b){
返回a+b;
}
}
...
类计算器{
公众:
计算器(){
运算符*add=新添加();
运算符*sub=新的sub();
运算符*mul=新的mul();
运算符*div=新div();
cal={{'+',add},{'-',sub},{'*',mul},{'/',div};
}
双计算(双a、双字符运算、双b){
返回校准(op)->评估(a、b);
}
~Calculator(){
用于(自动i:cal){
删除i.second;
}
}
私人:
布尔相等(双a,双b)常数{
返回fabs(a-b)
好吧,它确实使用了OOP的四个基本功能,但它真的很好吗?关于第二个代码,我有以下问题:

  • 它真的遵循了
    OCP
    ?(因为我认为如果我们添加一个新的操作符,我们仍然需要在
    Calculator
    中修改代码)
  • 引入太多的类不是很重要吗?
    例如,如果我编写一个很小的
    git
    ,可能会有很多参数,比如
    init
    commit
    等等。我应该为可能的命令引入这么多的类,而不是在第一个代码中使用开关表达式吗
  • 它是问题的完美解决方案吗?如果不是,它的完美设计是什么

  • 首先,最好避免直接使用
    new
    /
    delete
    ,除非你真的需要它们并且你知道你在做什么。在你的情况下,不需要它们。相反,使用
    std::unique_ptr
    std::shared_ptr

    其次,如果您想在不修改
    计算器
    类的情况下修改操作集,则只需提供一种方法即可。例如,这样的函数就足够了:

    void Calculator::addOperator(char op, Operator *obj) {
        cal.insert({op, obj});
    }
    

    这应该回答了你的第二个问题,因为你不能用
    开关做类似的事情,所以没有一个完美的设计。这是一个权衡,你需要根据你期望更频繁的改变和语言为你提供的保证来做。这实际上是一个古老的争论,你可以发现如果你查阅“模式匹配与子类型多态性”这篇综合性文章

    如果您希望有大量的
    switch
    语句为这些运算符赋予新的含义,而这些含义不一定属于运算符本身(在FP中,行为实际上并不属于类型),那么您可能更喜欢第一种方法(函数式)。请考虑以下内容:

    double identityValue(char op) { switch (op) .... }
    bool resultOverflows(double a, char op, N double b) { switch (op) ... }
    
    但是,当您添加一个新的
    运算符
    类型时,您必须更改所有这些方法,并且您希望该语言提供类似于模式匹配的功能,或者至少对
    大小写进行编译时检查

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

    但是,当您添加一个新的行为(另一种方法,如
    identityValue()
    ,以防您以后在项目中才发现需要它)时,您必须更改所有现有的
    操作符
    子类型


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

      <> LI>是一种自然的方式,用OOP语言编程,如C++(保持数据和行为在一起) <> LI>在C++中没有模式匹配的真正支持,所以伪函数方法在长期运行中难以维持。
    • 即使在一种对函数方法有更好支持的语言中(比如Scala,甚至Rust),您也会发现将行为附加到类型上更自然,尤其是当它合理地属于类型时,正如我上面的示例所示

    你说的是什么“OOP的四个基础”?OCP指的是开放/封闭原则吗?还有一个原因,因为简单就是最简单的(有时)。请注意你的最后一个问题“什么是完美的设计”这在很大程度上是一个自以为是的问题,因此被认为是堆栈溢出的话题。而且,
    git
    命令的数量永远不应该成为如何编写代码的基础--
    git
    与软件设计完全正交。@StephenNewell OOP的四个基本概念意味着封装、继承、抽象和多态。以及OCP表示打开/关闭principle@Human-编译器为“完美的设计”感到抱歉问题,下次我不会问这样的问题。模式匹配在c++/java中不受支持,所以如果我们希望有很多情况,
    switch
    expression是一个更好的选择吗?我会说不,java/c++的自然方法是多态方法。但我真的不知道你说的很多类是什么意思:你在很多地方做吗
    switch
    或许多操作符子类型?我在回答中添加了一个建议,只是想让你知道我会选择什么以及为什么。但这可能有偏见,因为多态方法自然是我的第一选择,而不考虑语言