简体   繁体   English

钻石继承(C ++)

[英]Diamond inheritance (C++)

I know that having diamond inheritance is considered bad practice. 我知道钻石继承被认为是不好的做法。 However, I have 2 cases in which I feel that diamond inheritance could fit very nicely. 但是,我有2个案例,我认为钻石继承可以很好地适应。 I want to ask, would you recommend me to use diamond inheritance in these cases, or is there another design that could be better. 我想问一下,您是否会建议我在这些情况下使用钻石继承,或者是否有其他设计可能更好。

Case 1: I want to create classes that represent different kinds of "Actions" in my system. 案例1:我想在我的系统中创建代表不同类型“动作”的类。 The actions are classified by several parameters: 这些操作按以下几个参数进行分类:

  • The action can be "Read" or "Write". 动作可以是“读取”或“写入”。
  • The action can be with delay or without delay (It is not just 1 parameter. It changes the behavior significantly). 动作可以延迟或没有延迟(它不仅仅是1个参数。它会显着改变行为)。
  • The action's "flow type" can be FlowA or FlowB. 动作的“流类型”可以是FlowA或FlowB。

I intend to have the following design: 我打算有以下设计:

// 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  {/*...*/};
//...

Of course, I will obey that no 2 actions (inheriting from Action class) will implement the same method. 当然,我会遵守没有2个动作(继承自Action类)将实现相同的方法。

Case 2: I implement the composite design pattern for a "Command" in my system. 案例2:我在系统中为“命令”实现了复合设计模式。 A command can be read, written, deleted, etc. I also want to have a sequence of commands, which can also be read, written, deleted, etc. A sequence of commands can contain other sequences of commands. 可以读取,写入,删除等命令。我还希望有一系列命令,这些命令也可以被读取,写入,删除等。一系列命令可以包含其他命令序列。

So I have the following design: 所以我有以下设计:

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

In addition, I have a special kind of commands, "Modern" commands. 另外,我有一种特殊的命令,“现代”命令。 Both one command and composite command can be modern. 一个命令和复合命令都可以是现代的。 Being "Modern" adds a certain list of properties to one command and composite command (mostly same properties for both of them). “现代”为一个命令和复合命令添加了一定的属性列表(两者的属性大多相同)。 I want to be able to hold a pointer to CommandAbstraction, and initialize it (via new) according to the needed type of command. 我希望能够拥有一个指向CommandAbstraction的指针,并根据所需的命令类型对其进行初始化(通过new)。 So I want to do the following design (in addition to the above) : 所以我想做以下设计(除了上面的):

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

Again, I will make sure that no 2 classes inheriting from CommandAbstraction class will implement the same method. 同样,我将确保从CommandAbstraction类继承的2个类不会实现相同的方法。

Thank you. 谢谢。

Inheritance is the second strongest (more coupling) relations in C++, preceded only by friendship. 继承是C ++中第二个最强大(更多耦合)的关系,仅在友谊之前。 If you can redesign into using only composition your code will be more loosely coupled. 如果您可以重新设计为仅使用合成,则代码将更松散地耦合。 If you cannot, then you should consider whether all your classes should really inherit from the base. 如果你不能,那么你应该考虑你的所有类是否应该真正从基类继承。 Is it due to implementation or just an interface? 是由于实现还是仅仅是一个界面? Will you want to use any element of the hierarchy as a base element? 您是否希望将层次结构中的任何元素用作基本元素? Or are just leaves in your hierarchy that are real Action's? 或者只是层次结构中的叶子是真正的Action? If only leaves are actions and you are adding behavior you can consider Policy based design for this type of composition of behaviors. 如果只有叶子是动作并且您正在添加行为,则可以考虑针对此类行为组合的基于策略的设计。

The idea is that different (orthogonal) behaviors can be defined in small class sets and then bundled together to provide the real complete behavior. 这个想法是可以在小类集中定义不同的(正交)行为,然后捆绑在一起以提供真实的完整行为。 In the example I will consider just one policy that defines whether the action is to be executed now or in the future, and the command to execute. 在示例中,我将仅考虑一个策略,该策略定义操作是现在还是将来执行,以及要执行的命令。

I provide an abstract class so that different instantiations of the template can be stored (through pointers) in a container or passed to functions as arguments and get called polymorphically. 我提供了一个抽象类,以便模板的不同实例可以在容器中存储(通过指针),或者作为参数传递给函数,并以多态方式调用。

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

The use of inheritance allows public methods from the policy implementations to be accessible through the template instantiation. 继承的使用允许通过模板实例化访问策略实现中的公共方法。 This disallows the use of aggregation for combining the policies as no new function members could be pushed into the class interface. 这不允许使用聚合来组合策略,因为没有新的函数成员可以被推入类接口。 In the example, the template depends on the policy having a wait() method, which is common to all waiting policies. 在该示例中,模板依赖于具有wait()方法的策略,该方法对于所有等待策略是通用的。 Now waiting for a time period needs a fixed period time that is set through the period() public method. 现在等待一段时间需要一段通过period()公共方法设置的固定时间段。

In the example, the NoWait policy is just a particular example of the WaitSeconds policy with the period set to 0. This was intentional to mark that the policy interface does not need to be the same. 在该示例中,NoWait策略只是WaitSeconds策略的一个特定示例,其周期设置为0.这是为了标记策略接口不需要相同。 Another waiting policy implementation could be waiting on a number of milliseconds, clock ticks, or until some external event, by providing a class that registers as a callback for the given event. 另一个等待策略实现可以通过提供一个注册为给定事件的回调的类来等待数毫秒,时钟周期或直到某些外部事件。

If you don't need polymorphism you can take out from the example the base class and the virtual methods altogether. 如果您不需要多态性,您可以从示例中完全取出基类和虚拟方法。 While this may seem overly complex for the current example, you can decide on adding other policies to the mix. 虽然这对于当前示例而言似乎过于复杂,但您可以决定将其他策略添加到组合中。

While adding new orthogonal behaviors would imply an exponential growth in the number of classes if plain inheritance is used (with polymorphism), with this approach you can just implement each different part separately and glue it together in the Action template. 如果使用普通继承(使用多态),添加新的正交行为将意味着类的数量呈指数增长,使用此方法,您可以单独实现每个不同的部分并将其粘贴在Action模板中。

For example, you could make your action periodic and add an exit policy that determine when to exit the periodic loop. 例如,您可以定期执行操作并添加退出策略以确定何时退出定期循环。 First options that come to mind are LoopPolicy_NRuns and LoopPolicy_TimeSpan, LoopPolicy_Until. 首先想到的选择是LoopPolicy_NRuns和LoopPolicy_TimeSpan,LoopPolicy_Until。 This policy method ( exit() in my case ) is called once for each loop. 对于每个循环,都会调用此策略方法(在我的示例中为exit())。 The first implementation counts the number of times it has been called an exits after a fixed number (fixed by the user, as period was fixed in the example above). 第一个实现计算在固定数字之后它被称为退出的次数(由用户修复,因为在上面的示例中固定了时间段)。 The second implementation would periodically run the process for a given time period, while the last one will run this process until a given time (clock). 第二个实现将定期运行该过程一段时间,而最后一个将执行此过程直到给定时间(时钟)。

If you are still following me up to here, I would indeed make some changes. 如果你还在跟我到这里,我的确会做一些改变。 The first one is that instead of using a template parameter Command that implements a method execute() I would use functors and probably a templated constructor that takes the command to execute as parameter. 第一个是不使用模板参数Command实现方法execute(),而是使用仿函数,可能是一个模板化构造函数,它将命令作为参数执行。 The rationale is that this will make it much more extensible in combination with other libraries as boost::bind or boost::lambda, since in that case commands could be bound at the point of instantiation to any free function, functor, or member method of a class. 理由是,这将使其与其他库如boost :: bind或boost :: lambda更加可扩展,因为在这种情况下,命令可以在实例化时绑定到任何自由函数,仿函数或成员方法一堂课。

Now I have to go, but if you are interested I can try posting a modified version. 现在我必须去,但如果你有兴趣,我可以尝试发布修改版本。

There's a design-quality difference between implementation-oriented diamond inheritance where implementation is inherited (risky), and subtyping-oriented inheritance where interfaces or marker-interfaces are inherited (often useful). 面向实现的钻石继承(实现是继承的)(风险)和面向子类型的继承(接口或标记接口是继承的)(通常很有用)之间存在设计质量差异。

Generally, if you can avoid the former, you're better off since somewhere down the line the exact invoked method may cause problems, and the importance of virtual bases, states, etc., starts mattering. 一般来说,如果你可以避免使用前者,那么你最好从某个地方开始,确切的调用方法可能会导致问题,虚拟基础,状态等的重要性开始变得重要。 In fact, Java wouldn't allow you to pull something like that, it supports only the interface hierarchy. 事实上,Java不会允许你拉这样的东西,它只支持接口层次结构。

I think that the "cleanest" design you can come up for this is to effectively turn all your classes in the diamond into mock-interfaces (by having no state information, and having pure virtual methods). 我认为你可以为此设计的“最干净”的设计是将钻石中的所有类有效地转换为模拟接口(通过没有状态信息,并具有纯虚方法)。 This reduces the impact of ambiguity. 这减少了歧义的影响。 And of course, you can use multiple and even diamond inheritance for this just like you would use implements in Java. 当然,您可以使用多个甚至钻石继承,就像在Java中使用implements一样。

Then, have a set of concrete implementations of these interfaces that can be implemented in different ways (Eg, aggregation, even inheritance). 然后,有一组这些接口的具体实现,可以以不同的方式实现(例如,聚合,甚至继承)。

Encapsulate this framework so that external clients only get the interfaces and never interact directly with the concrete types, and make sure to thoroughly test your implementations. 封装此框架,以便外部客户端只获取接口,并且永远不会直接与具体类型交互,并确保彻底测试您的实现。

Of course, this is a lot of work, but if you're writing a central and reusable API, this might be your best bet. 当然,这是很多工作,但如果您正在编写一个可重用的中央API,这可能是您最好的选择。

I ran into this problem just this week and found an article on DDJ that explained the problems and when you should or shouldn't be concerned about them. 本周我遇到了这个问题,发现了一篇关于DDJ的文章解释了这些问题以及何时应该或不应该关注它们。 Here it is: 这里是:

"Multiple Inheritance Considered Useful" “多重继承被认为是有用的”

"Diamonds" in the inheritance hierarchy of interfaces is quite safe - it's inheritance of code that get's you into hot water. 接口继承层次结构中的“钻石”非常安全 - 它是代码的继承,让你陷入热水。

To get code reuse, I advise you to consider mixins (google for C++ Mixins if you are unfamiliar with the tequnique). 为了获得代码重用,我建议你考虑一下mixins(如果你不熟悉tequnique,可以使用google for C ++ Mixins)。 When using mixins you feel like you can "go shopping" for the code snippets that you need to implement you class without using multiple inheritance of stateful classes. 使用mixins时,您觉得可以“购物”获取实现类所需的代码片段,而无需使用多重继承有状态类。

So, the pattern is - multiple inheritance of interfaces and a single chain of mixins (giving you code reuse) to help implement the concrete class. 因此,模式是 - 接口的多重继承和一个mixin链(给你代码重用)来帮助实现具体的类。

Hope that helps! 希望有所帮助!

With the first example..... 用第一个例子.....

its whether ActionRead ActionWrite need to be subclasses of action at all. 它的ActionRead ActionWrite是否需要成为动作的子类。

since you are going to end up with one concrete class that will be an action anyways, you could just inherit actionread and actionwrite without them being actions in themselves. 因为你最终将得到一个将成为动作的具体类,你可以继承actionread和actionwrite而不用它们本身就是动作。

though, you could invent code that would require them to be actions. 但是,你可以发明需要它们作为动作的代码。 But in general I'd try and separate Action, Read, Write, and Delay and just the concrete class mixes all that together 但总的来说,我会尝试将Action,Read,Write和Delay分开,只是具体的类将所有这些混合在一起

With out knowing more of what you are doing, I would probably reorganize things a bit. 如果不知道你在做什么,我可能会重新组织一些事情。 Instead of multiple inheritence with all these versions of action, I would make polymorphic reading and writing and writing classes, instanciated as delegates. 我不会对所有这些版本的动作进行多重继承,而是进行多态读取,编写和编写类,实现为委托。

Something like the following (which has no diamond inheritence): 类似下面的内容(没有钻石继承):

Here I present one of many ways implementing optional Delay, and assume the delay methodology is the same for all readers. 在这里,我介绍了实现可选Delay的许多方法之一,并假设所有读者的延迟方法都是相同的。 each subclass might have their own implementation of delay in which case you would pass down to Read and instance of the respective derived Delay class. 每个子类可能有自己的延迟实现,在这种情况下,您将传递给Read和相应的派生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
    {};
};

For case 2, isn't a OneCommand just a special case of CompositeCommand ? 对于案例2,是不是OneCommand只是CompositeCommand一个特例? If you eliminate OneCommand and allow CompositeCommand s to only have one element, I think your design gets simpler: 如果你消除OneCommand并允许CompositeCommand只有一个元素,我认为你的设计变得更简单:

              CommandAbstraction
                 /          \
                /            \
               /              \
        ModernCommand      CompositeCommand
               \               /
                \             /
                 \           /
             ModernCompositeCommand

You still have the dreaded diamond, but I think this may be an acceptable case for it. 你仍然有可怕的钻石,但我认为这可能是一个可接受的案例。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM