前端/后端设计:如何将后端与前端完全分离
Front-end/Back-end design: how to absolutely dissociate the back-end from the front-end?
我的问题是:(以上是|什么是)创建非侵入性前端的正确方法吗?
我用一个简单的例子来解释我的问题。
我有一个实现二叉树的后端:
// 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。是的,这是具有上述解决方案的虚拟关键字作业!但我们永远不知道是否有人真的会在这种情况下。
作为这一切的结论:你表达问题的方式总是影响解决问题的方式。
- ArrayFire中统一后端的使用
- 有没有办法知道Tracer是否成功地完全连接到了jaegerclientcpp中的jaeger后端服务器
- 如何在 LLVM 后端的机器级别找到 def-use 链
- 如何在 boost::msm 中实现可以访问状态机 (SM) 的后端/前端的"BaseState"
- 在单链表中的后端插入节点
- CUDA(GPU)作为OpenCV后端
- 如何将C 中的后端写入与TCL/TCK用户界面连接
- 在同一应用程序中,可以将C 用作后端和Javafx
- 是否可以使用 c++ 作为 Electron.js 的后端
- 我如何将 c++ 中的向量序列化为 char,以便于将 mondodb 用于后端
- 从Web前端发送请求到C 后端
- 在Boost Meta状态机的后端访问前端
- 前端和后端
- 将Python GTK GUI前端与后端一起使用C++后端
- 网站的后端代码怎么可能是C++而前端是PHP
- 如何设计一个具有Qt前端和可分离后端的程序
- WEB UI前端和c++后端之间的通信
- 前端Java,后端c++,如何加入
- 设计 Javascript 前端<> C++后端通信
- 前端/后端设计:如何将后端与前端完全分离