我知道钻石继承被认为是不好的做法。但是,我有2个案例,我认为钻石继承可以很好地适应。我想问一下,您是否会建议我在这些情况下使用钻石继承,或者是否有其他更好的设计。
案例1:我想在我的系统中创建代表不同类型“操作”的类。这些操作按以下几个参数进行分类:
我打算采用以下设计:
// 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 {/*...*/};
//...
当然,我会遵守没有2个动作(继承自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类继承的2个类不会实现相同的方法。
谢谢。
答案 0 :(得分:19)
继承是C ++中第二个最强(更多耦合)的关系,仅在友谊之前。如果您可以重新设计为仅使用合成,则代码将更松散地耦合。如果你不能,那么你应该考虑你的所有类是否应该真正从基类继承。是由于实现还是仅仅是一个界面?您是否希望将层次结构中的任何元素用作基本元素?或者只是层次结构中的叶子是真正的Action?如果只有叶子是动作并且您正在添加行为,则可以考虑针对此类行为组合的基于策略的设计。
这个想法是可以在小类集中定义不同的(正交)行为,然后捆绑在一起以提供真正的完整行为。在示例中,我将仅考虑一个策略,该策略定义操作是现在还是将来执行,以及要执行的命令。
我提供了一个抽象类,以便模板的不同实例可以在容器中存储(通过指针),或者作为参数传递给函数,并以多态方式调用。
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策略的一个特定示例,其中period设置为0.这是为了标记策略接口不需要相同。另一个等待策略实现可以通过提供一个注册为给定事件的回调的类来等待数毫秒,时钟周期或直到某些外部事件。
如果您不需要多态,您可以从示例中完全取出基类和虚拟方法。虽然这对于当前示例而言似乎过于复杂,但您可以决定将其他策略添加到组合中。
虽然添加新的正交行为意味着如果使用普通继承(使用多态),类的数量会呈指数增长,使用此方法,您可以单独实现每个不同的部分并将其粘贴在Action模板中。
例如,您可以定期执行操作并添加退出策略以确定何时退出定期循环。首先想到的选择是LoopPolicy_NRuns和LoopPolicy_TimeSpan,LoopPolicy_Until。对于每个循环,都会调用此策略方法(在我的示例中为exit())。第一个实现计算在固定数字之后它被称为退出的次数(由用户修复,因为在上面的示例中固定了时间段)。第二个实现将定期运行该过程一段时间,而最后一个实现将运行此过程直到给定时间(时钟)。
如果你还在跟我到这里,我的确会做一些改变。第一个是不使用模板参数Command实现方法execute(),而是使用仿函数,可能是一个模板化构造函数,它将命令作为参数执行。理由是,这将使其与其他库如boost :: bind或boost :: lambda更加可扩展,因为在这种情况下,命令可以在实例化时绑定到任何自由函数,仿函数或成员方法一个班级。
现在我必须去,但如果你有兴趣,我可以尝试发布修改版本。
答案 1 :(得分:9)
实现继承(风险)的面向实现的钻石继承与面向子类型的继承之间存在设计质量差异,其中接口或标记接口是继承的(通常是有用的)。
一般来说,如果你可以避免使用前者,那么你最好离开,因为确切的调用方法可能会导致问题,虚拟基础,状态等的重要性开始变得重要。事实上,Java不会允许你提取类似的东西,它只支持接口层次结构。
我认为你可以为此设计的“最干净”设计是将钻石中的所有类有效地转换为模拟界面(通过没有状态信息,并具有纯虚拟方法)。这减少了歧义的影响。当然,您可以使用多个甚至钻石继承,就像在Java中使用implements一样。
然后,有一组这些接口的具体实现,可以用不同的方式实现(例如,聚合,甚至继承)。
封装此框架,以便外部客户端只获取接口,并且永远不会直接与具体类型交互,并确保彻底测试您的实现。
当然,这是一项很多工作,但如果您正在编写一个可重用的中央API,这可能是您最好的选择。
答案 2 :(得分:5)
本周我遇到了这个问题,发现了一篇关于DDJ的文章解释了这些问题以及何时应该或不应该关注它们。这是:
答案 3 :(得分:4)
为了获得代码重用,我建议你考虑一下mixins(如果你不熟悉tequnique,可以使用google for C ++ Mixins)。使用mixins时,您觉得可以“购物”获取实现类所需的代码片段,而无需使用多重继承有状态类。
因此,模式是 - 接口的多重继承和一个mixin链(给你代码重用)来帮助实现具体的类。
希望有所帮助!
答案 4 :(得分:1)
用第一个例子.....
它的ActionRead ActionWrite是否需要成为动作的子类。
因为你最终会得到一个将成为动作的具体类,你可以继承actionread和actionwrite,而不需要它们本身就是动作。
但是,你可以发明需要它们作为动作的代码。但总的来说,我会尝试将Action,Read,Write和Delay分开,只是具体的类将所有这些混合在一起答案 5 :(得分:1)
不知道你在做什么, 我可能会重新整理一下。 而不是对所有这些版本的动作进行多重继承, 我会做多态阅读,写作和写作课程, 实例化为代表。
如下所示(没有钻石继承):
这里我介绍一种实现可选Delay的方法之一, 并假设延迟方法对所有读者都是一样的。 每个子类可能都有自己的延迟实现 在这种情况下,你将传递给Read和实例 相应的派生延迟等级。
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
{};
};
答案 6 :(得分:-1)
对于案例2,OneCommand
只是CompositeCommand
的特殊情况?如果您取消OneCommand
并允许CompositeCommand
只有一个元素,我认为您的设计变得更简单:
CommandAbstraction
/ \
/ \
/ \
ModernCommand CompositeCommand
\ /
\ /
\ /
ModernCompositeCommand
你仍然有可怕的钻石,但我认为这可能是一个可接受的案例。