4

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?

Kane
  • 179
  • 5
  • 3
    Possible duplicate of [Choosing the right Design Pattern](https://softwareengineering.stackexchange.com/questions/227868/choosing-the-right-design-pattern) – gnat Oct 30 '17 at 16:00
  • What's wrong with the "design pattern" you've come up with (namely, replacing `void` with an actual return value)? – Robert Harvey Oct 30 '17 at 16:46
  • @RobertHarvey, there is nothing wrong with it at the moment (I have not started implementing it yet). I am basically asking if it has a commonly accepted name, vocabulary and maybe a canonical implementation etc.? I just do not want to end up reinventing the wheel. – Kane Oct 30 '17 at 16:57
  • @Kane: Why don't you just call the method directly? The point of events is you don't know of care if anybody reacts to them. But in this case you require somebody to react, so why not just call them directly? – JacquesB Oct 30 '17 at 17:21
  • @Kane: So far, what I see here is just sensible object-oriented design. What you're doing here (adding a return type) is no different than what legions of object-oriented developers have been doing since C++ was first invented. – Robert Harvey Oct 30 '17 at 17:32
  • @RobertHarvey, true, but the same statement also applies to, say, singletons. Lots of developers had to implement singletons. But some of them named their concepts `ConstructableOnlyOnce`, while some others -- `UniqueInstance`. Many also had a race condition in their implementation of double-checked locking. That is why there is the observer pattern, which addresses these issues. – Kane Oct 30 '17 at 19:07
  • Looking at your singleton example, had you named your concept `ConstructableOnlyOnce` or `UniqueInstance`, I suspect that your development team would have immediately noticed, "Oh, that's a Singleton." Had nobody mentioned that, using `ConstructableOnlyOnce` would have been a perfectly valid alternative. It's unreasonable to have perfect clairvoyance on such matters, and since most things *do not* have specific names, your approach to this problem strikes me as... well, inefficient. See also https://softwareengineering.meta.stackexchange.com/a/6583 – Robert Harvey Oct 30 '17 at 19:24
  • To put it another way, given a sufficiently competent group of software developers, any well-known terminology is more likely than not to already be self-evident to your team, while those concepts you struggle to find well-known terminology for are, by definition, not well-known anyway. – Robert Harvey Oct 30 '17 at 19:26
  • @RobertHarvey, that is why I am asking this questions here, i.e. to, as you have said, a sufficiently competent group of software developers. An answer "we do not recognise the concept" is also perfectly fine. – Kane Oct 30 '17 at 19:51
  • Which is an answer your own team could have provided. Asking here is like finding a word in the dictionary by looking for its definition. You can see the problem there, can't you? – Robert Harvey Oct 30 '17 at 19:54

1 Answers1

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.

JacquesB
  • 57,310
  • 21
  • 127
  • 176
  • 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 a `react()` method which is going to be called by the source? – Kane Oct 30 '17 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 Oct 30 '17 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)` and `a->doThis(); b->doThat(); c->doStuff();`, then it is not scalable and introduces circular dependencies between modules. – Kane Oct 30 '17 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 Oct 31 '17 at 07:19
  • @Kane: To be clear, I'm not introducing a circular dependency, I'm pointing out you already have one. If you want to avoid this circular dependency, you could consider calling a shared interface rather than the class directly. But you should be clear on what you want to achieve before starting searching for patterns to use. – JacquesB Oct 31 '17 at 07:51
  • 1
    The 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 Oct 31 '17 at 10:41
  • Could you please give an example how a shared interface could help here? – Kane Oct 31 '17 at 10:42
  • @Kane: But *why* does the source care about this confirmation? – JacquesB Oct 31 '17 at 11:48
  • @Kane - The EVENT mechanism is not intended to do what you want. The event concept models the concept of here's an event and all listeners should get this signal at exactly the same time and the event signaler proceeds on-wards. Fire and forget. Like the start signal of a race. Because of limitations of the computer, listeners are instead notified synchronously which gives people the idea to try and do what you want. However, that breaks the metaphor. If your BatteryManager needs to wait then make the 'wait' mechanism explicit instead of hidden in the client code handling an event. – Dunk Oct 31 '17 at 18:39
  • ...In your specific example, you should have some higher level module that receives the low battery event and that higher level module should be responsible for coordinating whatever your system has to do in response. Why should the BatteryManager (ugh...just call it Battery) be dependent upon various random modules correctly handling the event? You know that some doofus isn't going to read the docs and will then lockup the BatteryManager. FWIW, since it is the BatteryManager that is locked, the person writing the BatteryManager code will get the blame, not the doofus. – Dunk Oct 31 '17 at 18:55