我的问题是:(上面是什么)是创建非侵入式前端的正确方法?
我用一个简化的例子来解释我的问题。
我有一个实现二叉树的后端:
// 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种节点(它是一个图形),所以我正在寻找一些优雅,灵活的东西,尽可能少的代码行(因此包装模板的想法):)
非常感谢。
答案 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。是的,这是具有上述解决方案的虚拟关键字作业!但我们永远不知道是否有人会在这种情况下真实。
作为所有这一切的结论:你表达问题的方式总会影响解决方法。