钻石继承 (C++)
-
22-08-2019 - |
题
我知道拥有钻石继承被认为是不好的做法。然而,我有两个案例,我觉得钻石继承非常适合。我想问,在这些情况下你会建议我使用钻石继承,还是有其他设计可以更好。
情况1: 我想创建代表系统中不同类型“操作”的类。这些动作按几个参数分类:
- 该操作可以是“读”或“写”。
- 该动作可以有延迟或无延迟(不只是 1 个参数。它显着改变了行为)。
- 操作的“流类型”可以是 FlowA 或 FlowB。
我打算有以下设计:
// abstract classes
class Action
{
// methods relevant for all actions
};
class ActionRead : public virtual Action
{
// methods related to reading
};
class ActionWrite : public virtual Action
{
// methods related to writing
};
class ActionWithDelay : public virtual Action
{
// methods related to delay definition and handling
};
class ActionNoDelay : public virtual Action {/*...*/};
class ActionFlowA : public virtual Action {/*...*/};
class ActionFlowB : public virtual Action {/*...*/};
// concrete classes
class ActionFlowAReadWithDelay : public ActionFlowA, public ActionRead, public ActionWithDelay
{
// implementation of the full flow of a read command with delay that does Flow A.
};
class ActionFlowBReadWithDelay : public ActionFlowB, public ActionRead, public ActionWithDelay {/*...*/};
//...
当然,我会遵守任何两个动作(继承自 Action 类)都不会实现相同的方法。
案例2: 我在我的系统中实现了“命令”的复合设计模式。一条命令可以被读、写、删除等。我还想要一个命令序列,也可以读取、写入、删除等。命令序列可以包含其他命令序列。
所以我有如下的设计:
class CommandAbstraction
{
CommandAbstraction(){};
~CommandAbstraction()=0;
void Read()=0;
void Write()=0;
void Restore()=0;
bool IsWritten() {/*implemented*/};
// and other implemented functions
};
class OneCommand : public virtual CommandAbstraction
{
// implement Read, Write, Restore
};
class CompositeCommand : public virtual CommandAbstraction
{
// implement Read, Write, Restore
};
此外,我还有一种特殊的命令,“现代”命令。单一命令和复合命令都可以是现代命令。“现代”向一个命令和复合命令添加了一系列属性(这两个命令的属性大多相同)。我希望能够保存一个指向 CommandAbstraction 的指针,并根据所需的命令类型对其进行初始化(通过 new)。所以我想做以下设计(除了上面的之外):
class ModernCommand : public virtual CommandAbstraction
{
~ModernCommand()=0;
void SetModernPropertyA(){/*...*/}
void ExecModernSomething(){/*...*/}
void ModernSomethingElse()=0;
};
class OneModernCommand : public OneCommand, public ModernCommand
{
void ModernSomethingElse() {/*...*/};
// ... few methods specific for OneModernCommand
};
class CompositeModernCommand : public CompositeCommand, public ModernCommand
{
void ModernSomethingElse() {/*...*/};
// ... few methods specific for CompositeModernCommand
};
再次,我将确保从 CommandAbstraction 类继承的两个类不会实现相同的方法。
谢谢。
解决方案
继承是 C++ 中第二强(耦合性更强)的关系,仅次于友谊。如果您可以重新设计为仅使用组合,您的代码将更加松散耦合。如果不能,那么您应该考虑所有的类是否都应该真正从基类继承。是由于实现还是只是一个接口?您想要使用层次结构的任何元素作为基本元素吗?或者只是层次结构中的叶子才是真正的操作?如果只有叶子是操作,并且您要添加行为,则可以考虑针对此类行为组合进行基于策略的设计。
这个想法是,可以在小类集中定义不同的(正交)行为,然后将其捆绑在一起以提供真正的完整行为。在示例中,我将仅考虑一项策略,该策略定义是现在执行还是将来执行操作,以及要执行的命令。
我提供了一个抽象类,以便模板的不同实例可以存储(通过指针)在容器中或作为参数传递给函数并被多态调用。
class ActionDelayPolicy_NoWait;
class ActionBase // Only needed if you want to use polymorphically different actions
{
public:
virtual ~Action() {}
virtual void run() = 0;
};
template < typename Command, typename DelayPolicy = ActionDelayPolicy_NoWait >
class Action : public DelayPolicy, public Command
{
public:
virtual run() {
DelayPolicy::wait(); // inherit wait from DelayPolicy
Command::execute(); // inherit command to execute
}
};
// Real executed code can be written once (for each action to execute)
class CommandSalute
{
public:
void execute() { std::cout << "Hi!" << std::endl; }
};
class CommandSmile
{
public:
void execute() { std::cout << ":)" << std::endl; }
};
// And waiting behaviors can be defined separatedly:
class ActionDelayPolicy_NoWait
{
public:
void wait() const {}
};
// Note that as Action inherits from the policy, the public methods (if required)
// will be publicly available at the place of instantiation
class ActionDelayPolicy_WaitSeconds
{
public:
ActionDelayPolicy_WaitSeconds() : seconds_( 0 ) {}
void wait() const { sleep( seconds_ ); }
void wait_period( int seconds ) { seconds_ = seconds; }
int wait_period() const { return seconds_; }
private:
int seconds_;
};
// Polimorphically execute the action
void execute_action( Action& action )
{
action.run();
}
// Now the usage:
int main()
{
Action< CommandSalute > salute_now;
execute_action( salute_now );
Action< CommandSmile, ActionDelayPolicy_WaitSeconds > smile_later;
smile_later.wait_period( 100 ); // Accessible from the wait policy through inheritance
execute_action( smile_later );
}
使用继承允许通过模板实例化来访问策略实现中的公共方法。这不允许使用聚合来组合策略,因为不能将新函数成员推送到类接口中。在示例中,模板取决于具有 wait() 方法的策略,该方法对于所有等待策略都是通用的。现在等待一个时间段需要一个固定的时间段,通过 period() 公共方法设置。
在示例中,NoWait 策略只是 WaitSeconds 策略的一个特定示例,其中周期设置为 0。这是有意标记策略接口不需要相同。另一种等待策略实现可以通过提供一个注册为给定事件回调的类来等待一定数量的毫秒、时钟滴答或直到某个外部事件。
如果您不需要多态性,您可以从示例中取出基类和虚拟方法。虽然对于当前示例来说这似乎过于复杂,但您可以决定添加其他策略。
虽然如果使用普通继承(具有多态性),添加新的正交行为将意味着类数量呈指数增长,但使用这种方法,您可以单独实现每个不同的部分,并将其在 Action 模板中粘合在一起。
例如,您可以使操作定期进行,并添加退出策略来确定何时退出定期循环。首先想到的选项是 LoopPolicy_NRuns 和 LoopPolicy_TimeSpan、LoopPolicy_Until。这个策略方法(在我的例子中是 exit() )为每个循环调用一次。第一个实现对固定次数(由用户固定,如上例中的周期固定)后被调用退出的次数进行计数。第二种实现将在给定时间段内定期运行该进程,而最后一个实现将运行该进程直到给定时间(时钟)。
如果你还关注我到这里,我确实会做出一些改变。第一个是,我不使用实现方法execute() 的模板参数Command,而是使用仿函数以及可能使用要执行的命令作为参数的模板化构造函数。理由是,这将使其与其他库(如 boost::bind 或 boost::lambda)结合起来更具可扩展性,因为在这种情况下,命令可以在实例化时绑定到任何自由函数、函子或成员方法一个类的。
现在我得走了,但如果你有兴趣,我可以尝试发布修改后的版本。
其他提示
面向实现的菱形继承和面向子类型的继承之间存在设计质量差异,其中实现是继承的(有风险),而面向子类型的继承是继承接口或标记接口(通常有用)。
一般来说,如果您可以避免前者,那么您的情况会更好,因为在某个地方,确切的调用方法可能会导致问题,并且虚拟基、状态等的重要性开始变得重要。事实上,Java 不允许你提取类似的东西,它只支持接口层次结构。
我认为您可以提出的“最干净”的设计是有效地将钻石中的所有类转换为模拟接口(通过没有状态信息并具有纯虚拟方法)。这减少了歧义的影响。当然,您可以为此使用多重甚至菱形继承,就像在 Java 中使用实现一样。
然后,拥有这些接口的一组具体实现,可以以不同的方式实现(例如,聚合,甚至继承)。
封装此框架,以便外部客户端仅获取接口,而不会直接与具体类型交互,并确保彻底测试您的实现。
当然,这需要大量工作,但如果您正在编写一个集中且可重用的 API,这可能是您最好的选择。
我本周就遇到了这个问题,并在 DDJ 上找到了一篇文章,其中解释了这些问题以及何时应该或不应该担心它们。这里是:
接口继承层次结构中的“钻石”是相当安全的——代码的继承会让你陷入困境。
为了实现代码重用,我建议您考虑 mixins(如果您不熟悉 tequnique,请谷歌搜索 C++ Mixins)。使用 mixins 时,您感觉可以“去购物”实现类所需的代码片段,而无需使用有状态类的多重继承。
因此,该模式是 - 接口的多重继承和单个混入链(使您可以重用代码)来帮助实现具体的类。
希望有帮助!
以第一个例子为例......
它是否需要 ActionRead ActionWrite 是 action 的子类。
因为你最终会得到一个具体的类,无论如何它都是一个动作,所以你可以继承 actionread 和 actionwrite 而它们本身并不是动作。
不过,您可以发明需要它们作为操作的代码。但总的来说,我会尝试将 Action、Read、Write 和 Delay 分开,然后用具体的类将所有这些混合在一起
知道您在做什么,我可能会重新组织一些事情。我将进行多态性阅读,写作和写作课程,而不是所有这些版本的动作版本,而不是多次继承。
类似于以下内容(没有钻石继承):
在这里,我提出了实施可选延迟的众多方法之一,并假设所有读者的延迟方法都是相同的。每个子类可能都有自己的延迟实施,在这种情况下,您将传递给各自派生的延迟类的读取和实例。
class Action // abstract
{
// Reader and writer would be abstract classes (if not interfaces)
// from which you would derive to implement the specific
// read and write protocols.
class Reader // abstract
{
Class Delay {...};
Delay *optional_delay; // NULL when no delay
Reader (bool with_delay)
: optional_delay(with_delay ? new Delay() : NULL)
{};
....
};
class Writer {... }; // abstract
Reader *reader; // may be NULL if not a reader
Writer *writer; // may be NULL if not a writer
Action (Reader *_reader, Writer *_writer)
: reader(_reader)
, writer(_writer)
{};
void read()
{ if (reader) reader->read(); }
void write()
{ if (writer) writer->write(); }
};
Class Flow : public Action
{
// Here you would likely have enhanced version
// of read and write specific that implements Flow behaviour
// That would be comment to FlowA and FlowB
class Reader : public Action::Reader {...}
class Writer : public Action::Writer {...}
// for Reader and W
Flow (Reader *_reader, Writer *_writer)
: Action(_reader,_writer)
, writer(_writer)
{};
};
class FlowA :public Flow // concrete
{
class Reader : public Flow::Reader {...} // concrete
// The full implementation for reading A flows
// Apparently flow A has no write ability
FlowA(bool with_delay)
: Flow (new FlowA::Reader(with_delay),NULL) // NULL indicates is not a writer
{};
};
class FlowB : public Flow // concrete
{
class Reader : public Flow::Reader {...} // concrete
// The full implementation for reading B flows
// Apparently flow B has no write ability
FlowB(bool with_delay)
: Flow (new FlowB::Reader(with_delay),NULL) // NULL indicates is not a writer
{};
};
对于情况 2,不是 OneCommand
只是一个特例 CompositeCommand
?如果你消除 OneCommand
并允许 CompositeCommand
如果只有一个元素,我认为你的设计会变得更简单:
CommandAbstraction
/ \
/ \
/ \
ModernCommand CompositeCommand
\ /
\ /
\ /
ModernCompositeCommand
你仍然拥有可怕的钻石,但我认为这可能是一个可以接受的情况。