2.4.8 Signals
Motivation
A problem carried over from the traditional MVC approach is the need for the Model to hold a collection of its listeners for notification purposes. This introduces a loose dependency between the Model and the listener interface that all Views must implement.
The design presented in this section decouples this dependency between Models and Views, extracting the notification system in a separate object, the Signal, acting as a relay between the two. Althought not immediately apparent, the benefits of this approach are important:
- Different Signals can represent different changes, providing the flexibility of a Qualified Notification.
- Signals can be made to call specific View’s methods, instead of a
generic
notify()
, passing specific arguments.
Design
A Signal class holds the notification infrastructure previously held by the Model:
a list of listeners and methods for listeners to register and unregister.
Notification to listeners is triggered via a Signal.emit()
method.
The Model defines its signalling capabilities by exposing member variables of type Signal. These member variables are given appropriate names to convey the nature of each reported event. Listeners subscribe to the signals they are interested in.
When a change of the appropriate type occurs, the Model triggers the
notification by calling emit()
on the Signal object, which in turn
will notify each individual listener.
Variation: additional flexibility to notification
A basic implementation just notifies the listeners. Increased flexibility can
be obtained by allowing the passing of arguments to the emit()
method,
allowing further qualification of the emitted signal.
The Signal can be made configurable in its functionality. A Signal class could for example implement three strategies for notification:
- open: the notification is performed as soon as triggered.
- closed: the notification is silenced.
- hold: the notification is not delivered, but it is retained for later (i.e. an Accumulator-like behavior)
Practical examples
A Signal implementation conforming to the above design can be found in Boost.Signal2.
In the following example, we demonstrate the registration of an arbitrary method
print_sum
to a Signal object and its execution when the signal is emitted.
The emit
functionality is implemented through operator()
. Note how
parameters can be passed to the connected functions.
void print_sum(float x, float y) {
std::cout << "The sum is " << x + y << std::endl;
}
void main() {
boost::signals2::signal<void (float, float)> sig;
sig.connect(&print_sum);
// emit
sig(5., 3.);
}
Another example of Signal design is Qt Signal/Slot mechanism, although the internals do not make use of a literal Signal object, and the mechanism requires the support from a special preprocessor
class Model : public QObject
{
Q_OBJECT
public:
Model() { value = 0; }
int value() const { return value; }
public slots:
void setValue(int new_value) {
if (new_value != value) {
value = new_value;
emit valueChanged(new_value);
}
}
signals:
void valueChanged(int new_value);
private:
int value;
};
The special signal valueChanged()
is emitted every time a new value is set. Receiving
objects can subscribe to the Signal and guarantee synchronization
Model *model1 = new Model()
Model *model2 = new Model()
QObject::connect(model1, SIGNAL(valueChanged(int)), model2, SLOT(setValue(int)))
model1->setValue(42)
In the above code, model2->setValue
will be called when model1 valueChanged
signal is
emitted. This will synchronize the two objects to hold the same value.
Practical example variation: notifying Views
One important feature of Qt signals is that they are not only found on Models,
but also on Widgets and Views. Buttons, for example, emit a clicked()
Signal when clicked. Signals can therefore be used as a generic notification
mechanism applicable to any context where decoupling is desired.