如何处理已发布的摘要基础 class 的变化?

How to handle a change in the published abstract base class?

我开发了一个 "Kennel" 应用程序来照顾各种狗。我的客户应该允许他们的狗加入我的应用程序以使用这些服务。

所以,我定义了一个通用的 "Dog" 接口。客户端需要实现接口来创建具体的狗类型(比如拉布拉多犬、贵宾犬等),实例化它们并将它们接纳到我的犬舍应用程序(使用 say,kennel::admitDog(dog *dog)。

这里是 Dog 抽象基础 class:

class Dog {
public:
    Dog()
    {

    }

    virtual ~Dog()
    {

    }

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

我发布了这个接口,我的客户已经开始使用它来创建他们自己的具体 Dog 类型。下个版本的应用,我打算支持狗窝里的机器狗。

问题来了。机器狗在上面的抽象基class中需要Dog::rechargeBattery()。而且,它不需要现有的 Dog::eatFood() 功能。将 Dog::rechargeBattery() 添加到上述抽象基础 class 将影响所有已经在使用此接口的现有客户端。他们将被迫执行 Dog::rechargeBattery() 并重新编译。这可能是不可取的。

  1. 此时解决方案是什么?
  2. 在初始设计中我应该做些什么来避免这个问题?

这两个问题可以回答相同,即您仍然可以实施设计来解决现在的问题并防止将来出现问题。为了处理实现不同操作子集的不同 classes(您的示例中的狗),只需添加一个 API 即可找出它们 can/need。 handler/client(在你的例子中是狗窝)然后能够找出它们 can/need 并相应地调用。

与基于了解所有可能派生的 class 以及它们每个 can/need.

来确定 needs/capabilities 相比,这是一个更透明的设计

考虑这种方法。 "I can tell that you are a robot (because you reek of oil), so I will charge your battery. You (other) have salivated all over me, so you seem to be a biological dog, that is why I will feed you."
将其与 "Do you want food? Nice sausage? Ah, you beg, so obviously you want it. Here, nice doggy." 和 "Your low battery indicator flashes, so I will show you the power outlet. Nice robby."
进行比较 关键是,如果某物乞求香肠并且电池电量低,那么你可以充电并喂它 - 无需首先被告知不仅有生物狗和机器人狗,还有新发明的机器人狗。
(对不起,如果这很可怕,但这是为了说明。)

是否需要食物或电力的指标可以在基地class中实现,避免对现有狗代码进行任何更改。 API 对任何你可以放在狗窝里的东西都是有意义的(并且可以抽象到任何 class 具有潜在 meaningful/-less 操作集的层次结构)。

要实现这个概念,您可以将虚拟检查器方法添加到基础 class,这允许犬舍找出狗需要什么。通过这些方法的默认实现,所有现有的生物狗都将学会根据它们的需求提供正确的信息,而无需更改它们的实现。

对于机器人(尚未被任何人实施),您可以要求需求检查器的行为与默认行为不同。

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

我会这样做:

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

此代码允许与您现有的客户端二进制兼容。他们的代码将继续按原样工作,新代码可以实现 RoboticDog 接口,该接口向后兼容 Dog.

dynamic_cast<> 安全地将指针和引用转换为 类 沿继承层次结构向上、向下和横向。这意味着如果你有一个接口指针的对象也实现了另一个接口,dynamic_cast<SecondInterface *>(pointerToFirstInterface) 将 return nullptr 如果基础对象 不是 实施 SecondInterface.

因此您可以简单地修改您的代码来检查您之前使用的 Dog * 指针是否恰好指向真正的 RoboticDog 对象,如果是,那么您您可以自由使用完整的 RoboticDog 界面。

这几乎就是他们在 COM+ 中扩展接口的方式(您可以在其中看到一堆 SomeInterfaceExThatInterface2 抽象 类)。

这个:

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.

进行任何更改都是如此。

既然客户应该知道他们交给狗舍的是哪种狗,就可以修改狗舍界面以将活狗与机器狗分开:

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

仍然没有演员表。如果不希望有两个 public 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;
    }
};