Using a single shared Object as a message bus in the entire application?
-
Qt's doc and examples seems to exclusively encourage putting signals in each component's class definitions. So that's what I did previously. However I recently found out that you can actually emit a signal in a different QObject. I always thought the "emit MySignal()" to be like a private call, but no, signals are public functions, you can do "someobj->MySignal()", or better yet "Q_EMIT someobj->MySignal()" to make it stand out more/make it greppable.
I'd like to hear your thoughts on an application architecture style where you use a single shared object as a public message bus throughout the entire app. So all the components of the app have a reference to this QObject given in their constructor, all the "public" signals used by the application are defined in this QObject instead of being defined in the object that emits them.
I've written a very simple example app to showcase this:
https://gitlab.com/tarre/message-bus-testNotice how the features in the 2nd and 3rd commits were bolted on to the existing architecture in a very straight-forward way, without having to modify unrelated source files.
Pros:
-
Simpler application architecture. Since there's just a single source of major events, a new developer doesn't need to understand (as much) where their code fits with all the rest. They can add certain features by just writing signal handlers for signals coming from that single event bus. Also, they can discover how components interact with each other by searching for the usage of each signal from that single file. When writing new components, they don't need to think as much where to place it, since there's already this structure in place.
-
Signals emitted by objects created at a low "depth" (for example Hardware -> SensorManager -> TemperatureSensor) can be used by components anywhere in the app in a simple clean way, without writing repetitive boilerplate code just to be able to connect a signal to a slot in a "far object". Also, this would have been something a new dev would struggle with.
-
If there's a refactoring somewhere, if a different component is now responsible for emitting SignalX, consumers don't have to change anything. Previously the only way to do this would be to use interfaces, which is boilerplatey.
Cons:
-
Not Qt-ic (whatever the Qt equivalent of "pythonic" is)
-
You lose the ability to use sender() to get a reference to the QObject* that did the real emit. With this, the sender is always the message bus object. (Correct me if I'm wrong. If there's a way to get the sender without adding it as an argument in every signal, that would be wonderful.)
What do you think?
-
-
Hi,
The signals are meant to be emitted from within the class they are defined in.
They are there to tell interested parties that something has changed within the class.
The fact that they are now public is an implementation detail.You also lose encapsulation with your implementation.
You already have a message bus with for example DBus.
-
Pros 1: is a tooling problem, not an architecture one. GammaRay can help with that.
Pros 2: Breaking of encapsulation is a major problem, you also lose a lot of abstraction. In your example, if you connect directly User->TemperatureSensor you lose the ability of changing implementations of TemperatureSensor and SensorManager without also changing User.
Pros 3: it's a double edged sword. see point 2
-
For reference, this was asked on the mailing list as well[1].
As for the actual question, if you need a bus, then make a bus. Having one global
QObject
which aggregates all the signals for an application isn't a bus, it looks like one, but it's simply a singleton - an application global state. Additionally, you already have such a global object -QCoreApplication
, you can send it (custom) events, which it will already ignore, and on the consumer side you can filter them out by installing an event filter and respond to what you want. However, the application object would be just your single point of contact, not one that'd be responsible for what you do with the events, nor should it care about what APIs the different components of your application expose. As @VRonin said, you're attempting to have the "god object", which is a bad idea - keep concerns separate instead.- Simpler application architecture. Since there's just a single source of major events, a new developer doesn't need to understand (as much) where their code fits with all the rest.
Quite the opposite, every object is going to be coupled to that global thing, so every developer will need to know where and when these signals originate and if it's valid to respond to them, and if responding to them won't create problems. Basically every developer is going to need to know everything about the system before they write a single line. That's what strong coupling does.
They can add certain features by just writing signal handlers for signals coming from that single event bus.
And emitting other signals may break stuff they have no idea about, since everything is coupled together.
Also, they can discover how components interact with each other by searching for the usage of each signal from that single file.
... mainly by fixing hard to diagnose bugs in the object dependency chain ...
- Signals emitted by objects created at a low "depth" (for example Hardware -> SensorManager -> TemperatureSensor) can be used by components anywhere in the app in a simple clean way, without writing repetitive boilerplate code just to be able to connect a signal to a slot in a "far object". Also, this would have been something a new dev would struggle with.
There's a better way - delegating the signals upwards. There's no boilerplate involved, you connect one signal to the next at the appropriate places and only if it makes sense. "Can be used by components anywhere" is exactly the opposite of how you design a flexible system. If they're components they should know as little as possible about other components, not the other way around.
- If there's a refactoring somewhere, if a different component is now responsible for emitting SignalX, consumers don't have to change anything. Previously the only way to do this would be to use interfaces, which is boilerplatey.
You don't know that, because everything before the actual signal is hidden behind a wall. You can't tell if the same logic was kept before or after the signal(s) were emitted.
- Not Qt-ic (whatever the Qt equivalent of "pythonic" is)
No such thing. It either is a good OO design, or it isn't. Code, style, personal preference comes later.
- You lose the ability to use sender() to get a reference to the QObject* that did the real emit. With this, the sender is always the message bus object. (Correct me if I'm wrong. If there's a way to get the sender without adding it as an argument in every signal, that would be wonderful.)
You shouldn't use
sender()
to begin with due to all the listed reasons.In conclusion, if you want a bus, design yourself a bus - something like the DBus with a strict requirement on the data interfaces/messages and such and then roll with that.
[1]: https://www.mail-archive.com/interest@qt-project.org/msg35958.html
-
This post is deleted!
-
Regarding breaking encapsulation...I don't see it as breaking encapsulation in spirit, because the appropriate emitter is still the one emitting the signal, it's just done in a bus QObject designed to facilitate connecting to signals. And the receiver has no idea who emitted the signal. Sure, the compiler won't prevent a RandomObject from emitting TemperatureChanged that's supposed to only be emitted by a TemperatureSensor, but just because the compiler won't stop you from doing something doesn't mean you're gonna do it, it would make no sense. The dev needs to assume responsability to a degree. Before Qt 5 we didn't even have compiler checks on connect() calls, didn't stop people from writing great Qt 4 apps.
Regarding DBus...
a) DBus is not crossplatform (there's mention of an "almost finished" Windows port on freedesktop, translation: unusable in production)
b) I would need to constantly convert between C/C++ types and Qt types, write serialization/deserialization code, etc, which is a nightmare
c) I would lose the automatic thread safety you get from Qt's AutoConnection (I know there's caveats, but for the most part it works great)
This sounds horrible to me. I'm not seeing what I'm gaining by using "a real bus". A QObject is a real bus. A 50 line custom-written class is a real bus (and I'd do this before I use DBus). D-Bus can give me multi-process/IPC, but I don't need that.Regarding my QObject bus being singleton...what is your justification for saying this? The AppMessageBus in my example holds no state as you can see, it just has signals. Also, having the technical ability to use QCoreApplication doesn't mean I should do it. What does it gain me? Saving me from using a QObject designed purposefully for that task? That's a lot messier. Btw, if you look at my example, it's not a global variable, I give it as a constructor argument to the components that need it.
-
Sorry for the late reply, I was really busy the last few days.
No need to get angry, you asked for an opinion, which is what I gave. Nobody said you must listen to my/our opinion.
@thierryhenry14 said in Using a single shared Object as a message bus in the entire application?:
Before Qt 5 we didn't even have compiler checks on connect() calls, didn't stop people from writing great Qt 4 apps.
I guess, but on that particular note before Qt5 signals were private (for the mentioned reasons). The fact that you can now emit a signal from outside the object is an implementation detail. Don't get me wrong, I've done it, it's sometimes useful if you have coupled objects (like a manager class that may need to force the emission through its underling), but it still is frowned upon.
Regarding DBus...
The DBus mention was just as an example, no one implied you should use it for objects communication. It wasn't designed for this, on the contrary it was conceived as an IPC layer.
b) I would need to constantly convert between C/C++ types and Qt types, write serialization/deserialization code, etc, which is a nightmare
Actually Qt has a DBus implementation, but that's really moot.
c) I would lose the automatic thread safety you get from Qt's AutoConnection (I know there's caveats, but for the most part it works great)
Again, you could simply pump messages through the event loop. I don't see how using a signal-slot call is somehow better or different.
This sounds horrible to me. I'm not seeing what I'm gaining by using "a real bus".
You gain (some) decoupling of the implementation from the data interface. Or to phrase it another way:
Why would unrelated components need to know what others implement, support, demand or represent. To give you an example, how does my radar control care about what data types are marshaled through and what interfaces are implemented by my data processing. Why would the radar component need to be recompiled (or possibly changed) to accommodate completely unrelated changes.Regarding my QObject bus being singleton...what is your justification for saying this?
Just by merit of it coupling everything together. Everybody knows about it and it knows about everything.
The AppMessageBus in my example holds no state as you can see, it just has signals.
It doesn't hold its own state, it is a state for the whole application though. It basically does what
QCoreApplication
will have done - manage event dispatching. Btw, beside some initialization of unrelated (to this case) code, this is essentially the crux of the application object.Also, having the technical ability to use QCoreApplication doesn't mean I should do it.
Of course, to reverse that argument though, having the technical ability to emit from outside a
QObject
doesn't necessarily mean that you should do it, right?Btw, if you look at my example, it's not a global variable, I give it as a constructor argument to the components that need it.
So? That's simply an implementation detail. I create my singleton objects (whenever I need them) in
main()
, similarly to howQCoreApplication
does it, but does that make them less global?