Шаблон «Наблюдатель» на современном C++
Мы начинаем серию постов, пытаясь представить популярные шаблоны проектирования на примерах современного C++. На этот раз мы поговорим о шаблоне наблюдателя, который поможет нам разделить различные модули в нашем коде.
Ключевыми ролями в этом шаблоне обладают субъекты, то есть действия, которые мы хотим выполнить, и когда они происходят, мы уведомляем наблюдателя. Звучит расплывчато, но пример поможет лучше понять механизм.
Внимание, спойлер: это не подробный технический текст. Это просто пример, который пытается на непрофессиональном уровне объяснить сухую тему. Если вы очень серьезно относитесь к этому, то сначала попробуйте почитать соответствующую книгу.
Давайте представим следующий сценарий. Мы создаем систему управления пользователями. Пользователь может зарегистрироваться, сбросить свой пароль (если забыл его) и обновить свой тарифный план с бесплатного уровня до премиума. Для этого у нас есть база данных, slack для корпоративного общения, электронная почта для общения с клиентами и логгер для ведения системного журнала.
Начнем с представления неоптимизированной версии.
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;
}
В этом случае мы инициализируем экземпляр базы данных, и у нас есть два API, отвечающих за пользователя и управление тарифным планом. Давайте посмотрим, как они выглядят:
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_;
};
и
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_;
};
В обоих API мы видим, что помимо основной задачи в каждом методе, например, в методе RegisterNewUser
задача создания нового пользователя в базе данных, всё остальное с ней не сильно связано, т.е. отправка сообщения в slack, отправка письма по электронной почте и т. д. Этот код попахивает слабой связностью. Метод должен знать и зависит от множества вещей. Аналогичная ситуация с PasswordForgotten
и UpgradePlan
.
Мы также замечаем, что в каждый API мы импортируем внешние функции, касающиеся Slack, базы данных, электронной почты и логгера, которые в реальной жизни представляют собой сервисы. Для проверки этого примера используются простые скрипты типа:
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());
}
На данный момент мы поняли ситуацию, устранили слабые места и нам нужно определить, как мы можем ее улучшить, применив шаблон наблюдатель (или другую его упрощенную реализацию, чтобы понять, как он работает). В этом случае мы будем использовать события вместо прямого обращения к библиотекам.
Начнем с создания нашего собственного менеджера системы событий, который выглядит так:
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_;
};
Сердцем системы событий является словарь, который связывает подписчиков с событиями. Когда происходит событие, подписчики получают уведомление. Таким образом, класс содержит метод для подписки подписчиков на определенное событие и метод для публикации события, которое является уведомлением подписчика о том, что конкретное событие произошло. События на самом деле представляют собой теги (строки), такие как «user_registered» (пользователь зарегистрирован), а подписчики – это функции.
После этого наши API управления пользователями и планами изменятся на:
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_;
};
Больше нет связанности, больше нет зависимости от всего. Всякий раз, когда что-то происходит, мы публикуем соответствующее событие. Нет никаких зависимостей с интерфейсами библиотеки. Но это еще не конец. Нам нужно создать обработчики событий. Например, нам нужно разработать слушатель Slack, который выглядит так:
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);
}
Важным моментом здесь является то, что с помощью метода SetupSlackEventHandlers
мы сопоставляем подписчиков (методы) с конкретными событиями. Точно так же мы можем реализовать обработчики событий для электронной почты, всё аналогично:
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);
}
Если собрать всё вместе, итоговая картина будет выглядеть так:
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;
}
Создаем систему событий, настраиваем обработчиков событий, а затем наши системы управления пользователями и тарифными планами могут творить чудеса. Легко ведь?