有效地在解耦的设计层之间传递通知

4

我正在升级一个设计,其中数据与UI轻松耦合:

class Object {
    UI * ui;
};


class UI {
    Object * object;
};

通过UI指针向UI推送更新通知非常简单,但是新的要求需要将数据完全与UI分离,并且不同的对象具有多个不同的UI表示形式,因此单个UI指针不再起作用,也不能成为数据层的一部分。由于对象数量很高(达到数亿级别),因此无法使用类似QObject和信号的东西,因为它会带来很大的开销,而QObject比层次结构中最大的对象大几倍。对于UI部分来说,这并不太重要,因为每次只有一部分对象可见。
我实现了一个UI注册表,它使用multihash存储所有UI,使用Object *作为键,以便能够获取给定对象的UI并发送通知,但是查找和注册/注销UI会产生显着的开销,考虑到对象数量很高。
所以我想知道是否有一些设计模式可以在解耦层之间发送通知,开销更小?
澄清一下:大多数更改都是在UI端完成的,UI元素保留了与相关对象的指针,因此这不是问题。但是,从UI端对某些对象进行的一些更改会导致数据层中相关对象发生更改,这些更改无法预测,以便请求更新受影响对象的UI。实际上,对一个对象进行的单个UI更改可能会导致级联更改其他对象,因此我需要能够通知其最终的UI表示形式以更新以反映这些更改。

听起来实际问题在于UI注册/注销性能的效率,而不是实际设计。您可以通过存储与模型对象对应的UI元素的数组或集合(组合模式可能适用)来加快速度,但这可能无法完全满足对象不得知道其UI的要求。 - Sergey Kalinichenko
@dasblinkenlight - 这就是我的困境。试图找到一种既能拥有蛋糕又能吃掉它的方法。为每个对象拥有一个UI数组似乎是一个简单明了的解决方案,但它会污染对象模型并引入更多的内存开销 - 一个数组不仅有指针还有大小和容量。在没有任何UI的情况下,每个对象额外增加12个字节,对于32位平台上的典型100万对象场景来说,浪费了1GB的内存,已经占用了可寻址内存的1/3。我更愿意仅为实际拥有UI的对象存储UI数据。 - dtech
您可以随时滚动UI关联。假设UI结构相对静态,您可以在对象通过UI“可见”时设置关联(和数组),并在对象“滚动出屏幕”时“回收”用于关联的内存。 - Sergey Kalinichenko
@dasblinkenlight - 至少在渲染方面,Qt 处理了那些东西。问题是尽管对象总数巨大,但通常每次最多只有几千个“可见”(具有 UI,不在屏幕上)对象。这就是我不想在每个 1 亿个对象中都有一个数组的原因,当我一次只会有几千个 UI 对象时。我已经确认额外的 CPU 时间是为节省内存和提高设计的额外灵活性和清晰度而进行的良好交易,我只是想知道是否可以做得更好。 - dtech
软件设计没有什么神奇的地方,特别是在设计直接的对象关联时:要么需要大量的内存和少量的 CPU,要么需要较少的内存和大量的 CPU,除非你能想出一种非常便宜(从 CPU 方面考虑),能够在旧对象离开视图并有新的对象进入视图时,“推动”对象与界面元素关联的方法。 - Sergey Kalinichenko
显示剩余4条评论
2个回答

1
一种通用的解耦通信机制是发布-订阅模式。在这种情况下,更新过的对象将向消息队列发布通知,然后消息队列负责通知已向其注册对接收特定类别通知感兴趣的UI组件。
原理上,这与您已经尝试过的UI注册表类似。主要区别在于,要更新的UI组件不仅仅是通过它们引用的Object来识别,而是通过通知类型来识别。
这样可以在特定性和状态保持之间取得平衡:如果模型被设置为每个与Objectobj相关的UI组件都会在obj的每次更新时得到通知,那么它相当于UI注册表。另一方面,模型可以被安排成只有某些UI组件在特定子类别的Object发布更新时才会得到通知,然后每个组件可以根据通知内容自行检查是否需要修改其状态。将其推而广之,每个UI对象都可以被任何Object发布的任何消息通知,这相当于全局“更新UI状态”的方法。
发布-订阅模型包括这两个极端,也包括中间的范围,在其中您可能会找到一个合适的折衷方案。

0

我想出了一种更为高效的解决方案。

我创建了一个Proxy对象来替代UI注册表,不再需要跟踪所有的UI。该Proxy对象是为每个具有任何可视化表示的对象创建的。它本身扩展了QObject并实现了一个接口,用于访问底层Object的属性,并将其包装成Qt样式的属性。

然后,将Proxy对象用作每个UI的属性,以便读取和写入底层Object的属性,因此对于可能引用特定代理的每个UI而言,它都可以“自动”工作。

这意味着无需为每个Object跟踪每个特定的UI,相反,Proxy的生命周期仅通过计算引用它的UI数量来管理。

我也设法通过添加一个单一位的 hasProxy 标志(从其他标志中留下几个空闲位)来消除所有无法产生结果的查找,每当创建或销毁代理时对每个对象进行切换。这样,在实际 Object 的成员中,我可以快速检查对象是否具有代理,而无需在注册表中查找;如果没有,则使用“盲”数据例程,如果有,则查找代理并通过它操作对象。这将仅限于少数实际获得结果的寄存器查找,并消除了大量完全徒劳无功的查找,只是为了意识到对象根本没有视觉表示。

简而言之,总结改进前设计的改进:

  • 现在注册表要存储指向对象本身和所有相关UI的向量,因此代理的大小大大缩小,仅为8个字节 - 对象的指针和任意数量相关UI的计数器。

  • 通知是自动化的,只需通知代理,它会自动通知所有引用它的UI。

  • 以前赋予UI的功能现在已经移动到代理并在所有UI之间共享,因此UI本身更轻便、更易于实现。事实上,我已经从必须为每种对象类型专门定制一个唯一的QQuickItem转变为可以使用通用的QML Item而无需为UI实现和编译任何本地类。

  • 我以前手动管理的东西,包括实际通知和负责它们的对象,现在都是自动管理的。

  • 在内存使用和CPU周期方面的开销大大减少。以前的解决方案为了相对于原始设计减少内存使用而牺牲了CPU时间,但新设计消除了大部分CPU开销,进一步减少了内存使用,并使实现更加容易和快速。

这就像是又要拥有蛋糕,又要享用它一样 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接