【问题标题】:designing shape class with circle and triangle用圆形和三角形设计形状类
【发布时间】:2017-06-21 03:49:21
【问题描述】:

我试图理解 is-a 与 is-like-a 的关系,我在某处读到我们必须尝试遵循设计,以便我们始终拥有 is-a 关系而不是 is-like-a。考虑形状基类和派生三角形和圆形类的经典示例。所以圆形是一个形状,三角形也是一个形状。功能显示区域在基类中定义。现在下面的程序运行良好。

#include "stdafx.h"
#include <cmath>
#include <iostream>

class shape
{
public:
    virtual void displayArea()=0;
};

class circle :public shape
{
    int radius;
public:
    circle(int radius2) :radius(radius2){  }
    void displayArea()
    {
        double area = 3.14*radius*radius;
        std::cout << " \n Area circle" << area<<std::endl;
    }
};

class triangle :public shape
{
    double a,b,c;
public:
    triangle(double a1, double b1, double c1): a(a1), b(b1),c(c1)
    {
        if (a + b > c && a + c > b && b + c > a)
            std::cout << "The sides form a triangle" << std::endl;
        else
            std::cout << "The sides do not form a triangle. Correct me !" << std::endl;
        
    }

    void displayArea()
    {
        double s = (a + b + c) / 2;
        double area = sqrt(s*(s - a)*(s - b)*(s - c));
        std::cout << " \n Area triangle"<< area<<std::endl;
    }
};

void main()
{
    shape * p1[2];
    p1[0]= new circle(20);

    p1[1] = new triangle(5.6,8.1,10.3);
    for (int i = 0; i < 2; ++i)
    {
        p1[i]->displayArea();
    }

    int y;
    std::cin >> y;
}

现在,如果需要实现modifyShape 函数,其中形状的每个参数都根据用户的参数进行修改,那么我应该如何更改我的类以使我的 is-a 关系不改变。当我看到它时,我觉得我必须在圆圈中定义一个参数modifyShape,在三角形中定义一个三参数modifyShape。但是这个函数在基类中应该是什么样子呢?

选项 1:我在形状上定义了单个参数和两个参数 modifyShape 函数,但这意味着我将在圆形中有一个额外的 2 个参数函数,在三角形中有一个额外的 1 个参数函数。

选项 2:我在形状上定义了一个可变参数函数 modifyShape,但不知何故这对我来说看起来并不干净。

【问题讨论】:

  • 我从未听说过 is-like-a。您如何定义这种关系?
  • "modifyShape 函数,其中形状的每个参数都根据用户的参数进行修改" 这看起来不像是一个明确的要求。 “基于用户的参数”究竟是什么意思?如果用户输入 5,我们有一个以 (7, 8) 为中心的单位三角形和一个以 (1, -2) 为中心的单位圆,预期的结果是什么?
  • 从 API 的角度考虑这个问题。 modifyShape 会做什么?你会传入所有新参数吗?如果是这样,为什么不创建移动和复制构造函数呢?

标签: c++ design-principles


【解决方案1】:

您可以使用第三个选项,您可以创建一个新的类层次结构 (或结构)将表示每个形状的参数。然后你可以通过 指向基类的指针作为虚函数的参数。 例如:

struct ShapeParams
{
     ...
}

struct TriangleParams : public ShapeParams
{
     double a;
     double b;
     double c:
}
class shape
{
  public:
    virtual void displayArea()=0;
    modifyShape (ShapeParams*) = 0;
};

class triangle :public shape
{
  public:
     void modifyShape (ShapeParams*) = override;

  private:
     TriangleParams m_params;
}

【讨论】:

  • 这是一个糟糕的建议。 modifyShape 实现必须强制转换 ShapeParams 参数。它很容易出错,而且您一无所获。您仍然需要将 TriangleParams 传递给三角形。
  • 我确实需要一个演员表,但它将在始终使用相同类型的 ShapeParams 的范围内完成。如果你传递一个指向其他类的指针可能会很危险,但这几乎可以说是任何事情。您通过这种方式获得的是一个虚拟调用,可用于概括使用形状的工作。例如,如果您使用模板方法设计模式,可能会很方便。
【解决方案2】:

你可以稍微重组你的类,但这需要另一个独立的类。您可以创建一组 2D 和 3D 数学矢量类,但您需要拥有矢量可以执行的所有重载运算符和数学函数,例如矢量或标量的加、减、乘,如果通过矢量,您有点和需要担心的交叉产品。您将需要规范化方法、长度等。一旦你有了这些工作数学向量类。然后,您可以改用向量重新设计形状类。或者,您可以使用数学库类,例如用于在 OpenGL 中工作的 GLM 的数学库,而不是编写自己的矢量类。它是免费和开源的,它也是一个只有标题的库。将库安装到路径后,您需要做的就是包含其标题。您不必担心链接。然后使用这些矢量类,可以更轻松地进行形状类中的数学运算,并且更容易设计形状类:以下是伪代码的示例:

#include <glm\glm.hpp>
// Needed If Doing Matrix Transformations: Rotation, Translation Scaling etc.
// #include <glm\gtc\matrix_transform.hpp> 

class Shape {
public:
    enum Type {
        NONE = 0,
        TRIANGLE,
        SQUARE,
        CIRCLE,
     };
protected:
    Type type_;
    glm::vec4 color_ { 1.0f, 1.0f, 1.0f, 1.0f }; // Initialize List Set To White By Default
    double perimeter_; // Also Circumference for Circle
    double area_;     
    // double volume_; // If in 3D.
public:
     // Default Constructor
     Shape() : type_( NONE ), color_( glm::vec4( 1.0f, 1.0f, 1.0f, 1.0f ) ) {}       
     // User Defined Constructors
     // Sets Shape Type Only Color Is Optional & By Default Is White
     explicit Shape( Type type, glm::vec4 color = glm::vec4() ) : type_(type), color_( color ) {}

     Type getType() const { return type_; }
     void setType( Shape::Type type ) {
         if ( type_ == NONE ) {
             // Its okay to set a new shape type
             type_ = type;
          } 

          // We Already Have a Defined Shape
          return;
      }

      // Getters That Are Commonly Found Across All Shapes
      double getPerimeter() const { return perimeter_; }
      double getArea() const { return area_; }

      // Common Functions that can be done to any shape
      void setSolidColor( glm::vec4 color ) { color_ = color };
      glm::vec4 getColor() const { return color; }

      // Common Interface That All Shapes Share But Must Override
      virtual double calculateArea() = 0;
      virtual double calculatePerimeter() = 0; 

      // Since we do not know what kind of shape to modify until we have one
      // to work with, we do not know how many parameters this function will need.
      // To get around this we can use a function template and then have overloads 
      // for each type we support
      template<typename Type = Shape>
      virtual void modify( Type* pShape /*,glm::vec3... params*/ );

      // Overloaded Types: - Should Be Defined & Overridden By the Derived Class
      virtual void modify<Triangle>( Triangle* pTriangle, glm::vec3, glm::vec3, glm::vec3, glm::vec4 = glm::vec4() ) { /* ... */ }
      virtual void modify<Circle>( Cirlce* pCircle, float radius, glm::vec4 color = glm::vec4() ) { /* ... * / }

};

那么继承的类看起来像:

class Triangle : public Shape {
public:
     // Could Be An Option To Where This is a base class as well to specific types of triangles:
     enum TriangleType {
         Acute = 0,
         Right,
         Equilateral,
         Obtuse
     } // then each of these would have properties specific to each type
private:
    glm::vec3[3] vertices_;

public:
    // Default Constructor
    Triangle() : Shape(TRIANGLE) {} // Sets The Shape Type But Has No Vertices Or Area; just default construction
    // Vertices But No Color
    Triangle( glm::vec3 A, glm::vec3 B, glm::vec3 C ) : Shape(TRIANGLE) {
        vertices_[0] = A;
        vertices_[1] = B;
        vettices_[2] = C;

        // Call These To Have These Values
        calculatePerimeter();
        calculateArea();            
    }
    // Vertices & Color
    Triangle( glm::vec3 A, glm::vec3 B, glm::vec3 C, glm::vec4 color ) : Shape(TRIANGLE) {
        vertices_[0] = A;
        vertices_[1] = B;
        vertices_[2] = C;

        calculatePerimeter();
        calculateArea();
     }

     // No Need To Do The Set & Get Colors - Base Class Does that for you.

     // Methods that this shape must implement
     virtual double calculateArea() override {
         // Calculations For Getting Area of A Triangle
         area_ = /* calculation */;
     };
     virtual double calculatePerimeter() override {
         // Calculations For Getting Perimeter of A Triangle
         perimeter_ = /* calculation */;
     };

     void modify<Triangle>( Triangle* pTriangle, glm::vec3, glm::vec3, glm::vec3, glm::vec4 = glm::vec4() ) override { /* ... */ }

};

现在至于显示信息;我个人不会在这些类中实现这一点。只需使用您的标准 std::coutstd::ofstream 等将值打印到屏幕或使用 getters 购买的文件,例如:

#include <iostream>
#include "Triangle.h"

int main() {
    Triangle t1( glm::vec3( 0.0f, 1.0f, -1.3f ),   // Vertex A
                 glm::vec3( 3.2f, 5.5f, -8.9f ),   //        B
                 glm::vec3( -4.5f, 7.6f, 8.2f ),   //        C
                 glm::vec4( 0.8f, 0.9f, 0.23f, 1.0f ) ); // Color

    std::cout << "Perimeter is " << t1.getPerimeter() << std::endl;
    std::cout << "Area is " << t1.getArea() << std::endl;

    return 0;
}

【讨论】:

    【解决方案3】:

    现在如果需要实现 modifyShape 函数……这个函数在基类中应该是什么样子?

    这个函数的外观是一个见仁见智的问题,但让我们通过以下方式解决这个问题:

    1. 识别函数的外观,并
    2. 基于一些“最佳做法”建议的替代外观。

    C++ Core Guidelines 通常被称为“最佳实践”指南,它建议使用preferring concrete regular types。我们可以使用该指南来解决问题,并提供此功能和设计的外观。

    首先,了解多态类型多态行为之间存在差异。

    多态类型是具有或继承至少一个虚函数的类型。这个shape 类和它的虚拟displayArea 成员函数就是这样一种多态类型。在 C++ 术语中,这些都是 T 类型,std:: is_polymorphic_v&lt;T&gt; 会为其返回 true

    就这个问题而言,多态类型与非多态类型的区别如下:

    1. 它们需要通过引用或指针来处理以避免切片。
    2. 它们不是自然规律的。 IE。它们不能被视为像int 这样的基本 C++ 类型。

    因此,以下代码不适用于您提供的设计,但指导是它确实有效:

    auto myShape = shape{triangle{1.0, 2.0, 2.0}}; // triangle data is sliced off
    myShape.displayArea(); // UB: invalid memory access in displayArea
    myShape = circle(4); // now circle data is sliced off from myShape
    myShape.displayArea(); // UB: also invalid memory access is displayArea
    

    同时,更重要的是shape多态行为,因此形状可以是圆形或三角形。如您所见,使用多态类型是一种提供多态行为的方法,但它不是唯一的方法,并且存在您询问如何解决的问题。

    提供多态行为的另一种方法是使用标准库类型,如std::variant,并定义shape,如:

    class circle {
        int radius;
    public:
        circle(int radius2) :radius(radius2){  }
        void displayArea() {
            double area = 3.14*radius*radius;
            std::cout << " \n Area circle" << area<<std::endl;
        }
    };
    
    class triangle {
        double a,b,c;
    public:
        triangle(double a1, double b1, double c1): a(a1), b(b1),c(c1) {
            if (a + b > c && a + c > b && b + c > a)
                std::cout << "The sides form a triangle" << std::endl;
            else
                std::cout << "The sides do not form a triangle. Correct me !" << std::endl;
        }
    
        void displayArea() {
            double s = (a + b + c) / 2;
            double area = sqrt(s*(s - a)*(s - b)*(s - c));
            std::cout << " \n Area triangle"<< area<<std::endl;
        }
    };
    
    using shape = std::variant<triangle,circle>;
    
    // Example of how to modify a shape
    auto myShape = shape{triangle{1.0, 2.0, 2.0}};
    myShape = triangle{3.0, 3.0, 3.0};
    

    并且可以编写一个shape访问函数来调用相应的displayArea

    虽然这样的解决方案更常规,但使用 std::variant 不会在分配给其他类型的形状(除了它定义的形状)时打开,并且像 myShape = rectangle{1.5, 2.0}; 这样的代码不会工作。

    我们可以使用std::any,而不是std::variant。这将避免只支持像std::variant 一样定义的形状的缺点。使用此 shape 的代码可能如下所示:

    auto myShape = shape{triangle{1.0, 2.0, 2.0}};
    myShape = triangle{3.0, 3.0, 3.0};
    std::any_cast<triangle&>(mShape).displayArea();
    myShape = rectangle{1.5, 2.0};
    std::any_cast< rectangle&>(mShape).displayArea();
    

    然而,使用 std::any 的一个缺点是它不会根据这些值的类型必须提供的任何概念功能来限制它可以采用的值。

    我将描述的最后一个替代方案是 Sean Parent 在他的演讲 Inheritance Is The Base Class of Evil 和其他地方描述的解决方案。人们似乎决定调用这些类型:多态值类型。我喜欢将此解决方案描述为扩展了更熟悉的 pointer to implementation (PIMPL) 成语。

    以下是shape 类型的多态值类型示例(省略了一些内容以便于说明):

    class shape;
    
    void displayArea(const shape& value);
    
    class shape {
    public:
        shape() noexcept = default;
    
        template <typename T>
        shape(T arg): m_self{std::make_shared<Model<T>>(std::move(arg))} {}
    
        template <typename T, typename Tp = std::decay_t<T>,
            typename = std::enable_if_t<
                !std::is_same<Tp, shape>::value && std::is_copy_constructible<Tp>::value
            >
        >
        shape& operator= (T&& other) {
            shape(std::forward<T>(other)).swap(*this);
            return *this;
        }
    
        void swap(shape& other) noexcept {
            std::swap(m_self, other.m_self);
        }
    
        friend void displayArea(const shape& value) {
            if (value.m_self) value.m_self->displayArea_();
        }
    
    private:
        struct Concept {
            virtual ~Concept() = default;
            virtual void displayArea_() const = 0;
            // add pure virtual functions for any more functionality required for eligible shapes
        };
    
        // Model enforces functionality requirements for eligible types. 
        template <typename T>
        struct Model final: Concept {
            Model(T arg): data{std::move(arg)} {}
            void displayArea_() const override {
                displayArea(data);
            }
            // add overrides of any other virtual functions added to Concept
            T data;
        };
    
        std::shared_ptr<const Concept> m_self; // Like a PIMPL
    };
    
    struct circle {
        int radius = 0;
    };
    
    // Function & signature required for circle to be eligible instance for shape
    void displayArea(const circle& value) {
         // code for displaying the circle
    }
    
    struct triangle {
        double a,b,c;
    };
    
    // Function & signature required for triangle to be eligible instance for shape
    void displayArea(const triangle& value) {
         // code for displaying the triangle
    }
    
    // Now we get usage like previously recommended...
    auto myShape = shape{triangle{1.0, 2.0, 2.0}}; // triangle data is saved
    displayArea(myShape); // calls displayArea(const triangle&)
    myShape = circle{4}; // now circle data is stored in myShape
    displayArea(myShape); // now calls displayArea(const circle&)
    
    // And changing the settings like a modifyShape function occurs now more regularly
    // by using the assignment operator instead of another function name...
    mShape = circle{5}; // modifies shape from a circle of radius 4 to radius 5 
    

    a link 基本上是这段代码,它显示了代码正在编译,并且这个 shape 也是具有多态行为的非多态类型。

    虽然这种技术在使事情正常运转的机制方面带来了负担,但也有人努力让这变得更容易(例如P0201R2)。此外,对于已经熟悉 PIMPL 习语的程序员来说,我不会说这很难接受,就像从引用语义和继承方面的思考转变为值语义和组合方面的思考一样。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-08-12
      • 2013-04-20
      • 1970-01-01
      • 1970-01-01
      • 2017-05-23
      • 2014-12-10
      • 1970-01-01
      • 2017-01-20
      相关资源
      最近更新 更多