OOP 中需要多态性吗?

What is the need Polymorphism in OOP?

我有一个关于 OOP 概念的愚蠢问题,我们选择多态性的原因是什么??

C++ 中的简单代码:

class Shape{
public:
virtual void draw(){ cout<<"Shape"<<endl;};
};

class Traingle: public Shape
{
public: void draw(){cout<<"Triangle"<<endl;}
};

class Rectangle: public Shape
{
public: void draw (){cout<<"Rectangle"<<endl;}
};

int main(){
Shape *ptr= new Traingle();
ptr->draw();
delete ptr;
return 7;
}

这里ptr->draw()函数会调用Triangle绘制,如果指向Rectangle则调用Rectangle绘制,即后期绑定。

创建 Base class 指针并将其指向不同的 class 有什么必要?我们可以在没有任何虚函数的情况下创建单独的 class 对象,并在需要时调用。 喜欢

int main(){
    Traingle tObj; 
    tObj->draw();
    Rectangle rObj; 
    rObj->draw();
}

这基本上做同样的事情;

为什么基本上是多态?为什么是虚拟的?

它有什么需要,或者使用它有什么不同 属性? 真实案例会有所帮助!!

想象一个基地 class Shape。它公开了一个 GetArea 方法。想象一下 Square class 和 Rectangle class,以及 Circle class。无需创建单独的 GetSquareAreaGetRectangleAreaGetCircleArea 方法,您只需在每个派生的 class 中实现一个方法。您不必知道您使用的是 Shape 的哪个子 class,您只需调用 GetArea 即可获得结果,而不管它是哪种具体类型。

看看这段代码:

#include <iostream>
using namespace std;

class Shape
{
public:
  virtual float GetArea() = 0;
};

class Rectangle : public Shape
{
public:
  Rectangle(float a) { this->a = a; }
  float GetArea() { return a * a; }
private:
  float a;
};

class Circle : public Shape
{
public:
  Circle(float r) { this->r = r; }
  float GetArea() { return 3.14f * r * r; }
private:
  float r;
};

int main()
{
  Shape *a = new Circle(1.0f);
  Shape *b = new Rectangle(1.0f);

  cout << a->GetArea() << endl;
  cout << b->GetArea() << endl;
}

这里要注意的一件重要事情是 - 您不必知道所使用的 class 的确切类型,只需知道基本类型,您就会得到正确的结果。这在更复杂的系统中也非常有用。

恕我直言,使用多态性的最佳示例是容器:

std::vector<Shape*> shapes;
shapes.push_back(new Triangle());
shapes.push_back(new Rectangle());

您可以遍历此容器而无需关心对象的实际类型:

for (int i=0;i<shapes.size();i++){
    shapes[i]->draw();
}

简单来说,多态就是可以把不同的形状放在同一个盒子里,把盒子里的所有东西都异类地对待。

扩展您的示例:

#include <iostream>
#include <memory>
#include <vector>

class Shape{
public:
    virtual void draw(){ std::cout<<"Shape"<< std::endl;};
};

class Traingle: public Shape
{
public:
    void draw() override
    {
        std::cout<<"Triangle"<< std::endl;
    }
};

class Rectangle: public Shape
{
public:
    void draw() override
    {
        std::cout<<"Rectangle"<< std::endl;
    }
};

int main(){

    std::vector<std::unique_ptr<Shape>> box_of_shapes;

    box_of_shapes.emplace_back(new Traingle);
    box_of_shapes.emplace_back(new Rectangle);

    for (const auto& pshape : box_of_shapes)
    {
        pshape->draw();
    }

    return 0;
}

Rectangle 是一个 Shape(因为它是从Shape公开派生的)因此它可以由处理的向量持有(指向)Shape。 但是,因为 draw 方法是虚拟的,所以即使调用者只是将其视为 Shape.

Rectangle 也会正确绘制

预期输出:

Triangle
Rectangle

多态性允许对相关类型的对象进行相同处理,从而允许重用代码。

考虑到您可能需要许多行为不同的子类:

struct Shape1: public Shape { /* .. */ }; // triangle
struct Shape2: public Shape { /* .. */ }; // rectangle
// ...
struct ShapeN: public Shape { /* .. */ }; // projection of rhombic triacontahedron

考虑到您可能需要处理由 Shape 指针数组指向的对象。

使用多态性,您需要一个向量和一个带有虚函数调用的循环:

std::vector<Shape*> v = get_shape_vector();
for(Shape* s : v)
    s->draw();

如果没有多态性,您将不得不为每种类型管理一个单独的数组并单独处理它们:

std::vector<Shape1> v1 = get_shape1_vector();
std::vector<Shape2> v2 = get_shape2_vector();
// ...
std::vector<ShapeN> vN = get_shapeN_vector();

for(Shape1& s : v1)
    s.draw();
for(Shape2& s : v2)
    s.draw();
// ...
for(ShapeN& s : vN)
    s.draw();

使用多态的 3 行代码比不使用多态的 3*N 行代码更容易维护。

考虑到您可能需要修改流程。也许您想在绘制之前添加一个函数调用。当您拥有多态性时,这很简单:

void pre_draw(Shape*);

for(Shape* s : v) {
    pre_draw(s);
    s->draw();
}

没有多态性,你需要定义几十个函数,并修改每一个循环:

void pre_draw1(Shape1&);
void pre_draw2(Shape2&);
// ...
void pre_drawN(ShapeN&);

for(Shape1& s : v1) {
    pre_draw1(s);
    s.draw();
}
for(Shape2& s : v1) {
    pre_draw2(s);
    s.draw();
}
// ...
for(ShapeN& s : v1) {
    pre_drawN(s);
    s.draw();
}

请考虑稍后添加形状。使用多态性,您只需定义新类型和虚函数。您可以简单地将指向它的指针添加到数组中,它们将像所有其他兼容类型的对象一样被处理。

struct ShapeN1: public Shape { /* .. */ }; // yet another shape

如果没有多态性,除了定义新类型外,您还必须为其创建一个新数组。您需要创建一个新的 pre_draw 函数。并且您需要添加一个新循环来处理它们。

void pre_drawN1(ShapeN1&);
// ...
std::vector<ShapeN1> vN1 = get_shapeN1_vector();
// ...
for(ShapeN1& s : vN1) {
    pre_drawN1(s);
    s.draw();
}

事实上,您需要遍历整个代码库,找到处理每种形状类型的地方,并在其中添加新类型的代码。


现在,N 可能很小也可能很大。 N越大,越避免重复多态性。但是,无论您的子类有多少,在添加新子类时不必查看整个代码库是一个很大的好处。

神奇的词是解耦。假设您正在创建一个应用程序来管理超市的存储。您几乎从一开始就知道您迟早会需要一种方法来存储和检索数据。但是,您还不知道哪种技术是最好的(例如,它可以是关系数据库或 NoSql 数据库)。

所以,你创建了一个接口(抽象class):

struct Serialize{
    virtual void save(Product product) = 0;
    ... other method here ...
};

现在,您可以继续开发应用程序的其他部分,只需实现内存中版本的 Serialize。

class InMemorySerialize : public Serialize {
    ... implement stuff here ...
};

依赖于 Serialize 的代码不关心使用哪个具体 class,唯一真正需要更改以修改 serialize 实现的地方是具体 class 的构造.但是 class 的构造很可能只在一个地方并且非常接近 main 函数(查看 strategy pattern 了解更多信息)。

//in the main (switching these lines you will use different implementations)
//the rest of your code will not change
unique_ptr<Serialize> serializer(new InMemorySerialize());
//unique_ptr<Serialize> serializer(new OnFileSerialize(myfolder));

此外,实现接口是实现 Open/Close principle 的最简单方法之一。如果你想支持序列化到文件而不是在内存中,你将创建一个新的 class ,它将在一个单独的文件中。因此,您愿意在不触及现有代码的情况下增加应用程序的功能。

最后一点,如果您依赖接口而不是具体对象,测试会容易得多。您可以轻松实现测试所需的行为。