前后端设计:如何做到后端与前端的彻底解离?

发布于 2024-10-31 19:28:42 字数 1102 浏览 1 评论 0原文

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

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

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

// 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 种节点(它是一个图),所以我正在寻找一些优雅、灵活、代码行数尽可能少的东西(因此包装器模板想法):)

非常感谢。

My question is: (is the above|what is) the right way to create a non intrusive front-end?

I am explaining my problem with a simplified example.

I have a back-end implementing a binary tree:

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

I would like now to implement the front-end to print the tree graphically. So my idea is to extend the back-end with graphical properties by wrapping it :

// 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);
}

There is a problem now to create a visitor printing the binary tree:

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
    };
};

As explained in comments, the back-end does not use my extended class.

Writing GNode "is-a" Node is not a solution neither since I would have to put the accept() method in the Node class as virtual and override it in GNode but I can't modify the back-end. Then, someone could say too that there is no need to declare accept() in the back-end, downcasting Node* to GNode* would work. Yes it works, but it downcasts...

In my case, I have ~10 kinds of nodes (it is a graph), so I am looking for something elegant, flexible, with as few lines of code as possible (hence the wrapper template idea) :)

Thank you very much.

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

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

发布评论

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

评论(4

酒儿 2024-11-07 19:28:42

完全分离代码是不可能的。他们必须说话。如果你真的想最大程度地强制解耦,应该使用某种IPC/RPC机制并有两个不同的程序。

也就是说——我不喜欢访客模式。

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

您可以在图形和行为之间建立实体关系,这是一个商业逻辑问题...

您将需要一些 thungus 来保存您的绘图上下文(img、屏幕、缓冲区)。

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

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

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

如果您发现您的渲染变得基于案例,具体取决于图形的类型,我建议将案例推送到图形,并重构为具有 get_primitives_list(),其中所需的基元是返回图形返回(我假设在某种程度上,您有核心图元、直线、圆、弧、标签等)。

我一直发现面向对象分析会浪费脑力,并且应该只针对手头的任务进行足够的分析。 YAGNI 是一个伟大的原则。

To absolutely disassociate code is impossible. They have to talk. If you really want to enforce decoupling to the maximal extent, some sort of IPC/RPC mechanism should be used and have two different programs.

That said -- I don't like visitor patterns.

You have a Graphical object, which is linked against a Behaving object. Maybe there are rules between behavior and the graphics, e.g., boundaries can't overlap.

You can do your entity relationship whatevers between the Graphicals and the Behaves, that's a biz logic question...

You will need some thungus that holds your drawing context (img, screen, buffer).

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

Your Graphical will have either an inheritance or a composition relationship with behaves.
At any rate, they will have the interface needed to do Drawing.

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

If you are finding that your Render is becoming case-based depending on the kind of Graphical, I suggest pushing the cases over to the Graphical, and refactoring to have a get_primitives_list(), wherein the needed primitives are returned for Graphical to return (I am presuming that at some level, you have core primitives, lines, circles, arcs, labels, etc).

I have always found that OO analysis lends itself to wasting mental energy and should be done only enough for the task at hand. YAGNI is a tremendous principle.

Saygoodbye 2024-11-07 19:28:42

如果您的包装类 (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 调用自身来递归地遍历树。

If your wrapper class (GNode) didn't have to maintain any state across visits (i.e., it only had one field - the wrapped Node object), you could use a pointer or a reference to the wrapped object instead of a copy, and then you would be able to wrap any node at runtime.

But even if you do maintain state (the x,y coordinates), don't you really just infer it from the wrapped object? In that case, wouldn't it be better to separate your visited class from the inferred data? For instance, consider this implementation:

// 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);
    };
};

Of course, if you don't need all the extensibility the visitor pattern offers, you can throw it away altogether and just walk the the tree recursively with XYCalculator.visit calling itself.

倾城月光淡如水﹏ 2024-11-07 19:28:42

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

Personally, I would make a drawing class with overloaded functions (one for each node type) rather than trying to hook into the existing structure with some sort of complicated inheritance solution.

深空失忆 2024-11-07 19:28:42

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

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 是一个节点。现在,我们可以通过后端结构中的虚拟方法accept()来创建GNode的二叉树并访问它。

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

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

I finally found an "elegant" solution with the decorator design pattern.
This pattern is used to extend an object without changing its interface.

GNode decorates/extends Node:

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

As you can see, it requires a little change in the back-end structure:

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

That's it ! GNode is-a Node. We can now create a binary tree of GNodes and visit it thanks to the virtual method accept() in the back-end structure.

In the case when we absolutely follow my question, i.e. we can't modify the back-end and it doesn't have the virtual entry point presented above, we can add features to GNode mapping the Node it wraps to itself. So that a visitor visiting GNodes (that can only have access to its sons) can find the GNodes of its sons. Yes, this is the virtual keyword job with the above solution! But we never know if someone would be in this case for real.

As a conclusion to all this: the way you express a problem always influences the way to resolve it.

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