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

Front-end/Back-end design: how to absolutely dissociate the back-end from the front-end?

本文关键字:后端 前端 分离      更新时间:2023-10-16

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

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

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

// 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"节点也不是一个解决方案,因为我必须把accept()方法放在Node类中作为虚拟的,并在GNode中重写它,但我不能修改后端。然后,有人也会说,没有必要在后端声明accept(),将Node*向下转换为GNode*就可以了。是的,它工作,但它向下投射…

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

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

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

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

你可以在图形和行为之间做你的实体关系,这是一个业务逻辑问题…

你将需要一些thungus来保存你的绘图上下文(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(); 
};

如果你发现你的渲染正在变成基于案例的,这取决于图形的类型,我建议把案例推到图形,重构成一个get_primitives_list(),其中需要的原语被返回给图形返回(我假设在某种程度上,你有核心原语,线,圆,弧,标签等)。

我总是发现OO分析本身会浪费精力,应该只做手头的任务。YAGNI是一个伟大的原则。

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

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

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

GNode 装饰/扩展 Node:

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

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

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