diff --git a/CMakeLists.txt b/CMakeLists.txt index b5c1be4..e0849e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,9 @@ set(APP_SOURCES "src/patterns/behavioral/Memento.cpp" "src/patterns/behavioral/Visitor.cpp" "src/patterns/behavioral/TemplateMethod.cpp" + "src/patterns/behavioral/Strategy.cpp" + "src/patterns/behavioral/State.cpp" + "src/patterns/behavioral/Observer.cpp" ) # Test files diff --git a/docs/uml/patterns_behavioral_observer.drawio.svg b/docs/uml/patterns_behavioral_observer.drawio.svg new file mode 100644 index 0000000..b8851c6 --- /dev/null +++ b/docs/uml/patterns_behavioral_observer.drawio.svg @@ -0,0 +1,4 @@ + + + +
main:
{
Subject* btn = new Subject();
IObserver* listener_1 = new ConcreteObserverA();
IObserver* listener_2 = new ConcreteObserverA();
IObserver* listener_3 = new ConcreteObserverB();
btn->attach(listener_1);
btn->attach(listener_2);
btn->attach(listener_3);
clientCode(btn );
}

clientCode:
{
 s->notify();
}
Client
+ clientCode(Subject* s): type
Subject
- m_observers: list<IObserver*>
+ attachObserver(IObserver* obs): Type
+ detachObserver(IObserver* obs): Type
+ notifyObservers(Type): Type

<<Interface>>
IObserver


+ update(Type): Type

ConcreteObserverA
+ field: type
+ update(Type): Type
ConcreteObserverB
+ field: type
+ update(Type): Type
attach:
{
 m_observers.push_back(obs);
}

detach:
{
 m_observers.remove(obs);
}

notify:
{
 for (IObserver o : m_observers){
   o->update();
 }
}
0..*
\ No newline at end of file diff --git a/docs/uml/patterns_behavioral_state.drawio.svg b/docs/uml/patterns_behavioral_state.drawio.svg new file mode 100644 index 0000000..5120dda --- /dev/null +++ b/docs/uml/patterns_behavioral_state.drawio.svg @@ -0,0 +1,4 @@ + + + +
main:
{
Context* ctx = new Context();
ctx->changeState(new ConcreteStateA());
clientCode(ctx);
}

clientCode:
{
 for(;;) // loop()
 ctx->operation();
}
Client
+ clientCode(Context* ctx): type
Context
+ field: type
- m_state: IState* 
+ operation(Type): Type
+ changeState(IState* state): Type

<<Interface>>
IState


+ setContext(Context* ctx): Type
+ handle(type): Type

1
operation:
{
 this->m_state->handle(type)
}
changeState:
{
 if(this->m_state != nullptr)
  delete this->m_state;
 this->m_state = state;
 this->m_state->setContext(this);
}

<<Abstract>>
AbstractState


+ m_context: Context*


+ setContext(Context* ctx): Type


ConcreteStateA
+ handle(type): Type
ConcreteStateB
+ handle(type): Type
ConcreteStateC
+ handle(type): Type
Extends
Extends
Extends
1
setContext:
{
 this->m_context = ctx;
}
handle-stateA:
{
 // do some logics
 // go to the next state 
 this->m_context->changeState(new ConcreteStateB());
}
\ No newline at end of file diff --git a/docs/uml/patterns_behavioral_strategy.drawio.svg b/docs/uml/patterns_behavioral_strategy.drawio.svg new file mode 100644 index 0000000..059a90d --- /dev/null +++ b/docs/uml/patterns_behavioral_strategy.drawio.svg @@ -0,0 +1,4 @@ + + + +
main:
{
Context* ctx = new Context();
ctx->setStrategy(new ConcreteStrategyA());
clientCode(ctx);
ctx->setStrategy(new ConcreteStrategyB());
clientCode(ctx);
}

clientCode:
{
 ctx->operation();
}
Client
+ clientCode(Context* ctx): type
Context
+ field: type
- m_strategy : IStrategy* 
+ operation(type): type
+ setStrategy(IStrategy): type

<<Interface>>
IStrategy


+ execute(Type): Type

ConcreteStrategy1
+ field: type
+ execute(Type): Type
ConcreteStrategy2
+ field: type
+ execute(Type): Type
1
operation:
{
 this->m_strategy->execute(type)
}
\ No newline at end of file diff --git a/docs/uml/pattern_behavioral_templatemethod.drawio.svg b/docs/uml/patterns_behavioral_templatemethod.drawio.svg similarity index 100% rename from docs/uml/pattern_behavioral_templatemethod.drawio.svg rename to docs/uml/patterns_behavioral_templatemethod.drawio.svg diff --git a/src/patterns/behavioral/Observer.cpp b/src/patterns/behavioral/Observer.cpp new file mode 100644 index 0000000..80bbf5e --- /dev/null +++ b/src/patterns/behavioral/Observer.cpp @@ -0,0 +1,204 @@ +// Observer is a behavioral design pattern that lets you define a subscription mechanism +// to notify multiple objects about any events that happen to the object they’re observing. +// Usage examples: The most popular usage of the Observer pattern in C++ code is facilitating communications between GUI components of an app. +// The synonym of the Observer is the `Controller` part of MVC pattern. +// Appicability: +// (*) when changes to the state of one object may require changing other objects, and the actual set of objects is unknown beforehand or changes dynamically. +// (**) when some objects in your app must observe others, but only for a limited time or in specific cases. + +// UML: docs/uml/patterns_behavioral_observer.drawio.svg + +#include +#include +#include + +namespace +{ + namespace Observer + { + enum class Event + { + CREATE = 0, + READ, + UPDATE, + DELETE, + }; + + static inline const char *getEventName(const Event &e) + { + switch (e) + { + case Event::CREATE: + return "CREATE"; + case Event::READ: + return "READ"; + case Event::UPDATE: + return "UPDATE"; + case Event::DELETE: + return "DELETE"; + } + return "invalid_event"; + } + + /** + * IObserver aka Subscriber + * The Subscriber interface declares the notification interface. + * In most cases, it consists of a single update method. + * The method may have several parameters that let the publisher pass some event details along with the update. + * E.g. Event Listen to UI events + */ + class IListenerObserver + { + public: + virtual ~IListenerObserver() = default; + + // update + virtual void update(const Event e) = 0; + }; + + /** + * Subject aka Publisher + * The Publisher issues events of interest to other objects. + * These events occur when the publisher changes its state or executes some behaviors. + * Publishers contain a subscription infrastructure that lets new subscribers join and current subscribers leave the list. + * + * E.g Widget dispatches click events to observers + */ + class IWidgetSubject + { + public: + virtual ~IWidgetSubject() {}; + // addListener + virtual void attach(IListenerObserver *observer) = 0; + // removeLister + virtual void detach(IListenerObserver *observer) = 0; + // e.g.click + virtual void notify(const Event &e) = 0; + }; + + class ButtonConcreteSubject : public IWidgetSubject + { + private: + std::list m_listeners; + + public: + void attach(IListenerObserver *observer) override + { + m_listeners.push_back(observer); + } + + void detach(IListenerObserver *observer) override + { + m_listeners.remove(observer); + } + + void notify(const Event &e) override + { + std::cout << "[Subject] notify event-" << getEventName(e) << "\n"; + for (IListenerObserver *o : m_listeners) + { + o->update(e); + } + } + }; + + class AbstractListenerObserver : public IListenerObserver + { + private: + int m_num; + inline static int num_observers = 0; + + protected: + void log(const Event &e) const + { + std::cout << "\t-id:" << m_num << "-event:" << getEventName(e) << "\n"; + } + + public: + explicit AbstractListenerObserver() + { + m_num = ++num_observers; + } + }; + + /** + * Concrete Subscribers perform some actions in response to notifications issued by the publisher. + * All of these classes must implement the same interface so the publisher isn’t coupled to concrete classes. + */ + class ConcreteListenerObserverA : public AbstractListenerObserver + { + private: + static const inline char *type = "A-type"; + + public: + void update(const Event e) override + { + std::cout << "\tListener: " << type; + log(e); + } + }; + + class ConcreteListenerObserverB : public AbstractListenerObserver + { + private: + static const inline char *type = "B-type"; + + public: + void update(const Event e) override + { + std::cout << "\tListener: " << type; + log(e); + } + }; + + /** + * The Client creates publisher and subscriber objects separately + * and then registers subscribers for publisher updates. + */ + namespace Client + { + void clientCode(IWidgetSubject *const s) + { + s->notify(Event::UPDATE); + } + } + + void run() + { + IWidgetSubject *btn = new ButtonConcreteSubject(); + + IListenerObserver *listener_1 = new ConcreteListenerObserverA(); + IListenerObserver *listener_2 = new ConcreteListenerObserverA(); + IListenerObserver *listener_3 = new ConcreteListenerObserverA(); + IListenerObserver *listener_4 = new ConcreteListenerObserverB(); + + btn->attach(listener_1); + btn->attach(listener_2); + btn->attach(listener_3); + btn->attach(listener_4); + Client::clientCode(btn); + + std::cout << "Remove listener2\n"; + btn->detach(listener_2); + Client::clientCode(btn); + + delete btn; + delete listener_1; + delete listener_2; + delete listener_3; + delete listener_4; + } + + } +} + +struct ObserverAutoRunner +{ + ObserverAutoRunner() + { + std::cout << "\n--- Observer Pattern Example ---\n"; + Observer::run(); + } +}; + +static ObserverAutoRunner instance; \ No newline at end of file diff --git a/src/patterns/behavioral/State.cpp b/src/patterns/behavioral/State.cpp new file mode 100644 index 0000000..22d9952 --- /dev/null +++ b/src/patterns/behavioral/State.cpp @@ -0,0 +1,170 @@ +// State is a behavioral design pattern that lets an object alter its behavior when its internal state changes. +// It appears as if the object changed its class. +// Appicability: +// (*) when you have an object that behaves differently depending on its current state +// , the number of states is enormous, and the state-specific code changes frequently. +// (**) when you have a class polluted with massive conditionals that alter +// how the class behaves according to the current values of the class’s fields. +// (***) when you have a lot of duplicate code across similar states and transitions of a condition-based state machine. +// UML: docs/uml/patterns_behavioral_state.drawio.svg + +#include +#include +namespace +{ + namespace State + { + class DeviceContext; + + /** + * The State interface declares the state-specific methods. + * These methods should make sense for all concrete states because you don’t want some of your states + * to have useless methods that will never be called. + */ + class IState + { + public: + virtual ~IState() = default; + virtual void setContext(DeviceContext *ctx) = 0; + virtual void handle() = 0; + }; + + /* + * To avoid duplication of similar code across multiple states, you may provide intermediate abstract classes + * that encapsulate some common behavior. + */ + class AbstractState : public IState + { + protected: + DeviceContext *m_ctx; + + public: + void setContext(DeviceContext *ctx) override + { + this->m_ctx = ctx; + } + }; + + /** + * Context stores a reference to one of the concrete state objects and delegates to it all state-specific work. + * The context communicates with the state object via the state interface. + * The context exposes a setter for passing it a new state object. + */ + class DeviceContext + { + private: + IState *m_state; + + public: + explicit DeviceContext(IState *state) : m_state{nullptr} + { + this->changeState(state); + } + + ~DeviceContext() + { + delete m_state; + } + + void changeState(IState *state) + { + std::cout << "[DeviceContext]: Changing state\n"; + if (this->m_state != nullptr) + { + delete this->m_state; + } + this->m_state = state; + this->m_state->setContext(this); + } + + void operation() + { + this->m_state->handle(); + } + }; + + /** + * Concrete States provide their own implementations for the state-specific methods. + */ + class IdeConcreteState : public AbstractState + { + public: + void handle() override; + }; + + class ProcessingConcreteState : public AbstractState + { + public: + void handle() override; + }; + + class ErrorConcreteState : public AbstractState + { + public: + void handle() override + { + std::cout << "[Error] Device error! Reset required.\n"; + + // After recover => go Idle + this->m_ctx->changeState(new IdeConcreteState()); + } + }; + + void IdeConcreteState::handle() + { + std::cout << "[Ide] Device is idle. Waiting...\n"; + this->m_ctx->changeState(new ProcessingConcreteState()); + } + + void ProcessingConcreteState::handle() + { + std::cout << "[Processing] Processing data...\n"; + bool ok = true; // Example processing result + static int index = 0; + index++; + if (index % 2 == 0) + { + ok = true; + } + else + { + ok = false; + } + if (ok) + { + // Back to idle after finishing job + this->m_ctx->changeState(new IdeConcreteState()); + } + else + { + this->m_ctx->changeState(new ErrorConcreteState()); + } + } + + namespace Client + { + void clientCode(DeviceContext *const device) + { + device->operation(); + } + } + void run() + { + DeviceContext *device = new DeviceContext(new IdeConcreteState()); + for (int loopIdx = 0; loopIdx < 10; ++loopIdx) + Client::clientCode(device); + delete device; + } + } +} + +struct StateAutoRunner +{ + StateAutoRunner() + { + std::cout << "\n--- State Pattern Example ---\n"; + State::run(); + } +}; + +static StateAutoRunner instance; \ No newline at end of file diff --git a/src/patterns/behavioral/Strategy.cpp b/src/patterns/behavioral/Strategy.cpp new file mode 100644 index 0000000..fc778b5 --- /dev/null +++ b/src/patterns/behavioral/Strategy.cpp @@ -0,0 +1,125 @@ +// cppcheck-suppress-file [functionStatic] + +// Strategy is a behavioral design pattern that lets you define a family of algorithms, +// put each of them into a separate class, and make their objects interchangeable. +// Appicability: +// (*) when you want to use different variants of an algorithm within an object and be able to switch from one algorithm to another during runtime. +// (**) when you have a lot of similar classes that only differ in the way they execute some behavior. +// (***) when your class has a massive conditional statement that switches between different variants of the same algorithm. +// UML: docs/uml/patterns_behavioral_strategy.drawio.svg + +#include +#include +namespace +{ + namespace Strategy + { + class IExportStrategy + { + public: + virtual ~IExportStrategy() = default; + virtual std::string executeExportData(const std::string &content) const = 0; + }; + + class ExportContext + { + private: + std::string m_content; + IExportStrategy *m_strategy; + + public: + ~ExportContext() + { + delete m_strategy; + } + + explicit ExportContext(std::string content, IExportStrategy *const strategy = nullptr) + : m_content{content}, m_strategy{strategy} {} + + void setExportStrategy(IExportStrategy *const strategy) + { + delete m_strategy; + this->m_strategy = strategy; + } + + // The old approach using if-else for each format is commented out: + // if(format == "HTML") { + // // export HTML + // } else if(format == "JSON") { + // // export JSON + // } else if(format == "Markdown") { + // // export Markdown + // } + // + // 1. This approach mixes data (Context) and behavior (export logic), which is hard to maintain. + // 2. Adding new formats requires modifying this function, violating the Open/Closed Principle. + // 3. Strategy Pattern allows each format to be a separate class, and Context only holds data. + // ====================================================================================== + std::string exportDocument() const + { + if (m_strategy != nullptr) + { + return this->m_strategy->executeExportData(this->m_content); + } + else + { + std::cout << "Context: Strategy isn't set\n"; + return ""; + } + } + }; + + class JsonExportStrategy : public IExportStrategy + { + public: + std::string executeExportData(const std::string &content) const override + { + return "{\"content\": \"" + content + "\" }"; + } + }; + + class HtmlExportStrategy : public IExportStrategy + { + public: + std::string executeExportData(const std::string &content) const override + { + return "

" + content + "

"; + } + }; + + namespace Client + { + void clientCode(const ExportContext *ctx) + { + std::cout << ctx->exportDocument(); + std::cout << "\n"; + } + } + void run() + { + ExportContext *ctx = new ExportContext{"This is the report content."}; + Client::clientCode(ctx); + + std::cout << " ===HTML Export ===\n"; + ctx->setExportStrategy(new HtmlExportStrategy()); + Client::clientCode(ctx); + + std::cout << " ===JSON Export ===\n"; + ctx->setExportStrategy(new JsonExportStrategy()); + Client::clientCode(ctx); + + delete ctx; + } + } +} + +struct StrategyAutoRunner +{ + StrategyAutoRunner() + { + std::cout << "\n--- Strategy Pattern Example ---\n"; + Strategy::run(); + } +}; + +static StrategyAutoRunner instance; \ No newline at end of file