20

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

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

当然,我会遵守没有 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
};

同样,我将确保没有 2 个从 CommandAbstraction 类继承的类将实现相同的方法。

谢谢你。

4

7 回答 7

20

继承是 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 策略只是时间段设置为 0 的 WaitSeconds 策略的一个特定示例。这是有意标记策略接口不需要相同。通过提供一个注册为给定事件回调的类,另一个等待策略实现可能是等待若干毫秒、时钟滴答或直到某个外部事件。

如果您不需要多态性,您可以从示例中完全取出基类和虚拟方法。虽然对于当前示例来说这可能看起来过于复杂,但您可以决定将其他策略添加到组合中。

如果使用普通继承(使用多态性),添加新的正交行为意味着类的数量会呈指数增长,但使用这种方法,您可以单独实现每个不同的部分并将其粘合到 Action 模板中。

例如,您可以使您的操作周期性并添加一个退出策略来确定何时退出周期性循环。首先想到的选项是 LoopPolicy_NRuns 和 LoopPolicy_TimeSpan、LoopPolicy_Until。这个策略方法(在我的例子中是 exit())为每个循环调用一次。第一个实现在一个固定的数字(由用户固定,如上例中固定的周期)之后计算它被称为退出的次数。第二种实现将在给定的时间段内定期运行该过程,而最后一个实现将运行该过程直到给定时间(时钟)。

如果你还跟着我到这里,我确实会做出一些改变。第一个是,不是使用实现方法 execute() 的模板参数命令,而是使用函子,可能还有一个模板化的构造函数,它将命令作为参数执行。基本原理是,这将使其与其他库(如 boost::bind 或 boost::lambda)结合起来更具可扩展性,因为在这种情况下,命令可以在实例化时绑定到任何自由函数、仿函数或成员方法一类的。

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

于 2008-12-18T23:36:11.310 回答
9

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

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

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

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

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

当然,这是很多工作,但如果您正在编写一个可重用的中央 API,这可能是您最好的选择。

于 2008-12-18T20:17:25.283 回答
4

本周我遇到了这个问题,发现一篇关于 DDJ 的文章解释了这些问题以及何时应该或不应该关注这些问题。这里是:

“多重继承被认为是有用的”

于 2008-12-18T20:18:39.603 回答
4

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

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

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

希望有帮助!

于 2008-12-18T23:49:46.930 回答
1

以第一个例子......

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

因为你最终会得到一个无论如何都会是一个动作的具体类,你可以只继承 actionread 和 actionwrite 而它们本身不是动作。

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

于 2008-12-18T20:29:50.127 回答
1

如果不知道你在做什么,我可能会重新组织一下。我将创建多态读写类,而不是所有这些版本的动作的多重继承,并将其实例化为委托。

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

在这里,我介绍了实现可选延迟的多种方法之一,并假设延迟方法对所有读者都是相同的。每个子类可能都有自己的延迟实现,在这种情况下,您将传递给 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
    {};
};
于 2008-12-18T22:35:41.383 回答
-1

对于案例 2,不OneCommand只是 的一个特例CompositeCommand吗?如果您消除OneCommand并允许CompositeCommands 只有一个元素,我认为您的设计会变得更简单:

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

你仍然有可怕的钻石,但我认为这可能是一个可以接受的情况。

于 2008-12-18T20:49:43.750 回答