Context
There is a modular C++ application. Each module provides an interface (an abstract class) which is used by other modules. There are no circular dependencies. The module interaction happens via direct method calls in one way, and via events (a variation of the observer pattern, similar to Qt's signals) in the other way.
For example:
// an interface towards the battery manager
class IBatteryManager
{
public:
// get the battery level
virtual int batteryLevel() = 0;
// the system is about to run out of battery and enter
// a power saving mode
virtual Event<> &aboutToRunOutOfBattery() = 0;
};
The Event
class looks like this:
template<typename... tArgs>
class Event
{
public:
// client: the provided function will be called when this
// event is emitted
void subscribe(std::function<void(tArgs...)>);
// server: emit the event
void notify(tArgs...);
};
Problem and Possible Solution
Sometimes a fire-and-forget event is not enough. A module might want to notify its clients about some event and wait until it is processed (and maybe get back some values).
For instance, BatteryManager
might want to wait until all clients have processed the about-to-run-out-of-battery event before entering the power saving mode.
I can imagine multiple ways how this can be done. One of them would be to extend the Event
class with the future/promise pattern in the following way:
template<typename tValue, typename... tArgs>
class Event
{
public:
// client: now must return some value
void subscribe(std::function<Future<tValue>(tArgs...)>);
// server: this future is fulfilled as soon as all clients
// have completed their promises
Future<std::vector<tValue>> notify(tArgs...);
};
Questions
Updated: I have rephrased questions a bit to be more clear.
Is there a design pattern which describes the approach shown above? In other words, does such approach have a commonly accepted name, vocabulary, pros/cons, a canonical implementation, a list of things one needs to pay attention to, and other stuff what patterns usually have?
I am asking because this might be not a problem which is unique to me. So I do not want to end up reinventing the wheel, both in terms of implementation (by probably re-doing someone else's mistakes) and terminology (by introducing my own terms, which will make the communication with other developers harder).
Secondary questions: Are there maybe better approaches? What would be the best practice in this case?
1 Answer 1
The point of the "Event" pattern is the source does not know or care if any listeners are attached. The source is decoupled from the clients. Events often are executed synchronously, but the source should not know about that either.
But in your case you want to wait for a return value, which means you are depending on the client and you are making assumptions about the behavior of the client (otherwise you wouldn't care if it had finished processing or not). So you have a straightforward dependency, which means the source should just go ahead and call the method directly - skip all the indirection with subscription and whatnot.
In short, what you are trying to do is not a recognized as a "pattern", since it is so simple: You want to call a method on another object.
-
The source (
BatteryManager
in this example) does not care if and how many listeners are attached. It only cares if all attached listeners (whatever the count is) have processed the request. If there are no listeners,notify()
just returns an empty vector. At least this is my current vision. Could you please update your answer with some code examples? I do not really understand yet what your proposal is. Do you suggest to inherit all clients from, say,ILowBatteryObserver
, which would have areact()
method which is going to be called by the source?Kane– Kane10/30/2017 19:23:46Commented Oct 30, 2017 at 19:23 -
@Kane: I'm just suggesting BattteryManager directly call the method(s) you want called, nothing else. No need for inheritance or subscriptions or patterns or code examples - you just call the method! Nothing in your question suggest you need anything more complex than this.JacquesB– JacquesB10/30/2017 20:41:43Commented Oct 30, 2017 at 20:41
-
The source does not know who its clients are. Thus, it does not know which method and on which objects to call. If you mean
BatteryManager(ClientA *a, ClientB *b, ClientC *c)
anda->doThis(); b->doThat(); c->doStuff();
, then it is not scalable and introduces circular dependencies between modules.Kane– Kane10/30/2017 21:09:48Commented Oct 30, 2017 at 21:09 -
@Kane: I will ague something is off in the way you state the problem. You cannot on one hand state the source does not know anything about the clients, and on the other hand wait for the clients return a value. How can the source handle the returned value in a meaningful way if it doesn't know anything about what it means? If the source depend on the clients process the message synchronously, then it depends on certain behavior in the clients. So I am pointing out that the source do make assumptions about the clients, which means they are not decoupled. You already have the dependency.JacquesB– JacquesB10/31/2017 07:19:05Commented Oct 31, 2017 at 7:19
-
1The source does not do any assumptions about clients. The source defines the interface with the event with an acknowledgement mechanism. The contract is "whoever you are, if you are interested in this event, you will have to asynchronously send back the confirmation". So the source enforces the specific behaviour of its clients. Yet it does not care what the clients are and how do they handle the event.Kane– Kane10/31/2017 10:41:19Commented Oct 31, 2017 at 10:41
void
with an actual return value)?