The Observer Pattern is used here to notify changes in a Person's data. For example, when age
changes, the value of can_vote()
can change too, and all observers shall be notified of this change too. A simple if-statement can handle this. But this cannot be maintained when later age
affects many other things (e.g. can_drink()
, can_quit_school()
, etc...), and when many other data members of Person
start affecting these and other things too. To handle this explosion in dependencies, I've decided to use
std::map<std::string, std::vector<std::string>> dependenciesMap
to store a list of things affected by changes in these data members. This is how I implemented this:
#include <iostream>
#include <vector>
#include <map>
#include <tuple>
enum OutputType { Int, Bool, String, NumOutputTypes, FirstType = Int };
template <typename T> struct GetOutputType;
template <> struct GetOutputType<int> : std::integral_constant<OutputType, Int> { };
template <> struct GetOutputType<bool> : std::integral_constant<OutputType, Bool> { };
template <> struct GetOutputType<std::string> : std::integral_constant<OutputType, String> { };
template <typename T>
struct Observer {
virtual void field_changed (T&, const std::string& field_name, OutputType) = 0;
};
template <typename T>
class Observable {
std::vector<Observer<T>*> observers;
public:
template <typename U>
void notify_field_change (T& source, const std::string& field_name) {
const OutputType output_type = GetOutputType<U>::value;
for (Observer<T>* obs : observers)
obs->field_changed(source, field_name, output_type);
}
void subscribe (Observer<T>* f) { observers.push_back(f); }
void unsubscribe (Observer<T>* f) { observers.erase(remove(observers.begin(), observers.end(), f), observers.end()); }
};
class Person : public Observable<Person> {
int age, height;
std::string name, country, zip_code;
static const std::map<std::string, std::vector<std::string>> dependenciesMap; // Keeps a list of properties affected by a property.
public:
Person (const std::string& n, int a, const std::string& c, const std::string& z) : name(n), age(a), country(c), zip_code(z) { }
static const std::map<std::string, int(Person::*)() const> intMethodMap;
static const std::map<std::string, bool(Person::*)() const> boolMethodMap;
static const std::map<std::string, std::string(Person::*)() const> stringMethodMap;
using TupleMaps = std::tuple<decltype(intMethodMap), decltype(boolMethodMap), decltype(stringMethodMap)>;
static const TupleMaps methodMaps;
int get_age() const { return age; }
void set_age (int a) { set_field(&Person::age, "age", a); }
int get_height() const { return height; }
std::string get_name() const { return name; }
std::string get_country() const { return country; }
void set_country (const std::string& c) { set_field(&Person::country, "country", c); }
std::string get_zip_code() const { return zip_code; }
void set_zip_code (const std::string& z) { set_field(&Person::zip_code, "zip_code", z); }
bool can_vote() const { return age >= 16 && country == "USA"; }
bool can_quit_school() const { return age >= 15 && country == "USA" && zip_code == "90210"; }
private:
template <typename T> void set_field (T Person::*, const std::string& field_name, const T& value);
};
const std::map<std::string, std::vector<std::string>> Person::dependenciesMap = {
{"age", {"can_vote()", "can_quit_school()"}}, // age affects get_can_vote and can_quit_school.
{"country", {"can_vote()", "can_quit_school()"}}, // country affects can_vote and can_quit_school.
{"zip_code", {"can_quit_school()"}} // zip_code affects can_quit_school
};
const std::map<std::string, int(Person::*)() const> Person::intMethodMap = {
{"age", &Person::get_age}, {"height", &Person::get_height}
};
const std::map<std::string, bool(Person::*)() const> Person::boolMethodMap = {
{"can_vote()", &Person::can_vote}, {"can_quit_school()", &Person::can_quit_school}
};
const std::map<std::string, std::string(Person::*)() const> Person::stringMethodMap = {
{"name", &Person::get_name}, {"country", &Person::get_country}, {"zip_code", &Person::get_zip_code}
};
const Person::TupleMaps Person::methodMaps = std::make_tuple(Person::intMethodMap, Person::boolMethodMap, Person::stringMethodMap);
struct ConsolePersonObserver : Observer<Person> {
virtual void field_changed (Person& source, const std::string& field_name, OutputType output_type) override { field_changed_helper<FirstType>(source, field_name, output_type); }
private:
template <OutputType> void field_changed_helper (Person& source, const std::string& field_name, OutputType output_type);
};
template <OutputType N>
void ConsolePersonObserver::field_changed_helper (Person& source, const std::string& field_name, OutputType output_type) {
if (output_type == N)
std::cout << source.get_name() << "'s " << field_name << " has changed to " << (source.*std::get<N>(Person::methodMaps).at(field_name))() << ".\n";
else
field_changed_helper<static_cast<OutputType>(N+1)>(source, field_name, output_type);
}
template <>
void ConsolePersonObserver::field_changed_helper<NumOutputTypes> (Person&, const std::string&, OutputType) { } // End of recursion
template <typename T>
void Person::set_field (T Person::*ptr, const std::string& field_name, const T& value) {
if (value == this->*ptr)
return;
std::map<std::string, bool> trackedValues; // *** All the methods to be invoked through dependenciesMap return bool (if not, then this will be more complicated).
for (const std::string& str : dependenciesMap.at(field_name))
trackedValues[str] = (this->*boolMethodMap.at(str))();
this->*ptr = value;
notify_field_change<typename std::remove_reference<decltype(this->*ptr)>::type>(*this, field_name);
for (const std::pair<std::string, bool>& pair : trackedValues) {
if (pair.second != (this->*boolMethodMap.at(pair.first))())
notify_field_change<bool>(*this, pair.first);
}
}
int main() {
Person p("Tom", 14, "USA", "90210");
ConsolePersonObserver cpo;
p.subscribe(&cpo);
p.set_age(15);
p.set_age(16);
p.set_zip_code("999999");
p.set_country("France");
}
Output:
Tom's age has changed to 15.
Tom's can_quit_school() has changed to 1.
Tom's age has changed to 16.
Tom's can_vote() has changed to 1.
Tom's zip_code has changed to 999999.
Tom's can_quit_school() has changed to 0.
Tom's country has changed to France.
Tom's can_vote() has changed to 0.
Here is my extended code to handle the general case when the output types from the methods obtained by dependenciesMap
are different (I removed the template argument of notify_field_change
to make this work). For example, country
affects can_vote
, can_quit_school()
, years_left()
, and nickname
, which return bool, bool, int, and std::string respectively. But I have a feeling there is a cleaner way to handle this than my code below:
#include <iostream>
#include <vector>
#include <map>
#include <tuple>
enum OutputType { Int, Bool, String, NumOutputTypes, FirstOutputType = Int };
template <typename T> struct GetOutputType;
template <> struct GetOutputType<int> : std::integral_constant<OutputType, Int> { };
template <> struct GetOutputType<bool> : std::integral_constant<OutputType, Bool> { };
template <> struct GetOutputType<std::string> : std::integral_constant<OutputType, String> { };
template <typename T>
struct Observer {
virtual void field_changed (T&, const std::string& field_name, OutputType) = 0;
};
template <typename T>
class Observable {
std::vector<Observer<T>*> observers;
public:
void notify_field_change (T& source, OutputType output_type, const std::string& field_name) {
for (Observer<T>* obs : observers)
obs->field_changed(source, field_name, output_type);
}
void subscribe (Observer<T>* f) { observers.push_back(f); }
void unsubscribe (Observer<T>* f) { observers.erase(remove(observers.begin(), observers.end(), f), observers.end()); }
};
class Person : public Observable<Person> {
int age, height;
std::string name, country, zip_code;
static const std::map<std::string, std::vector<std::pair<std::string, OutputType>>> dependenciesMap; // Keeps a list of properties affected by a property.
public:
Person (const std::string& n, int a, const std::string& c, const std::string& z) : name(n), age(a), country(c), zip_code(z) { }
static const std::map<std::string, int(Person::*)() const> intMethodMap;
static const std::map<std::string, bool(Person::*)() const> boolMethodMap;
static const std::map<std::string, std::string(Person::*)() const> stringMethodMap;
using TupleMaps = std::tuple<decltype(intMethodMap), decltype(boolMethodMap), decltype(stringMethodMap)>;
using TupleMaps2 = std::tuple<std::map<std::string, int>, std::map<std::string, bool>, std::map<std::string, std::string>>;
static const TupleMaps methodMaps;
int get_age() const { return age; }
void set_age (int a) { set_field(&Person::age, "age", a); }
int get_height() const { return height; }
std::string get_name() const { return name; }
std::string nickname() const {
if (country != "France")
return name;
return "Francois";
}
std::string get_country() const { return country; }
void set_country (const std::string& c) { set_field(&Person::country, "country", c); }
std::string get_zip_code() const { return zip_code; }
void set_zip_code (const std::string& z) { set_field(&Person::zip_code, "zip_code", z); }
bool can_vote() const { return age >= 16 && country == "USA"; }
bool can_quit_school() const { return age >= 15 && country == "USA" && zip_code == "90210"; }
int years_left() const {
if (country == "France")
return 10;
return 80 - age;
}
private:
template <typename T> void set_field (T Person::*, const std::string& field_name, const T& value); // T Person::* is pointer to data member.
template <OutputType> void set_field_helper (TupleMaps2& trackedValuesAllTypes, const std::pair<std::string, OutputType>&);
template <OutputType> void notify_other_field_changes (const TupleMaps2&);
};
const std::map<std::string, std::vector<std::pair<std::string, OutputType>>> Person::dependenciesMap = {
{"age", {{"can_vote()", Bool}, {"can_quit_school()", Bool}, {"years_left()", Int}}}, // age affects can_vote(), can_quit_school(), and years_left().
{"country", {{"can_vote()", Bool}, {"can_quit_school()", Bool}, {"years_left()", Int}, {"nickname", String}}}, // country affects can_vote, can_quit_school(), years_left(), and nickname.
{"zip_code", {{"can_quit_school()", Bool}}} // zip_code affects can_quit_school
};
const std::map<std::string, int(Person::*)() const> Person::intMethodMap = {
{"age", &Person::get_age}, {"height", &Person::get_height}, {"years_left()", &Person::years_left}
};
const std::map<std::string, bool(Person::*)() const> Person::boolMethodMap = {
{"can_vote()", &Person::can_vote}, {"can_quit_school()", &Person::can_quit_school}
};
const std::map<std::string, std::string(Person::*)() const> Person::stringMethodMap = {
{"name", &Person::get_name}, {"country", &Person::get_country}, {"zip_code", &Person::get_zip_code}, {"nickname", &Person::nickname}
};
const Person::TupleMaps Person::methodMaps = std::make_tuple(Person::intMethodMap, Person::boolMethodMap, Person::stringMethodMap);
struct ConsolePersonObserver : Observer<Person> {
virtual void field_changed (Person& source, const std::string& field_name, OutputType output_type) override { field_changed_helper<FirstOutputType>(source, field_name, output_type); }
private:
template <OutputType> void field_changed_helper (Person& source, const std::string& field_name, OutputType output_type);
};
template <OutputType N>
void ConsolePersonObserver::field_changed_helper (Person& source, const std::string& field_name, OutputType output_type) {
if (output_type == N)
std::cout << source.get_name() << "'s " << field_name << " has changed to " << (source.*std::get<N>(Person::methodMaps).at(field_name))() << ".\n";
else
field_changed_helper<static_cast<OutputType>(N+1)>(source, field_name, output_type);
}
template <>
void ConsolePersonObserver::field_changed_helper<NumOutputTypes> (Person&, const std::string&, OutputType) { } // End of recursion
template <typename T>
void Person::set_field (T Person::*ptr, const std::string& field_name, const T& value) {
if (value == this->*ptr)
return;
TupleMaps2 trackedValuesAllTypes;
for (const std::pair<std::string, OutputType>& pair : dependenciesMap.at(field_name))
set_field_helper<FirstOutputType>(trackedValuesAllTypes, pair);
this->*ptr = value;
const OutputType type = GetOutputType<typename std::remove_reference<decltype(this->*ptr)>::type>::value;
notify_field_change(*this, type, field_name);
notify_other_field_changes<FirstOutputType>(trackedValuesAllTypes);
}
template <OutputType N>
void Person::set_field_helper (TupleMaps2& trackedValuesAllTypes, const std::pair<std::string, OutputType>& pair) {
if (pair.second == N)
std::get<N>(trackedValuesAllTypes)[pair.first] = (this->*std::get<N>(Person::methodMaps).at(pair.first))();
else
set_field_helper<static_cast<OutputType>(N+1)>(trackedValuesAllTypes, pair);
}
template <>
void Person::set_field_helper<NumOutputTypes> (TupleMaps2&, const std::pair<std::string, OutputType>&) { } // End of recursion.
template <OutputType N>
void Person::notify_other_field_changes (const TupleMaps2& trackedValuesAllTypes) {
for (const auto& pair : std::get<N>(trackedValuesAllTypes)) {
if (pair.second != (this->*std::get<N>(methodMaps).at(pair.first))())
notify_field_change(*this, N, pair.first);
}
notify_other_field_changes<static_cast<OutputType>(N+1)>(trackedValuesAllTypes);
}
template <>
void Person::notify_other_field_changes<NumOutputTypes> (const TupleMaps2&) { } // End of recursion.
int main() {
Person p("Tom", 14, "USA", "90210");
ConsolePersonObserver cpo;
p.subscribe(&cpo);
p.set_age(15);
p.set_age(16);
p.set_zip_code("999999");
p.set_country("France");
}
Output:
Tom's age has changed to 15.
Tom's years_left() has changed to 65.
Tom's can_quit_school() has changed to 1.
Tom's age has changed to 16.
Tom's years_left() has changed to 64.
Tom's can_vote() has changed to 1.
Tom's zip_code has changed to 999999.
Tom's can_quit_school() has changed to 0.
Tom's country has changed to France.
Tom's years_left() has changed to 10.
Tom's can_vote() has changed to 0.
Tom's nickname has changed to Francois.