【问题标题】:Quickly determine subclass based on abstract class基于抽象类快速确定子类
【发布时间】:2018-12-01 16:35:35
【问题描述】:

我有一个 C++ 基类 CAbstrInstruction 和大量的直接子类:

class CAbstrInstruction { /* ... */ };

class CSleepInstruction: public CAbstrInstruction { /* ... */ };
class CSetInstruction: public CAbstrInstruction { /* ... */ };
class CIfInstruction: public CAbstrInstruction { /* ... */ };
class CWhileInstruction: public CAbstrInstruction { /* ... */ };
// ...

还有一个 CScriptWorker 公开了一个公共方法 execute:

class CScriptWorker
{
    public:
        void execute (const CAbstrInstruction *pI);

    private:
        void doSleep (const CSleepInstruction *pI);
        void doSet (const CSetInstruction *pI);
        void doIf (const CIfInstruction *pI);
        void doWhile (const CWhileInstruction *pI);
        // ...
};

目前execute方法的实现是这样的:

void CScriptWorker::execute (const CAbstrInstruction *pI)
{
    const CSleepInstruction *pSleep =
        dynamic_cast<const CSleepInstruction *>(pI);

    if (pSleep != NULL)
    {
        doSleep (*pSleep);
        return;
    }

    const CSetInstruction *pSet =
        dynamic_cast<const CSetInstruction *>(pI);

    if (pSet != NULL)
    {
        doSet (*pSet);
        return;
    }

    const CIfInstruction *pIf =
        dynamic_cast<const CIfInstruction *>(pI);

    if (pIf != NULL)
    {
        doIf (*pIf);
        return;
    }

    const CWhileInstruction *pWhile =
        dynamic_cast<const CWhileInstruction *>(pI);

    if (pWhile != NULL)
    {
        doWhile (*pWhile);
        return;
    }

    /* ... */
}

这非常笨拙,需要 O(log(n)) 才能调用正确的私有方法。有没有 是否有任何设计模式或语言结构可以简化这一点?

澄清:我可以将私有执行方法 do... 移动到指令中 类。执行方法将简单地变成:

    void execute (const CAbstrInstruction *pI) { pI->execute(); }

但是,这不是我想要的。为什么不? 关注点分离:CAbstrInstruction 的实例只是对要做什么的描述。它们构成了脚本的抽象语法树。这已经足够令人担忧了。 CScriptWorker 关心的是实际执行指令所描述的内容。 CScriptWorker 知道脚本运行的上下文。CAbstrInstruction 不应该知道。

【问题讨论】:

  • 看看访问者设计模式
  • 你说得对,我认为这可能会解决问题。
  • 你的CAbstrInstruction 什么也没提供,你所做的相当于投射void*。作为接口应该提供哪些指令?
  • 不确定你的意思。当然,我已经删除了类中的所有细节。
  • 我的意思是,您没有使用任何有意义的继承/多态性功能,代码与转换 void* 相同,这绝不是一个好兆头。 CAbstrInstruction应该向用户保证什么?

标签: c++ inheritance design-patterns


【解决方案1】:

如果事情很简单,将execute 方法的实现移动到CAbstrInstruction 的子类中可能是答案。但是,OP 明确指出 execute 方法应单独保存在 CScriptWorker 中,以便将了解 what 将要完成的操作(指令的工作)与 分开如何 完成(CScriptWorker 的工作)。这可以通过 double dispatch 实现,有时也称为 visitor pattern

class IInstructionDispatchTarget
{
    public:
    virtual void onDispatch (const CSleepInstruction &instr) = 0;
    virtual void onDispatch (const CSetInstruction &instr) = 0;
};
class CAbstrInstruction
{
    public:
    virtual void dispatch (IInstructionDispatchTarget &t) const = 0;
};
class CSleepInstruction: public CAbstrInstruction
{
     public:
     virtual void dispatch (IInstructionDispatchTarget &t) const override
         { t.onDispatch (*this); }
};
class CSetInstruction: public CAbstrInstruction
{
     public:
     virtual void dispatch (IInstructionDispatchTarget &t) const override
         { t.onDispatch (*this); }
};
class CScriptWorker: public IInstructionDispatchTarget
{
    public:
    void execute (const CAbstrInstruction *pI)
        { pI->dispatch (*this); }
    virtual void onDispatch (const CSleepInstruction &instr) override
    {
        // do sleep
    }
    virtual void onDispatch (const CSetInstruction &instr) override
    {
        // do set
    }
};

executeCScriptWorker 调用时,它会调用指令的dispatch 方法。作为回报,该指令调用调度目标上的onDispatch 方法,使用其特定的this 指针,从而调用正确的方法。

接口IInstructionDispatchTarget 有两个用途。一方面,它确保CAbstrInstruction 的实例根本不需要知道CScriptWorker;他们只需要知道界面。另一方面,它允许其他调度目标使用相同的机制,例如遍历指令以优化 AST 时。

如果认为IInstructionDispatchTarget 的存在是不必要的,则可以稍微简化一下,如 ROX 的回答所示。

【讨论】:

  • 如果您有其他将派生自 IInstructionVisitor 的类,则此处引入 IInstructionVisitor 接口非常有用。如果没有,那么可以使用稍微简单的双重调度版本来实现。
  • 这个更简单的版本会是什么样子?
【解决方案2】:

CAbstrInstruction 应该定义一个纯虚方法(在您的示例中为execute()),您的子类应该覆盖并实现该方法。

举个例子:

class CAbstrInstruction 
{
     /* ... */
     virtual void execute() const = 0;
}

class CSleepInstruction 
{
     /* ... */
     void execute() override const
     {
         /* your code here */
     }
}

/* ... */
void CScriptWorker::execute (const CAbstrInstruction *pI)
{
    pI->execute();
}

【讨论】:

  • 如果你读过以“澄清”开头的段落,你就会知道这不是我想要的。执行的实现不应该在指令中。
  • 指令中没有。为简化起见,您在基类中创建了一个“占位符”,然后将其“填充”到子类中。
  • 抽象类的目的是提供一个通用接口和/或跨子类的通用行为。我无法查看您的问题所在。
  • 您是否 100% 需要“工人”课程? CSetInstruction 不是仅仅通过名称就可以揭示行为吗?为什么需要将其行为委托给工作人员?
  • 抽象函数的继承不违反关注点。它定义了一个接口,如果有任何东西可以增强关注点分离。搜索 Liskov 替换原则。
【解决方案3】:

当客户端不需要知道对象的具体类型时,最好使用继承。您想使用variant type,因为您有固定数量的已知指令,并且您的执行程序需要知道它正在执行哪种指令。如果您是 C++17 之前的版本,则使用 std::variantboost::variant 最简单。

#include <variant>

struct Set {};
struct If {};
struct While {};

using Instruction = std::variant<
    Set,
    If,
    While
    >;

#include <iostream>

struct Executor {
    void operator()(Set const&) const { std::cout << "Set\n"; }
    void operator()(If const&) const { std::cout << "If\n"; }
    void operator()(While const&) const { std::cout << "While\n"; }
};

void execute(Instruction const& i) {
    std::visit(Executor(), i);
}

例子:

#include <vector>

int main() {
    for (auto const& i : std::vector<Instruction>{While(), If(), Set()}) {
        execute(i);
    }
}

输出:

While
If
Set

【讨论】:

  • 有趣,我不知道 std::variant。我去看看。
  • 嗯,显然,变体不允许保存引用或数组。我的指示都有。
  • 变体不能直接保存引用或数组。它们可以保存包含引用或数组的对象。如果您需要 Instruction 可移动,或者如果您想将它们存储在堆上以减少内存使用(std::variant 的大小由其最大的替代项决定),您可以将指令包装在 std::unique_ptr 中。跨度>
  • 我认为它可以工作,虽然我更喜欢访问者模式。
  • 我更喜欢std::variant,因为它的样板更少且耦合更松散。 IMO,访问者模式是一种在没有适当变体类型的情况下使用的解决方法。
【解决方案4】:

如果您有其他将实现 IInstructionVisitor 接口的类,则访问者模式可以很好地工作。这确保所有这些类都可以处理相同的指令类集。

如果您没有从 IInstructorVisitor 派生的其他类,那么您可以稍微简化一下:-

class CScriptWorker
{
    public:
        void execute (const CAbstrInstruction* pI)
        {
           pI->ResolveInstructionType(*this);
        }

    // Can be made friends of appropriate instruction classes or left public as you see fit
       void doInstruction (const CSleepInstruction* pI);
       void doInstruction (const CSetInstruction* pI);
       void doInstruction (const CIfInstruction* pI);
       void doInstruction (const CWhileInstruction* pI);
    // note the name is now the same, name of the parameter should be enough to tell what's being done
    // also I'd probably make these references not pointers

};


class CAbstrInstruction
{
    public:
    virtual void ResolveInstructionType (CScriptWorker& v) = 0;
};


class CSleepInstruction: public CAbstrInstruction
{
     public:
     void ResolveInstructionType (CScriptWorker& w) override { w.doInstruction (this); }
};

简化的一点好处是,现在代码稍微少了一点,如果添加新指令需要修改的代码稍微少一点,您可以选择除访问、访问者等以外的名称。

【讨论】:

  • doInstruction 是私有的,CSleepInstruction 不能调用它。
  • 另外,CSleepInstruction 需要了解 CScriptWorker,这还可以,但并不理想。
  • 我想这是使用访问者界面的另一个好处,(尽管说明仍然需要知道这一点)。 AbstractInstruction 只需要工作类的前向声明,而不是完整的定义,但派生类需要了解 doInstruction 方法。访问者模式很好(尽管在我看来访问者命名不是很好)我只是提出这个另一种选择,我并不是说它更好,两者都有优点和缺点。
  • 你说得对,命名不是很好。我为我的答案添加了一个替代命名。
  • 我认为访问者模式中的命名通常不是那么好,不仅仅是你的版本。
猜你喜欢
  • 1970-01-01
  • 2013-01-02
  • 1970-01-01
  • 2017-01-08
  • 1970-01-01
  • 2020-03-26
  • 1970-01-01
  • 2018-07-11
  • 1970-01-01
相关资源
最近更新 更多