To podejście łączy elastyczność Observera z polimorficzną hierarchią zdarzeń. Zamiast wpisywać typ zdarzenia w sygnaturę metody, tworzysz klasę bazową Event i wyprowadzasz z niej konkretne typy. Jeden mechanizm subskrypcji obsługuje wtedy wiele typów zdarzeń, każdy z własnymi danymi, a bezpieczeństwo typów zostaje zachowane.
Czym to się różni od podejścia tradycyjnego
Tradycyjny Observer.
class Observer {
virtual void on_value_changed(int value) = 0;
virtual void on_state_changed(State state) = 0;
virtual void on_error_occurred(std::string error) = 0;
};
// Każdy obserwator musi implementować wszystkie trzy metody,
// nawet jeśli dba tylko o jeden typ zdarzenia.
System oparty na zdarzeniach.
class EventListener {
virtual void on_event(const Event& event) = 0;
};
class ValueChangedEvent : public Event { /* ... */ };
class StateChangedEvent : public Event { /* ... */ };
class ErrorOccurredEvent : public Event { /* ... */ };
// Obserwator otrzymuje wszystkie zdarzenia; używa RTTI lub visitora do wysłania
listener->on_event(ValueChangedEvent{10});
listener->on_event(StateChangedEvent{State::READY});
Korzyść. Nowe typy zdarzeń dodajesz bez zmiany interfejsu obserwatora. Obserwator może obsługiwać tylko te zdarzenia, które go interesują.
Kod
#include <iostream>
#include <vector>
#include <memory>
#include <typeinfo>
#include <string>
#include <sstream>
// ── Klasa bazowa Event ─────────────────────────────────────
class Event {
public:
virtual ~Event() = default;
virtual std::string type_name() const = 0;
virtual void describe() const = 0;
};
// ── Konkretne typy Event ───────────────────────────────────
class ValueChangedEvent : public Event {
public:
int old_value;
int new_value;
ValueChangedEvent(int old_val, int new_val)
: old_value(old_val), new_value(new_val) {}
std::string type_name() const override {
return "ValueChangedEvent";
}
void describe() const override {
std::cout << " [Event] Wartość zmieniona: " << old_value
<< " → " << new_value << "\n";
}
};
class StateChangedEvent : public Event {
public:
enum class State { IDLE, LOADING, READY, ERROR };
State old_state;
State new_state;
StateChangedEvent(State old, State new_st)
: old_state(old), new_state(new_st) {}
std::string type_name() const override {
return "StateChangedEvent";
}
void describe() const override {
auto state_name = [](State s) {
switch(s) {
case State::IDLE: return "IDLE";
case State::LOADING: return "LOADING";
case State::READY: return "READY";
case State::ERROR: return "ERROR";
}
return "?";
};
std::cout << " [Event] Stan zmieniony: " << state_name(old_state)
<< " → " << state_name(new_state) << "\n";
}
};
class ErrorOccurredEvent : public Event {
public:
std::string message;
explicit ErrorOccurredEvent(const std::string& msg)
: message(msg) {}
std::string type_name() const override {
return "ErrorOccurredEvent";
}
void describe() const override {
std::cout << " [Event] Błąd: " << message << "\n";
}
};
// ── Interfejs Event Listener ───────────────────────────────
class EventListener {
public:
virtual ~EventListener() = default;
virtual void on_event(const Event& event) = 0;
};
// ── Konkretni Listener'y ───────────────────────────────────
// Listener, któremu zależy na WSZYSTKICH zdarzeniach
class LoggingListener : public EventListener {
public:
void on_event(const Event& event) override {
std::cout << "[Logger] Odebrano " << event.type_name() << ":\n";
event.describe();
}
};
// Listener, któremu zależy tylko na zmianie wartości
class DisplayListener : public EventListener {
public:
void on_event(const Event& event) override {
// Podejście RTTI: sprawdzenie typu w runtime
if (const auto* value_event = dynamic_cast<const ValueChangedEvent*>(&event)) {
std::cout << "[Display] Nowa wartość: " << value_event->new_value << "\n";
}
// Ignoruj inne zdarzenia
}
};
// Listener, któremu zależy tylko na błędach
class AlertListener : public EventListener {
public:
void on_event(const Event& event) override {
if (const auto* error_event = dynamic_cast<const ErrorOccurredEvent*>(&event)) {
std::cout << "[Alert] " << error_event->message << "\n";
}
}
};
// Listener z selektywnym zainteresowaniem
class StateMonitor : public EventListener {
private:
int value_changes_count_ = 0;
int errors_count_ = 0;
public:
void on_event(const Event& event) override {
if (const auto* value_event = dynamic_cast<const ValueChangedEvent*>(&event)) {
value_changes_count_++;
} else if (const auto* error_event = dynamic_cast<const ErrorOccurredEvent*>(&event)) {
errors_count_++;
std::cout << "[Monitor] Błąd #" << errors_count_ << ": "
<< error_event->message << "\n";
}
}
void report() const {
std::cout << "\n[Monitor] Podsumowanie:\n";
std::cout << " - Zmiany wartości: " << value_changes_count_ << "\n";
std::cout << " - Błędy: " << errors_count_ << "\n";
}
};
// ── Event Emitter ──────────────────────────────────────────
class EventSystem {
private:
std::vector<std::unique_ptr<EventListener>> listeners_;
public:
void subscribe(std::unique_ptr<EventListener> listener) {
listeners_.push_back(std::move(listener));
}
void emit(const Event& event) {
for (auto& listener : listeners_) {
listener->on_event(event);
}
}
size_t listener_count() const { return listeners_.size(); }
};
// ── Subject, który emituje zdarzenia ───────────────────────
class DataModel {
private:
int value_ = 0;
bool is_loaded_ = false;
EventSystem& event_system_;
public:
explicit DataModel(EventSystem& events)
: event_system_(events) {}
void set_value(int new_value) {
if (value_ != new_value) {
ValueChangedEvent event(value_, new_value);
event_system_.emit(event);
value_ = new_value;
}
}
void load() {
event_system_.emit(StateChangedEvent(
StateChangedEvent::State::IDLE,
StateChangedEvent::State::LOADING
));
// Symuluj ładowanie...
if (true) { // Sukces
event_system_.emit(StateChangedEvent(
StateChangedEvent::State::LOADING,
StateChangedEvent::State::READY
));
is_loaded_ = true;
} else { // Porażka
event_system_.emit(StateChangedEvent(
StateChangedEvent::State::LOADING,
StateChangedEvent::State::ERROR
));
event_system_.emit(ErrorOccurredEvent("Ładowanie nie powiodło się"));
}
}
};
// ── Użycie ────────────────────────────────────────────────
int main() {
EventSystem events;
// Subskrybuj różnych listener'ów
events.subscribe(std::make_unique<LoggingListener>());
events.subscribe(std::make_unique<DisplayListener>());
events.subscribe(std::make_unique<AlertListener>());
auto* monitor = new StateMonitor();
events.subscribe(std::unique_ptr<EventListener>(monitor));
std::cout << "Połączono " << events.listener_count() << " listener'ów\n\n";
DataModel model(events);
std::cout << "Ustawianie wartości na 42:\n";
model.set_value(42);
std::cout << "\nŁadowanie danych:\n";
model.load();
std::cout << "\nUstawianie wartości na 100:\n";
model.set_value(100);
std::cout << "\nWyzwalanie błędu:\n";
events.emit(ErrorOccurredEvent("Połączenie z bazą danych nie powiodło się"));
monitor->report();
return 0;
}
Wyjście
Połączono 4 listener'ów
Ustawianie wartości na 42:
[Logger] Odebrano ValueChangedEvent:
[Event] Wartość zmieniona: 0 → 42
[Display] Nowa wartość: 42
Ładowanie danych:
[Logger] Odebrano StateChangedEvent:
[Event] Stan zmieniony: IDLE → LOADING
[Logger] Odebrano StateChangedEvent:
[Event] Stan zmieniony: LOADING → READY
[Display] Nowa wartość: 42
Ustawianie wartości na 100:
[Logger] Odebrano ValueChangedEvent:
[Event] Wartość zmieniona: 42 → 100
[Display] Nowa wartość: 100
Wyzwalanie błędu:
[Logger] Odebrano ErrorOccurredEvent:
[Event] Błąd: Połączenie z bazą danych nie powiodło się
[Alert] Połączenie z bazą danych nie powiodło się
[Monitor] Błąd #1: Połączenie z bazą danych nie powiodło się
[Monitor] Podsumowanie:
- Zmiany wartości: 2
- Błędy: 1
Warianty projektowe
1. Wzorzec Visitor do wysyłania zdarzeń
Zamiast dynamic_cast możesz użyć wzorca Visitor, żeby wysyłanie było bezpieczne typowo:
class EventVisitor {
public:
virtual ~EventVisitor() = default;
virtual void visit(ValueChangedEvent&) = 0;
virtual void visit(StateChangedEvent&) = 0;
virtual void visit(ErrorOccurredEvent&) = 0;
};
class Event {
public:
virtual void accept(EventVisitor& visitor) = 0;
};
class ValueChangedEvent : public Event {
public:
void accept(EventVisitor& v) override { v.visit(*this); }
// ...
};
// W listener:
class DisplayListener : public EventListener, public EventVisitor {
public:
void on_event(const Event& event) override {
const_cast<Event&>(event).accept(*this);
}
void visit(ValueChangedEvent& e) override {
std::cout << "[Display] " << e.new_value << "\n";
}
void visit(StateChangedEvent&) override { /* ignoruj */ }
void visit(ErrorOccurredEvent&) override { /* ignoruj */ }
};
2. Zdarzenia typowane szablonami
Dla pełnego bezpieczeństwa typów bez RTTI:
template<typename EventType>
class TypedEventSystem {
private:
std::vector<std::function<void(const EventType&)>> callbacks_;
public:
void subscribe(std::function<void(const EventType&)> cb) {
callbacks_.push_back(cb);
}
void emit(const EventType& event) {
for (auto& cb : callbacks_) {
cb(event);
}
}
};
// Użycie:
TypedEventSystem<ValueChangedEvent> value_system;
value_system.subscribe([](const ValueChangedEvent& e) {
std::cout << "Wartość: " << e.new_value << "\n";
});
Zalety
- Hierarchia typów. Zdarzenia tworzą drzewo dziedziczenia.
- Selektywna obsługa. Listener zajmuje się tylko wybranymi zdarzeniami.
- Rozszerzalność. Nowy typ zdarzenia nie wymaga zmiany interfejsu listenera.
- Czytelne nazwy. Nazwa zdarzenia mówi, co się stało (
ValueChangedEvent, a nieupdate()). - Wiele typów zdarzeń. Jeden mechanizm subskrypcji obsługuje różne powiadomienia.
Wady
- Narzut RTTI.
dynamic_castkosztuje w czasie działania, a Visitor dodaje złożoność. - Łańcuch funkcji wirtualnych.
Event::acceptilistener::visitto kolejny poziom pośredniości. - Czas życia. Nadal potrzebujesz inteligentnych wskaźników (patrz następna strona).
- Więcej kodu. Dla każdego typu powiadomienia powstaje osobna klasa zdarzenia.
- Słabsze bezpieczeństwo typów.
dynamic_castomija sprawdzanie w kompilacji, choć istnieją bezpieczne alternatywy.
Kiedy stosować
- Złożone systemy z wieloma typami zdarzeń.
- Listenery interesują się tylko częścią zdarzeń.
- Systemy, w których typy zdarzeń często się zmieniają.
- Potrzebujesz nazw zdarzeń, które same mówią, co się stało.
- Integrujesz się z systemami, które oczekują hierarchii zdarzeń.
Porównanie
| Aspekt | Tradycyjny | Oparte na funkcjach | Qt Signals | System zdarzeń |
|---|---|---|---|---|
| Typy zdarzeń | Jeden na sygnał | Niejawny w sygnaturze | Jeden na sygnał | Hierarchia polimorficzna |
| Zakres listenera | Musi obsłużyć wszystkie | Osobno dla callbacku | Wszystkie sloty dostają zdarzenie | Selektywna obsługa |
| Dodawanie zdarzeń | Zmiana interfejsu | Dodanie sygnału | Zmiana interfejsu | Dziedziczenie po Event |
| Type safety | Kompilacja | Stracone | Kompilacja | Czas działania (RTTI) |
| Złożoność | Niska | Średnia | Średnia | Wysoka |
Następne kroki
- Zarządzanie czasem życia. Zarządzanie czasem życia i zapobieganie wiszącym wskaźnikom.