钻石继承 (C++)

发布于 2024-07-10 04:17:17 字数 2641 浏览 5 评论 0原文

我知道拥有钻石继承被认为是不好的做法。 然而,我有两个案例,我觉得钻石继承非常适合。 我想问,在这些情况下你会建议我使用钻石继承,还是有其他设计可以更好。

情况 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 类继承的两个类不会实现相同的方法。

谢谢。

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. 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. 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).
  • The action's "flow type" can be FlowA or 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.

Case 2: I implement the composite design pattern for a "Command" in my system. 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. 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.

Thank you.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

荒人说梦 2024-07-17 04:17:18

第一个例子......

它是否需要 ActionRead ActionWrite 是 action 的子类。

因为您最终将得到一个具体的类,该类无论如何都是一个操作,因此您可以继承 actionread 和 actionwrite 而无需它们本身成为操作。

不过,您可以发明需要它们作为操作的代码。 但总的来说,我会尝试将 Action、Read、Write 和 Delay 分开,然后用具体的类将所有这些混合在一起

With the first example.....

its whether ActionRead ActionWrite need to be subclasses of action at all.

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.

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

悲念泪 2024-07-17 04:17:18

在不了解自己在做什么的情况下,
我可能会稍微重新组织一下。
而不是所有这些版本的操作的多重继承,
我会开设多态阅读、写作和写作课程,
实例化为代表。

类似于以下内容(没有钻石继承):

这里我提出了实现可选延迟的多种方法之一,
并假设所有读者的延迟方法都是相同的。
每个子类可能有自己的延迟实现
在这种情况下,您将传递给 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
    {};
};

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.
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.

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
    {};
};
老街孤人 2024-07-17 04:17:18

对于情况 2,OneCommand 不就是 CompositeCommand 的特殊情况吗? 如果您消除 OneCommand 并允许 CompositeCommand 仅具有一个元素,我认为您的设计会变得更简单:

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

您仍然拥有可怕的钻石,但我认为这可能是可以接受的案例。

For case 2, isn't a OneCommand just a special case of CompositeCommand? If you eliminate OneCommand and allow CompositeCommands to only have one element, I think your design gets simpler:

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

You still have the dreaded diamond, but I think this may be an acceptable case for it.

送舟行 2024-07-17 04:17:17

继承是 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)结合起来更具可扩展性,因为在这种情况下,命令可以在实例化时绑定到任何自由函数、函子或成员方法一个类的。

现在我得走了,但如果你有兴趣,我可以尝试发布修改后的版本。

Inheritance is the second strongest (more coupling) relations in C++, preceded only by friendship. 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? 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. Now waiting for a time period needs a fixed period time that is set through the period() public method.

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. 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.

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. This policy method ( exit() in my case ) is called once for each loop. 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. 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.

Now I have to go, but if you are interested I can try posting a modified version.

渡你暖光 2024-07-17 04:17:17

面向实现的菱形继承和面向子类型的继承之间存在设计质量差异,其中实现是继承的(有风险),而面向子类型的继承是继承接口或标记接口(通常有用)。

一般来说,如果您可以避免前者,那么您的情况会更好,因为在某个地方,确切的调用方法可能会导致问题,并且虚拟基、状态等的重要性开始变得重要。 事实上,Java 不允许你提取类似的东西,它只支持接口层次结构。

我认为您可以提出的“最干净”的设计是有效地将钻石中的所有类转变为模拟接口(通过没有状态信息并具有纯虚拟方法)。 这减少了歧义的影响。 当然,您可以为此使用多重甚至菱形继承,就像在 Java 中使用实现一样。

然后,拥有这些接口的一组具体实现,可以以不同的方式实现(例如,聚合,甚至继承)。

封装此框架,以便外部客户端仅获取接口,而不会直接与具体类型交互,并确保彻底测试您的实现。

当然,这需要大量工作,但如果您正在编写一个集中且可重用的 API,这可能是您最好的选择。

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.

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.

Then, have a set of concrete implementations of these interfaces that can be implemented in different ways (E.g., 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.

鹿港巷口少年归 2024-07-17 04:17:17

我本周就遇到了这个问题,并在 DDJ 上找到了一篇文章,其中解释了这些问题以及何时应该或不应该担心它们。 这是:

“多重继承被认为有用”

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. Here it is:

"Multiple Inheritance Considered Useful"

吐个泡泡 2024-07-17 04:17:17

接口继承层次结构中的“钻石”是相当安全的——代码的继承会让你陷入困境。

为了获得代码重用,我建议您考虑 mixins(如果您不熟悉 tequnique,请谷歌搜索 C++ Mixins)。 使用 mixins 时,您感觉可以“去购物”实现类所需的代码片段,而无需使用有状态类的多重继承。

因此,该模式是 - 接口的多重继承和单个混入链(使您可以重用代码)来帮助实现具体的类。

希望有帮助!

"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). 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.

So, the pattern is - multiple inheritance of interfaces and a single chain of mixins (giving you code reuse) to help implement the concrete class.

Hope that helps!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文