I am upgrading a design where data was lightly coupled with the UI:
class Object {
UI * ui;
};
class UI {
Object * object;
};
It was fairly straightforward to push update notifications to the ui through the UI pointer, but new requirements for data to be entirely separated from UI and also for different objects to have multiple different UI representations, so a single UI pointer no longer does it nor is allowed to be part of the data layer whatsoever.
It is not possible to use something like QObject
and signals due to its overhead because of the high object count (in the range of hundreds of millions) and QObject
is several times larger than the biggest object in the hierarchy. For the UI part it doesn't matter that much, because only a portion of the objects are visible at a time.
I implemented a UI registry, which uses a multihash to store all UIs using the Object *
as a key in order to be able to get the UI(s) for a given object and send notifications, but the lookup and the registration and deregistration of UIs presents a significant overhead given the high object count.
So I was wondering if there is some design pattern to send notifications between decoupled layers with less overhead?
A clarification: most changes are done on the UI side, the UI elements keep a pointer to the related object, so that's not an issue. But some changes made to some objects from the UI side results in changes which occur in related objects in the data layer which can't be predicted in order to request update of the affected object's UIs. In fact a single change on the UI made to one object can result in a cascade of changes to other objects, so I need to be able to notify their eventual UI representations to update to reflect those changes.
I managed to come up with a immensely more efficient solution.
Instead of tracking all UIs using a "UI registry" I created a Proxy
object and replaced the UI registry with a Proxy registry.
The Proxy
object is created for each object that has any visual representation. It itself extends QObject
and implements an interface to access the properties of the underlying Object
, wrapping them in Qt style properties.
Then the Proxy
object is used as a property for each UI
to read and write the underlying Object
properties, so it works "automatically" for every UI that might be referencing the particular proxy.
Which means there is no need to track every particular UI
for every Object
, instead the lifetime of the Proxy
is managed simply by counting the number of UIs which reference it.
I also managed to eliminate all the look-ups which would not yield a result by adding a single bit hasProxy
flag (had a few free bits left from the other flags) which is toggled for every object when a proxy is created or destroyed. This way in the actual Object
's members I can quickly check if the object has a proxy without a look-up in the registry, if not use the "blind" data routines, if so look-up the proxy and manipulate the object through it. This limits registry look-ups to only the few which will actually get a result and eliminates a tremendous amount of those which would be pretty much in vain, just to realize the object has no visual representation at all.
In short, to summarize the improvements over the previous design:
the registry is now much smaller, from having to store a pointer for the object itself and a vector of all associated UIs I am now down to 8 bytes for the Proxy
- the pointer to the object and a counter for any number of associated UIs
notifications are automated, only the proxy needs to be notified, it automatically notifies all UIs which reference it
the functionality previously bestowed to the UIs is now moved to the proxy and shared between all UIs, so the UIs themselves are lighter and easier to implement, in fact I've gone from having to specialize a unique QQuickItem
for each object type to being able to use a generic QML Item
without having to implement and compile any native classes for the UI
stuff I previously had to manage manually, both the actual notifications and the objects, responsible for them are now managed automatically
the overhead in both memory usage and CPU cycles has been reduced tremendously. The previous solution sacrificed CPU time for less memory usage relative to the original design, but the new design eliminates most of the CPU overhead and decreases memory usage further, plus makes the implementation much easier and faster.
It's like having a cake and eating it too :)