如何处理已发布的摘要基础 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()
并重新编译。这可能是不可取的。
- 此时解决方案是什么?
- 在初始设计中我应该做些什么来避免这个问题?
这两个问题可以回答相同,即您仍然可以实施设计来解决现在的问题并防止将来出现问题。为了处理实现不同操作子集的不同 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+ 中扩展接口的方式(您可以在其中看到一堆 SomeInterfaceEx
和 ThatInterface2
抽象 类)。
这个:
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;
}
};
我开发了一个 "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()
并重新编译。这可能是不可取的。
- 此时解决方案是什么?
- 在初始设计中我应该做些什么来避免这个问题?
这两个问题可以回答相同,即您仍然可以实施设计来解决现在的问题并防止将来出现问题。为了处理实现不同操作子集的不同 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+ 中扩展接口的方式(您可以在其中看到一堆 SomeInterfaceEx
和 ThatInterface2
抽象 类)。
这个:
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;
}
};