We have started a series of posts trying to present ridiculous popular design patterns using examples in modern C++. Again, the inspiration came from the great youtube video series, ArjanCodes. This time we are going to talk about the observer pattern which is going to help us separate various modules in our code.
The key roles in the pattern are the subjects, which is the action that we want to perform and once happen we notify the observer. It sounds vague, but an example will help to understand better the mechanism.
Spoiler Alert: This is not a detailed technical text. It’s just an example that tries using layman’s terms to explain a dry topic. If you are very serious about that, please try a relevant technical book instead first.
Let’s imagine the following scenario. We build a user management system. The user can register, renew their password if forgotten) and upgrade their plan from the free tier to premium (subscription). To support that, we have a database, slack for corporate communication, email for customer communication and a logger to maintain the system log.
We will begin by presenting the non optimised version.
int main() { Database users_db{}; UserManagement plain_user_system{users_db}; PlanManagement plain_plan_system{users_db}; plain_user_system.RegisterNewUser("angelos", "1234", "angelos@in.gr"); plain_user_system.PasswordForgotten("angelos@in.gr"); plain_plan_system.UpgradePlan("angelos@in.gr"); return 0; }
In this case, we initiate the database instance and we have two APIs responsible for the user and the plan management. Let’s see how they look like:
class UserManagement { public: explicit UserManagement(Database &user_database) : database_{user_database} {} void RegisterNewUser(std::string_view name, std::string_view password, std::string_view email) { User a_user = database_.Create_User(name, password, email); PostSlackMessage("sales", name.data() + std::string(" has registered with email ") + email.data()); Log(std::string("User registered with email address ") + email.data()); SendEmail(a_user.name_, a_user.email_, "Welcome", "Thanks for registering, " + a_user.name_); } void PasswordForgotten(std::string_view email) const { const auto found_the_user = database_.FindUser(email); if (found_the_user.has_value()) { User the_user = found_the_user.value(); the_user.reset_code_ = "11111"; SendEmail(the_user.name_, the_user.email_, "Reset your password", "To reset your password, use this very secure code:" + the_user.reset_code_); Log("User with email address " + the_user.email_ + " requested a password reset"); } } private: Database &database_; };
and
class PlanManagement { public: explicit PlanManagement(Database &user_database) : database_{user_database} {} void UpgradePlan(std::string_view email) const { const auto found_the_user = database_.FindUser(email); if (found_the_user.has_value()) { User the_user = found_the_user.value(); the_user.plan_ = "paid"; PostSlackMessage("sales", the_user.name_ + " has upgraded their plan."); SendEmail(the_user.name_, the_user.email_, "Thank you", "Thanks for upgrading, You're gonna love it."); Log("User with email address " + the_user.email_ + " has upgraded their plan"); } } private: Database &database_; };
In both APIs, we see that apart from the first task in every method which is the desired action, e.g. in the RegisterNewUser method, the task of creating the new user in the database, everything else is not strongly related, e.g. send a slack message, send email etc. This is the code smell of weak cohesion. The method needs to know and depend on many different things. The situation is similar with the PasswordForgotten and the UpgradePlan.
We also notice that in every API we import external functionality regarding Slack, database, email and logger which in real life represent services. For the shake of this example are simple scripts like:
class Database { public: Database() = default; User Create_User(std::string_view name, std::string_view pasword, std::string_view email) { User new_user{name, pasword, email}; users_.push_back(new_user); return new_user; } [[nodiscard]] std::optional<User> FindUser(std::string_view email) const { for (auto user : users_) { if (user.email_ == email) return user; } return std::nullopt; } std::vector<User> users_; };
class User { public: User(std::string_view name, std::string_view password, std::string_view email) : name_{name} , password_{password} , email_{email} , plan_{"basic"} {} void ResetPassword(std::string_view code, std::string_view new_password) { if (code == reset_code_) { password_ = new_password; } else { std::cout << "Invalid password reset code." << std::endl; } } void PrintUser() const { std::cout << "[" << name_ << ", " << email_ << "]" << std::endl; } std::string name_; std::string password_; std::string email_; std::string plan_{"basic"}; std::string reset_code_; };
void PostSlackMessage(std::string_view channel, std::string_view message) { spdlog::info("[SlackBot {}]: {}", channel, message); } #endif//ARJAN_OBSERVER_PATTERN_LIB_SLACK_H_
void SendEmail(std::string_view name, std::string_view address, std::string_view subject, std::string_view body) { std::cout << "Sending email to " << name << " (" << address << ")" << std::endl; std::cout << "-----" << std::endl; std::cout << "Subject: " << subject << std::endl; std::cout << body << std::endl; }
void Log(std::string_view message) { spdlog::info(std::string("[Log]: ") + message.data()); }
So far, we have understood the situation, address the weaknesses and we need to identify how we can improve it, by applying the observer pattern (or another simplistic implementation of that to understand how it works). In this case, we will use events instead of direct calling with the libraries.
We will start by creating our own event system manager, which looks like:
class EventSystem { public: void Subscribe(std::string_view event_type, const std::function<void(const User &)> &function) { subscribers_[event_type.data()].push_back(function); } void PostEvent(std::string_view event_type, const User &a_user) { for (const auto &fn : subscribers_[event_type.data()]) { fn(a_user); } } [[maybe_unused]] void ListSubscribers() { for (const auto &[event, fun] : subscribers_) spdlog::info("EventSystem type: " + event + " subscribers " + std::to_string(subscribers_[event].size())); } private: std::map<std::string, std::vector<std::function<void(const User &)>>> subscribers_; };
The heart of the event system is a dictionary that relates subscribers with events. When an event happens the subscribers get notified. So the class contains a method to subscribe subscribers to a specific event and a method to post an event which is the notification of the subscriber that the specific event took place. The events are actually tags (strings) like “user registered” and the subscribers are functions.
Having done that our User and Plan management APIs will change to:
class UserManagementEventDriven { public: explicit UserManagementEventDriven(Database &user_database, EventSystem &event_system) : database_{user_database}, event_system_{event_system} {} void RegisterNewUser(std::string_view name, std::string_view password, std::string_view email) { User a_user = database_.Create_User(name, password, email); event_system_.PostEvent("user_registered", a_user); } void PasswordForgotten(std::string_view email) const { const auto found_the_user = database_.FindUser(email); if (found_the_user.has_value()) { User the_user = found_the_user.value(); the_user.reset_code_ = "11111"; event_system_.PostEvent("user_password_forgotten", the_user); } } private: Database &database_; EventSystem &event_system_; };
class PlanManagementEventDriven { public: explicit PlanManagementEventDriven(Database &user_database, EventSystem &event_system) : database_{user_database} , event_system_{event_system} {} void UpgradePlan(std::string_view email) const { const auto found_the_user = database_.FindUser(email); if (found_the_user.has_value()) { User the_user = found_the_user.value(); the_user.plan_ = "paid"; event_system_.PostEvent("user_upgrade_plan", the_user); } } private: Database &database_; EventSystem &event_system_; };
No more coupling, no more dependency on everything. Whenever something happens we post the relevant event. There are no dependencies with the library interfaces. But, there is not over yet. We need to create the event handlers. For example, we need to develop the Slack listener which looks like:
void HandleUserRegisteredEvent(const User &user) { PostSlackMessage("sales", user.name_ + " has registered with email address " + user.email_); } void HandleUserPasswordForgottenEvent(const User &user) { PostSlackMessage("sales", user.name_ + " has upgraded their plan."); } void HandleUserUpgradePlanEvent(const User &user) { PostSlackMessage("sales", user.name_ + " has forgotten their password."); } void SetupSlackEventHandlers(EventSystem &events_system) { events_system.Subscribe("user_registered", HandleUserRegisteredEvent); events_system.Subscribe("user_password_forgotten", HandleUserPasswordForgottenEvent); events_system.Subscribe("user_upgrade_plan", HandleUserUpgradePlanEvent); }
The important point is that with the SetupSlackEventHandlers method we set up the subscribers (methods) with the specific events. Similarly, we can implement the emails event handlers, exactly the same story.
void HandleUserRegisteredEventEmail(const User &user) { SendEmail(user.name_, user.email_, "Welcome", "Thanks for registering, " + user.name_ + "!"); } void HandleUserPasswordForgottenEventEmail(const User &user) { SendEmail(user.name_, user.email_, "Reset your password", "To reset your password, use this very secure code: " + user.reset_code_ + "."); } void HandleUserUpgradePlanEventEmail(const User &user) { SendEmail(user.name_, user.email_, "Thank you", "Thanks for upgrading, " + user.name_ + "! You're gonna love it."); } void SetupEmailEventHandlers(EventSystem &events_system) { events_system.Subscribe("user_registered", HandleUserRegisteredEventEmail); events_system.Subscribe("user_password_forgotten", HandleUserPasswordForgottenEventEmail); events_system.Subscribe("user_upgrade_plan", HandleUserUpgradePlanEventEmail); }
If we put everything together, the final picture will look like this:
int main() { EventSystem events_system{}; SetupSlackEventHandlers(events_system); SetupEmailEventHandlers(events_system); SetupLogEventHandlers(events_system); Database users_db{}; UserManagementEventDriven users_management_system{users_db, events_system}; PlanManagementEventDriven plans_management_system{users_db, events_system}; users_management_system.RegisterNewUser("angelos", "1234", "angelos@in.gr"); users_management_system.PasswordForgotten("angelos@in.gr"); plans_management_system.UpgradePlan("angelos@in.gr"); return 0; }
We create an event system, we set up the event handlers and then our User and Plan management systems can do their magic. Easy, huh?
If you want to understand it better, my advice is to watch ArjanCode’s video here and go through my GitHub code repository line by line.
Enjoy…