Event Bus и паттерн Observer в Rust: строим гибкую событийную архитектуру
В современной разработке программного обеспечения часто возникает необходимость в слабой связности компонентов. Паттерн Observer и его практическая реализация — Event Bus (шина событий) — позволяют организовать эффективное взаимодействие между модулями без прямых зависимостей. Rust, с его строгой системой типов и акцентом на безопасность, предоставляет отличные возможности для реализации этого паттерна. В этой статье мы разберём, как построить собственный Event Bus в Rust, используя встроенные возможности языка и популярные крейты.
Что такое Event Bus и паттерн Observer?
Паттерн Observer определяет отношение "один ко многим" между объектами: когда один объект (издатель) меняет своё состояние, все зависимые объекты (подписчики) уведомляются об этом автоматически. Event Bus — это централизованная шина, которая управляет подписками и рассылкой событий, позволяя любому компоненту публиковать события и подписываться на них.
Основные преимущества:
- Слабая связность: издатели и подписчики не знают друг о друге напрямую
- Масштабируемость: легко добавлять новых подписчиков без изменения существующего кода
- Тестируемость: можно изолированно тестировать компоненты, эмулируя события
- Асинхронность: события могут обрабатываться в фоновом режиме
Базовая реализация Event Bus на Rust
Начнём с простой синхронной реализации, используя стандартные средства Rust. В основе будет лежать HashMap, где ключом выступает тип события, а значением — список обработчиков.
use std::collections::HashMap;
use std::any::{Any, TypeId};
use std::marker::PhantomData;
// Тип события с уникальным идентификатором
pub trait Event: Any + Clone + Send + 'static {
fn event_type(&self) -> TypeId {
TypeId::of::<Self>()
}
}
// Обработчик события
pub trait EventHandler<E: Event>: Send + 'static {
fn handle(&self, event: &E);
}
// Сама шина событий
pub struct EventBus {
handlers: HashMap<TypeId, Vec<Box<dyn Any>>>,
}
impl EventBus {
pub fn new() -> Self {
EventBus {
handlers: HashMap::new(),
}
}
pub fn subscribe<E: Event, H: EventHandler<E>>(&mut self, handler: H) {
let type_id = TypeId::of::<E>();
let boxed_handler = Box::new(handler) as Box<dyn Any>;
self.handlers.entry(type_id).or_insert(Vec::new()).push(boxed_handler);
}
pub fn publish<E: Event>(&self, event: &E) {
let type_id = event.event_type();
if let Some(handlers) = self.handlers.get(&type_id) {
for handler in handlers {
if let Some(typed_handler) = handler.downcast_ref::<Box<dyn EventHandler<E>>>() {
typed_handler.handle(event);
}
}
}
}
}
Пример использования
// Определяем события
#[derive(Clone)]
struct UserLoggedIn {
user_id: u32,
username: String,
}
impl Event for UserLoggedIn {}
#[derive(Clone)]
struct DataUpdated {
table: String,
record_id: u64,
}
impl Event for DataUpdated {}
// Определяем обработчики
struct Logger;
impl EventHandler<UserLoggedIn> for Logger {
fn handle(&self, event: &UserLoggedIn) {
println!("[LOG] User {} (ID: {}) logged in", event.username, event.user_id);
}
}
struct EmailNotifier;
impl EventHandler<UserLoggedIn> for EmailNotifier {
fn handle(&self, event: &UserLoggedIn) {
println!("[EMAIL] Sending welcome email to {}", event.username);
}
}
fn main() {
let mut bus = EventBus::new();
bus.subscribe::<UserLoggedIn, Logger>(Logger);
bus.subscribe::<UserLoggedIn, EmailNotifier>(EmailNotifier);
let event = UserLoggedIn { user_id: 42, username: "alice".to_string() };
bus.publish(&event);
// Вывод:
// [LOG] User alice (ID: 42) logged in
// [EMAIL] Sending welcome email to alice
}
Асинхронная реализация с tokio
Для реальных приложений часто требуется асинхронная обработка событий. Используем крейт tokio и каналы broadcast:
use tokio::sync::broadcast;
use std::fmt::Debug;
#[derive(Clone, Debug)]
pub enum AppEvent {
UserAction { user_id: u32, action: String },
System