如何处理已发布的抽象基类的更改?

3
我已经开发了一款名为“Kennel”的应用程序,可以照顾各种不同品种的狗。我的客户需要将他们的狗添加到我的应用程序中以获得服务。
因此,我定义了一个通用的“Dog”接口。客户需要实现该接口以创建具体的狗类型(例如拉布拉多、贵宾犬等),实例化并将它们加入我的狗舍应用程序(使用例如kennel::admitDog(dog *dog))。
以下是抽象的Dog基类:
class Dog {
public:
    Dog()
    {

    }

    virtual ~Dog()
    {

    }

    virtual void eatFood() = 0;
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;
};

我已发布了这个接口,我的客户已经开始使用它来创建他们自己的具体狗类型。在应用程序的下一个版本中,我计划支持狗舍中的机器狗。
问题来了。机器狗需要上面的抽象基类中的“Dog::rechargeBattery()”。而且,它不需要现有的“Dog::eatFood()”函数。将“Dog::rechargeBattery()”添加到上述抽象基类中会影响所有已经使用此接口的现有客户。他们将被强制实现“Dog::rechargeBattery()”并重新编译代码,这可能是不可取的。
1. 在这一点上的解决方案是什么? 2. 在最初的设计中,我应该做些什么以避免这个问题?

Dog中移除eatFood方法,并添加LiveDogRobotDog派生的抽象类? - user2100815
@NeilButterworth 但是,删除 eatFood 将会影响现有的客户端。他们都将被迫实现 LiveDog - binary_baba
是的,这是对#2的评论。但是对于#1,看起来需要进行一些重新设计和重新实现——这就是生活。 - user2100815
3个回答

0

这两个问题的答案相同,即仍然可以实现设计来解决问题并预防未来出现。为了处理实现不同操作子集的不同类别(例如你的例子中的狗),只需添加一个 API 来查找它们能够/需要的内容。然后处理程序/客户端(例如你的例子中的狗舍)就能够找出它们能够/需要的内容并相应地调用。

这种设计比基于知道所有可能的派生类和它们中每个类可以/需要的内容来确定需求/功能更加透明。

考虑这种方法。“我可以看出你是机器人(因为你散发着油味),所以我会给你充电。你(其他人)已经在我身上流口水了,所以你似乎是一只生物狗,这就是为什么我要喂你。”
将其与“你想要食物吗?好香肠?啊,你乞求,显然你想要它。来,好狗狗。”和“你的低电量指示灯闪烁,所以我会向你展示电源插座。好机器人。”进行比较。
重点是,如果某物乞求香肠并且有低电量指示灯,则可以在未被告知存在生物狗和机器狗以及新发明的半机械狗的情况下,同时给它充电和喂食。
(如果这让您感到恐惧,请原谅,这只是为了说明。)

是否需要食物或电力的指示器可以在基类中实现,避免对现有狗代码进行任何更改。该API对于任何您可以放在狗舍中的东西都有意义(并且可以抽象为具有一组可能有意义/无意义操作的任何类层次结构)。

为了实现这个概念,您可以向基类添加虚拟检查器方法,使狗舍能够找出狗的需求。通过这些方法的默认实现,所有现有的生物狗都将学会在其需求上提供正确的信息,而无需更改其实现。
对于尚未被任何人实现的机器人,您可以要求需求检查器的行为与默认值不同。
class Dog {
public:
    Dog()
    {

    }

    virtual ~Dog()
    {

    }

    virtual void eatFood() = 0;
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;

    virtual bool boNeedsFood(void)
    {return true; /* standard dog */
    }

    virtual bool boNeedsElectricity(void)
    {return false; /* standard dog */
    }

    virtual void rechargeBattery(void)
    {    /* optional exception handling, in case kennel is malfunctioning */;
         /* sorry for the mental image... */
    }
};

class robodog
: public dog
{

public:

    /* ... */ 

    virtual bool boNeedsFood(void)
    {    return false; /* standard dog */
    }

    virtual bool boNeedsElectricity(void)
    {    return true; /* standard dog */
    }

    virtual void eatFood(void)
    {/* optional exception handling, in case kennel is malfunctioning */;}

    virtual void rechargeBattery(void)
    {
        /* actual code */
    }

}

/* ... somewhere in kennel ... */

if (doginstance.needsFood())
{doginstance.eatFood();
} /* intentionally no "else", could be cyborg dog, which needs both */
if (doginstance.needsElectricity())
{ doginstance.rechargeBattery();
}

0

以下是我会做的:

#include <iostream>

using namespace std;

class Dog {
public:
    virtual ~Dog()
    {
    }

    virtual void eatFood() = 0;
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;
};

class RoboticDog : public Dog {
public:
    virtual void rechargeBattery() = 0;
};

class Pug : public Dog {
private:
    void eatFood()
    {
    }

    void takeBath()
    {
    }

    void play()
    {
        cout << "Pug::play()" << endl;
    }

    void sleep()
    {
    }
};

class Robo1 : public RoboticDog {
private:
    void eatFood()
    {
    }

    void takeBath()
    {
    }

    void play()
    {
        cout << "Robo1::play()" << endl;
    }

    void sleep()
    {
    }

    void rechargeBattery()
    {
        cout << "Robo1::rechargeBattery()" << endl;
    }
};

int main()
{
    Pug pug;
    Robo1 robo1;

    Dog *dogs[] = { &pug, &robo1 };

    for(unsigned i = 0; i < sizeof(dogs) / sizeof(Dog *); ++i) {
        dogs[i]->play();

        RoboticDog *robo = dynamic_cast<RoboticDog *>(dogs[i]);
        if(robo) // If dynamic_cast<> returned != nullptr, this is a RoboticDog
            robo->rechargeBattery();
    }
}

这段代码允许与现有客户端进行二进制兼容。他们的代码将继续正常工作,而新代码可以实现与Dog向后兼容的RoboticDog接口。

dynamic_cast<>安全地将指针和引用转换为类的上、下和侧面沿继承层次结构。这意味着,如果你拥有一个接口指针的对象还实现了另一个接口,那么dynamic_cast<SecondInterface *>(pointerToFirstInterface)将返回nullptr,如果底层对象没有实现SecondInterface

因此,您只需修改代码,检查之前使用的Dog *指针是否指向真正的RoboticDog对象,如果是,则可以自由地使用完整的RoboticDog接口。

这基本上就是他们在COM+中扩展接口的方式(在那里您可以看到一堆SomeInterfaceExThatInterface2抽象类)。


0

这个:

class Dog {
    virtual void eatFood() = 0;
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;
};

class Kennel {
    void admit (Dog*);
}

变成这样:

class DogLike {
    // virtual void eatFood() = 0; <-- removed
    virtual void takeBath() = 0;
    virtual void play() = 0;
    virtual void sleep() = 0;
};

class Dog : public DogLike {
    virtual void eatFood() = 0; // <-- added
};

class RoboticDog : public DogLike {
    virtual void rechargeBatteries() = 0;
};

class Kennel {
    void admit (DogLike*);
};

现有的客户端应该与您修改后的库源代码兼容(但几乎肯定不是二进制兼容)。实现二进制兼容可能是可能的,也可能不可能,但这是对于任何对已发布的Dog类的更改都是如此。

由于客户端应该知道他们正在交付给狗舍什么样的狗,因此可以修改狗舍接口以将活体狗与机器狗分开:

class Kennel {
  public:
    void admit (Dog*);
    void admit (RoboticDog*);
};

其他种类的狗将不被允许进入。

接下来呢?据说狗舍有设施为所有回答DogLike接口的狗提供服务,并为真实狗和机器狗分别提供设施。

void Kennel::admit (Dog* dog) {
    commonFacilities.admit(dog);
    messHall.admit(dog);
}

void Kennel::admit (RoboticDog* dog) {
    commonFacilities.admit(dog);
    chargingStation.admit(dog);
}

仍然没有看到演员。如果不想要两个公共的admit方法,可以将它们隐藏在一个幕墙后面,在幕后检查动态类型。

class Kennel {
    void admit (Dog*);
    void admit (RoboticDog*);
  public:
    void admit (DogLike* dog) {
      if (auto d = dynamic_cast<Dog*>(dog)) 
        admit (d);
      else if (auto d = dynamic_cast<RoboticDog*>(dog)) 
        admit (d);
      else
        throw UnknownDogTypeError;
    }
};

一个狗窝如何为DogLike对象提供食物或充电?即,如何基于指向DogLike的指针调用适当的方法? - Yunnosch
@Yunnosch 这是一个非常好的问题。Kennel可以使用dynamic_cast来区分不同类型的狗。我知道这是一个不太优雅的解决方案,但实际上不同种类的狗吃不同类型和数量的食物,所以狗舍需要想出一种分类狗的方法。或者,我们可以声明电力是机器狗所需的食物类型,从而避免整个问题。 - n. m.
谢谢夸奖。你想展示一下你会怎么做吗?其他的答案有一部分可以演示,我认为那样更有帮助。也许你可以展示如何以最简单的方式完成它。 - Yunnosch
@Yunnosch 我们可以提出两个问题。(1)如何从零开始设计整个系统,以便它可以处理两种或更多略微不兼容的狗?(2)在现有设计的基础上,如何以最少的痛苦方式进行更改,以实现接近(1)的状态?我根本没有尝试回答(1),因为这不是问题所在。我只是试图保持两个类的清晰和分离。假设单独的类的解决方案应该可以在我的答案之上实现。(注:此处指编程中的类,不是动物类别) - n. m.
@n.m. 感谢您的回答。实际上,我在一开始就问了如何避免这个问题。请参考我的第二个问题。如果可能的话,您能否编辑您的答案,包括原始设计应该如何? - binary_baba

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