-
Notifications
You must be signed in to change notification settings - Fork 130
Manual updates vs. Signals
This example demonstrates, how even a simple case of manual change propagation results in a disproportionate amount of additional code, and how signals can be used to accomplish the same thing in a more concise and safe manner.
Here's a class Entity
with two dimensions Width
and Height
that can be changed imperatively:
class Entity
{
public:
int Width = 0;
int Height = 0;
};
Initially, we want to get its size (width * height
).
An obvious solution would be adding a simple member function:
int Size() const { return Width * Height; }
This gets the job done, but whenever Size()
is called, the calculation is repeated, even if the the dimensions did not change after the previous call.
With this simple example that's fine, but let's assume calculating size is an expensive operation.
We rather want to re-calculate it once after width or height have been changed, save the result, and return it in Size()
.
Furthermore, external clients should be able to react to changes of Size
, so we need a mechanism to support that.
In summary:
- Re-calculate size when it changes.
- Allow clients to react to size changes.
First, we handle re-calculation part:
class Entity
{
public:
int Width() const { return width_; }
int Height() const { return height_; }
int Size() const { return size_; }
void SetWidth(int v)
{
if (width_ == v) return;
width_ = v;
updateSize();
}
void SetHeight(int v)
{
if (height_ == v) return;
height_ = v;
updateSize();
}
private:
// If we forget to call this, size_ != width_ * height_
void updateSize() { size_ = width_ * height_; }
int width_ = 0;
int height_ = 0;
int size_ = 0;
};
Entity myEntity;
// Set dimensions
myEntity.SetWidth(20);
myEntity.SetHeight(20);
// Get size
auto curSize = myEntity.Size();
This adds quite a bit of boilerplate code and as usual when having to do things manually, we can make mistakes. And what if more dependent attributes should added? Using the current approach, updates are manually triggered from the dependencies. This requires changing all dependencies when adding a new dependent values, which gets increasingly complex.
Next, we add the code to handle the second requirement . To do this, we use callbacks:
#include <vector>
class Entity
{
public:
// ...
// Added an additional function to API of the class
template <typename F>
void AddSizeChangeCallback(const F& cb)
{
sizeCallbacks_.push_back(cb);
}
// ...
private:
// ...
void updateSize()
{
auto oldSize = size_;
size_ = width_ * height_;
// Invoke callbacks manually
if (oldSize != size_)
for (const auto& cb : sizeCallbacks_)
cb(size_);
}
std::vector<std::function<void(int)>> sizeCallbacks_;
// ...
};
Entity myEntity;
// Callback on change
myEntity.AddSizeChangeCallback([] (int newSize) {
redraw();
});
The code got even more complex.
Instead of implementing the callback mechanism ourselves, we can use external libraries for that, for example boost::signals2
, which handles storage and batch invocation of callbacks:
#include <boost/signals2.hpp>
class Entity
{
public:
// ...
template <typename F>
void AddSizeChangeCallback(const F& f) { sizeSig_.connect(f); }
// ...
private:
// ...
void updateSize()
{
auto oldSize = size_;
size_ = width_ * height_;
if (oldSize != size_)
sizeSig_(size_);
}
boost::signals2::signal<void(int)> sizeSig_;
// ...
};
This is not much different from the previous example. In summary, the main issues with the solution so far are error-proneness, boilerplate code, complexity and API pollution. The source of these issues is the fact that change propagation must be handled by hand.
This implementation solves all of the above mentioned problems:
#include "react/Domain.h"
#include "react/Signal.h"
#include "react/Observer.h"
using namespace react;
REACTIVE_DOMAIN(D)
class Entity
{
public:
USING_REACTIVE_DOMAIN(D)
VarSignalT<int> Width = MakeVar<D>(0);
VarSignalT<int> Height = MakeVar<D>(0);
SignalT<int> Size = Width * Height;
};
Entity myEntity;
// Set dimensions
myEntity.Width <<= 20;
myEntity.Height <<= 20;
// Get size
auto curSize = myEntity.Size(); // Or more verbose: myEntity.Size.Value()
Size
now behaves like a pure function of Width
and Height
similar to Solution 1.
But behind the scenes, it still does everything it does in Solution 2, i.e. only re-calculate size when width or height have changed.
Every reactive value automatically supports registration of callbacks (they are called observers here):
// Callback on change
Observe(myEntity.Size, [] (int newSize) {
redraw();
});
// Those work, too
Observe(myEntity.Width, [] (int newWidth) { ... });
Observe(myEntity.Height, [] (int newHeight) { ... });