用圆形和三角形设计形状类

时间:2017-06-21 03:49:22

标签: c++ design-principles

我试图理解is-a vs 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函数需要实现modifyShape函数,其中形状的每个参数都是根据用户的参数进行修改的,那么我应该如何更改我的类,以便我的is-a关系不会改变。当我看到它时,我觉得我必须在圆圈中定义一个参数modifyShape,在三角形中定义一个3参数modifyShape。但是这个函数在基类中应该怎么样?

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

选项2:我在形状中定义了一个变量参数函数{{1}},但不知怎的,这对我来说看起来并不干净。

3 个答案:

答案 0 :(得分:0)

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

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;
}

答案 1 :(得分:0)

你可以稍微重组一下你的课程,但这需要另一个独立的课程。您可以创建一组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() ) { /* ... * / }

};

然后一个Inherited类看起来像:

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;
}

答案 2 :(得分:0)

现在,如果要求需要实现ModifyShape函数,那么该函数在基类中应该是什么样?

此函数的外观是一个有问题的问题,但我们可以通过以下方法解决此问题:

  1. 识别该功能的外观,
  2. 替代方案着眼于一些“最佳实践”建议。

C++ Core Guidelines通常被称为“最佳实践”指南,它建议preferring concrete regular types。我们可以使用该指南来解决该问题,并提供一种可以使该功能和设计看起来更清晰的方式。

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

多态类型是具有或继承至少一个虚函数的类型。这个shape类及其虚拟displayArea成员函数就是这种多态类型。用C ++术语来说,它们都是T返回std:: is_polymorphic_v<T>的所有类型true

在此问题上,多态类型与非多态类型存在差异,如下所示:

  1. 它们需要通过引用或指针进行处理,以避免切片。
  2. 他们自然不是常规的。即它们不能像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并不能分配给其他类型的形状(除了为其定义的形状)和类似{{1}的代码}无效。

我们可以使用myShape = rectangle{1.5, 2.0};代替std::variant。这样可以避免仅支持std::any所定义的形状的缺点。然后,使用此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(); 的一个缺点是,它不会根据这些值的类型必须提供的任何概念功能来限制它可以采用的值。

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

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

shape

这里是a link的基本代码,它显示了代码的编译过程,并且此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 也是具有多态行为的非多态类型。

尽管此技术在使事情正常运行的机制上带来负担,但仍在努力使其变得更容易(例如P0201R2)。此外,对于已经熟悉PIMPL惯用语的程序员,我不会说这很难接受,就像从参考语义和继承方面的思考转向价值语义和组成方面的思考一样。