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

3

我试图理解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:我在形状中定义了一个可变参数函数modifyShape,但是这种方式对我来说不太优雅。

1
我从未听说过is-like-a。你如何定义这种关系? - n. m.
“modifyShape函数可以根据用户参数修改形状的每个参数。” 这似乎不是一个清晰明确的需求。 "based on parameter of the user" 的确切含义是什么?如果用户输入5,我们有一个以(7,8)为中心的单位三角形和一个以(1,-2)为中心的单位圆,那么预期的结果是什么? - n. m.
1
从 API 的角度来考虑这个问题。 modifyShape 函数会做什么?你会传入所有新的参数吗?如果是这样,那为什么不创建移动和复制构造函数呢? - Alex Shirley
你可以让 modifyShape 函数接受可变数量的参数 (https://en.cppreference.com/w/cpp/utility/variadic)。你也可以使用初始化列表 (https://en.cppreference.com/w/cpp/utility/initializer_list)。但我认为这并不必要。我想不出在什么情况下会对一个指向 Shape 的指针调用 modifyShape 函数,而不知道它是 Triangle 还是 Circle -- 实际上,你不能这样做,因为如果你不知道它是哪种 Shape,你就不知道要传递多少个参数。 - Topological Sort
1
也许如果你从文件中加载类似于“Triangle 2 3 5”这样的行,但如果是这样做的话,可以这么说:if (typestring=="Triangle") ((Triangle*) shapeptr)->modifyTriangle(arg1, arg2, arg3); - Topological Sort
3个回答

0
现在,如果有这样的要求需要实现modifyShape函数...那么这个函数应该在基类中如何实现呢?
如何实现这个函数是一个主观问题,但我们可以通过以下方式来解决:
1.认识到这个函数可能会有哪些实现方式;
2.根据一些“最佳实践”建议来制定替代方案。
C++核心准则通常被称为“最佳实践”指南,它建议优先选择具体的常规类型。我们可以使用这个指南来回答这个问题,并提供一个这个函数和设计可能的实现方式。
首先,要理解多态类型和多态行为之间的区别。

多态类型是具有或继承至少一个虚函数的类型。这个形状类及其虚拟displayArea成员函数就是这样一种多态类型。在C++术语中,这些都是满足std::is_polymorphic_v<T>返回true的类型T

与非多态类型相比,多态类型在以下几个方面存在差异:

  1. 它们需要通过引用或指针来处理,以避免切片问题。
  2. 它们不是自然正则的。也就是说,它们不能像基本的C++类型int一样对待。

所以根据您提供的设计,以下代码将无法工作,但指导意见是它确实能工作:

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

而且我们可以编写一个“形状”访问函数来调用适当的“显示面积”。

虽然这样的解决方案更加“规范”,但使用“std::variant”在分配给其他类型的形状时是不开放的(除了它所定义的类型),而像“myShape = rectangle{1.5, 2.0};”这样的代码将无法工作。

与其使用“std::variant”,我们可以使用“std::any”。这将避免只支持为其定义的形状(就像使用“std::variant”一样)的缺点。使用此“形状”的代码可能如下所示:

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 

这里有一个链接,基本上展示了这段代码的编译情况,以及这个shape是一个具有多态行为的非多态类型。

虽然这种技术在使事情正常工作方面带来了负担,但也有努力让它变得更容易(例如P0201R2)。此外,对于已经熟悉PIMPL习惯用法的程序员来说,我不认为接受这一点会很困难,因为需要从引用语义和继承思维转向值语义和组合思维。


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

2
这是一个不好的建议。modifyShape实现将不得不转换ShapeParams参数。这很容易出错,而且你什么也得不到。你仍然需要将TriangleParams传递给三角形。 - Lior Kogan
我确实需要一个转换,但它将在作用域内完成,始终使用相同类型的ShapeParams。如果您传递指向其他类的指针,则可能会很危险,但几乎可以说这适用于任何事情。 通过这种方式获得的好处是可以使用虚拟调用来概括形状处理工作。例如,如果您使用模板方法设计模式,则可能会很方便。 - user8160628

0

你可以稍微重构一下你的类,但这需要另一个独立的类。你可以创建一组二维和三维数学向量类,但你需要拥有所有向量可以执行的重载运算符和数学函数,例如加、减、乘以向量或标量,如果是向量相乘,则需要考虑点积和叉积。你需要规范化方法、长度等。一旦你有了这些工作中的数学向量类,那么你就可以使用向量来重新设计你的形状类。或者,你可以使用数学库类,如GLM的数学库,用于在OpenGL中工作,而不是编写自己的向量类。它是免费和开源的,也是一个仅包含头文件的库。一旦你将库安装到路径中,你只需要包含它的头文件即可,无需担心链接问题。然后,使用这些向量类会使你的形状类中的数学计算更容易进行,并且更容易设计形状类:以下是伪代码示例:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接