前端/后端设计:如何将后端与前端完全分离?

时间:2011-04-12 14:44:39

标签: c++ oop templates design-patterns

我的问题是:(上面是什么)是创建非侵入式前端的正确方法?

我用一个简化的例子来解释我的问题。

我有一个实现二叉树的后端:

// Back-end
struct Node
{
  Label label;
  Node* r, l;
};

我现在想要实现前端以图形方式打印树。所以我的想法是通过包装它来扩展后端的图形属性:

// Front-end
struct Drawable
{
  uint x, y;
};

class Visitor;
template <class T> struct GNode : public Drawable
{
  T* wrapped;
  template <class V> void accept(V& v); // v.visit(*this);
}

现在有一个问题是创建一个打印二叉树的访问者:

struct Visitor
{
    void visit(GNode<Node>& n)
    {
      // print the label and a circle around it: ok.

      if (n.wrapped.l) // l is a Node, not a GNode, I can't use the visitor on it
        // Problem: how to call this visitor on the node's left child?

      // the same with n.wrapped.r
    };
};

如评论中所述,后端不使用我的扩展类。

编写GNode“is-a”Node既不是解决方案,因为我必须将Node类中的accept()方法作为虚拟对象并在GNode中覆盖它,但我无法修改后端。然后,有人也可以说没有必要在后端声明accept(),向下转发Node *到GNode *会起作用。是的它有效,但它是低垂的...

在我的情况下,我有~10种节点(它是一个图形),所以我正在寻找一些优雅,灵活的东西,尽可能少的代码行(因此包装模板的想法):)

非常感谢。

4 个答案:

答案 0 :(得分:2)

绝对不能解释代码问题。他们不得不说话。如果你真的想要强制解耦到最大程度,应该使用某种IPC / RPC机制并有两个不同的程序。

那就是说 - 我不喜欢访客模式。

您有一个Graphical对象,它与Behaving对象链接。也许在行为和图形之间存在规则,例如,边界不能重叠。

你可以在Graphicals和Behaves之间进行实体关系,这是一个商业逻辑问题......

你需要一些能够保存你的绘图背景的文章(img,screen,buffer)。

class DrawingThungus { 
  void queue_for_render(Graphical*);
  void render();
};

您的图形将具有行为的继承或组合关系。 无论如何,他们将拥有绘图所需的界面。

//abstract base class class Graphical  {   
  get_x();  
  get_y();  
  get_icon(); 
  get_whatever(); 
};

如果你发现Render正在变成基于案例的,取决于Graphical的类型,我建议将案例推送到Graphical,并重构为get_primitives_list(),其中返回所需的原语返回图形(我假设在某种程度上,你有核心基元,线,圆,弧,标签等)。

我一直发现,OO分析有助于浪费精神能量,应该只为手头的任务做足够的事情。 YAGNI是一个伟大的原则。

答案 1 :(得分:1)

如果您的包装类(GNode)不必跨访问维护任何状态(即,它只有一个字段 - 包装的Node对象),您可以使用指针或对包装对象的引用而不是复制,然后您就可以在运行时包装任何节点。

但即使你确实维护状态(x,y坐标),你真的只是从包装对象中推断它吗?在这种情况下,将您访问的类与推断数据分开不是更好吗?例如,考虑这个实现:

// This is an adapter pattern, so you might want to call it VisitorAdapter if you
// like naming classes after patterns.
template typename<T>
class VisitorAcceptor
{
private:
    T& wrapped;
public:
    VisitorAcceptor(T& obj)
    {
        wrapped = obj;
    }

    template <typename VisitorT>
    void accept(VisitorT& v)
    {
        v.visit(wrapped);
    }
};

struct GNode
{
    uint x, y;
    shared_ptr<GNode> l,r; // use your favourite smart pointer here

    template <typename VisitorT>
    void accept(VisitorT& v)
}

// You don't have to call a visitor implementation 'Visitor'. It's better to name
// it according to its function, which is, I guess, calculating X,Y coordinates.
{
    shared_ptr<GNode> visit(Node& n)
    {
        shared_ptr<GNode> gnode = new GNode;
        // calculate x,y
        gnode->x = ...
        gnode->y = ...

        if (n.l)
            gnode->l = VisitorAdapter(n.r).accept(*this);
        if (n.r)
            gnode->r = VisitorAdapter(n.l).accept(*this);
    };
};

Now you can have a different visitor for drawing:

struct GNodeDrawer
{
    void visit(GNode& gnode)
    {
        // print the label and a circle around it: ok.

        if (n.r)
            visit(n.l);
        if (n.r)
            visit(n.r);
    };
};

当然,如果你不需要访问者模式提供的所有可扩展性,你可以完全抛弃它,只需用XYCalculator.visit调用自己来递归地遍历树。

答案 2 :(得分:0)

就个人而言,我会创建一个具有重载函数的绘图类(每个节点类型一个),而不是尝试使用某种复杂的继承解决方案挂钩到现有结构。

答案 3 :(得分:0)

我终于找到了一个装饰设计模式的“优雅”解决方案。 此模式用于扩展对象而不更改其接口。

GNode 装饰/扩展节点:

template <class T> struct GNode : public T, public Drawable
{
  virtual void accept(Visitor& v); // override Node::accept()
}

正如您所看到的,它需要对后端结构稍作改动:

struct Node
{
  Label label;
  Node* r, l;
  virtual void accept(Visitor& v);
};

就是这样! GNode 是一个节点。我们现在可以创建一个GNode的二叉树,并通过后端结构中的虚方法accept()来访问它。

在我们绝对遵循我的问题的情况下,即我们无法修改后端并且它没有上面提到的虚拟入口点,我们可以添加功能到GNode映射它包装到自身的Node。因此,访问GNodes(只能访问其儿子)的访问者可以找到其儿子的GNode。是的,这是具有上述解决方案的虚拟关键字作业!但我们永远不知道是否有人会在这种情况下真实。

作为所有这一切的结论:你表达问题的方式总会影响解决方法。