【问题标题】:Null Object Pattern, Recursive Class, and Forward Declarations空对象模式、递归类和前向声明
【发布时间】:2009-11-30 20:16:16
【问题描述】:

我有兴趣做以下类似的事情来遵守 Null 对象设计模式并避免大量的 NULL 测试:

class Node;
Node* NullNode;

class Node {
public:
  Node(Node *l=NullNode, Node *r=NullNode) : left(l), right(r) {};
private:
  Node *left, *right;
};

NullNode = new Node();

当然,正如所写,NullNode 在 Node 类声明之前和之后具有不同的内存位置。如果您不想使用默认参数(即删除 Node *r=NullNode),则可以在没有前向声明的情况下执行此操作。

另一种选择是使用一些继承:创建一个带有两个子类(NullNode 和 FullNode)的父类(Node)。那么上面的节点示例将是 FullNode 的代码,并且上面代码中的 NullNode 将是从 Node 继承的 NullNode 类型。我讨厌通过继承来解决简单的问题。

所以,问题是:如何在 C++ 中将 Null 对象模式应用于具有默认参数(即同一类的实例!)的递归数据结构(类)?

【问题讨论】:

  • 您仍然需要将所有节点与NullNode 进行比较,以查看它们是否有效。这能为您节省什么?
  • Donnie,一般的想法是,而不是:如果(NULL 测试)do nothing else do something。你会这样做: thisObject.doStuff() 如果 thisObject 是 NullObject,那么它(瞧!)什么也不做(也许它有许多空函数定义 {})。如果 thisObject 是一个“真正的节点”,那么它会做一些有用的事情(比如打印自己)。比较 NullNode::Print() {} 与 FullNode::Print{ cout
  • 如果没有继承(并使一切都成为多态),难道每个Node方法都必须检查当前实例是否是NullNode,所以它们不会做任何事情吗?另一个问题:return 的方法呢?在这些情况下 NullNode 会返回什么?
  • 查看 wikipedia 文章,在我看来,该模式的重点并不是消除任何 NULL 对象和/或异常。如果一个函数可以返回一个空字符串来表示“没有结果”,那么返回 NULL 可能没有多大意义。这并不意味着您不需要区分无结果和错误的情况。特别是,这种模式似乎不适合 ADT。考虑链表:所以它会以一个空对象终止?那么你会如何认清结局呢?对,你仍然需要测试指针。
  • 关键是,任何用于表示“无结果”的“填充类型”不是我们正在使用的实际类型,都是“坏主意”。我把坏主意放在引号里,b/c 这就是索赔。这真的是一个权衡。如果您期望一个节点并且您得到 NULL、"" 或 0,您仍然需要检查它。如果你返回这个“东西”,那么你必须返回一个“无结果”和“有用的东西”的联合。使用继承和多态(顺便说一句,没有继承:您是否建议函数重载就足够了?),您删除了检查(if/then 或 try/except)。参见 Martin,敏捷软件开发,第 189 页。

标签: c++ recursion object null default


【解决方案1】:

使用extern:

extern Node* NullNode;
...
Node* NullNode = new Node();

更好的是,让它成为一个静态成员:

class Node {
public:
  static Node* Null;
  Node(Node *l=Null, Node *r=Null) : left(l), right(r) {};
private:
  Node *left, *right;
};

Node* Node::Null = new Node();

也就是说,在现有代码和上述修改中,您都会泄漏Node 的实例。你可以使用auto_ptr,但这会很危险,因为全局和静态的破坏顺序不确定(一些全局的析构函数可能需要Node::Null,到那时它可能已经消失了,也可能还没有)。

【讨论】:

  • 对。当然,我知道我忘记了其中一个“名称操纵者”。我忘记了 extern b/c 我通常在不同的上下文中使用它(即,当前文件外部的声明!)。我认为我倾向于基于继承的解决方案,以便 NullNode 可以“什么都不做”(而不是到处测试)。这是给学生的示例代码,所以我正在平衡代码的可读性(太多的测试把事情弄得一团糟)和类结构的可读性(继承可能会让人目瞪口呆)。谢谢!
  • 自更正:不管你是否使用auto_ptr,初始化的顺序都是一个问题。如果您绝对需要正确处理,则必须使用“phoenix singleton”模式。
【解决方案2】:

我实际上已经实现了一个递归树(用于 JSON 等)做这样的事情。基本上,您的基类成为“NULL”实现,其接口是派生的所有接口的联合。然后,您就有了实现片段的派生类——“DataNode”实现了数据获取器和设置器等。

这样,您可以对基类接口进行编程并为自己省去很多痛苦。您设置基本实现来为您执行所有样板逻辑,例如

class Node {
    public:
    Node() {}
    virtual ~Node() {}

    virtual string OutputAsINI() const { return ""; }
};

class DataNode {
    private:
    string myName;
    string myData;

    public:
    DataNode(const string& name, const string& val);
    ~DataNode() {}

    string OutputAsINI() const { string out = myName + " = " + myData; return out; }
};

这样我就不需要测试任何东西——我只是盲目地调用“OutputAsINI()”。整个界面的类似逻辑将使大多数空测试消失。

【讨论】:

    【解决方案3】:

    反转层次结构。将空节点放在基层:

    class Node {
    public:
      Node() {}
      virtual void visit() const {}
    };
    

    然后根据需要进行专业化:

    template<typename T>
    class DataNode : public Node {
    public:
      DataNode(T x, const Node* l=&Null, const Node* r=&Null)
        : left(l), right(r), data(x) {}
    
      virtual void visit() const {
        left->visit();
        std::cout << data << std::endl;
        right->visit();
      }
    
    private:
      const Node *left, *right;
      T data;
      static const Node Null;
    };
    
    template<typename T>
    const Node DataNode<T>::Null = Node();
    

    示例用法:

    int main()
    {
      DataNode<char> a('A', new DataNode<char>('B'),
                            new DataNode<char>('C'));
    
      a.visit();
    
      return 0;
    }
    

    输出:

    $ ./node 
    B
    A
    C
    

    【讨论】:

    • 我昨天开车回家也得出了类似的结论。对 Node->DataNode 层次结构与 Node->NullNode 和 Node->DataNode 版本的成本/收益有何想法?我们可以进行哪些修改以使其中一个比另一个更灵活?
    • NullNode为基础,假设DataNode是一个模板,你不必像class NullNode : public DataNode&lt;int&gt; ...那样使用丑陋的dummy template-parameter,而且这种排列方式也更好适合“is-a”关系。
    猜你喜欢
    • 1970-01-01
    • 2017-06-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-17
    • 1970-01-01
    • 1970-01-01
    • 2023-03-06
    相关资源
    最近更新 更多