Friday, May 6, 2022

The State Design pattern in C++ using timer and notification






As a #guru, let me pay my respects today to my #guru in the tech world, who has influenced my technical skills and mindset.

Although I have never seen them, nor do they know I exist.

But I am grateful to them - and because of them, I have at last become an engineer in the true sense.

First guru: The authors of The Gang of Four Design Pattern Book

Second Guru: Linus Torvalds for offering Linux and rather helping me come out of the vicious cycle of windows

Third Guru: The Android creator and Google for showing me the different nitty-gritty of a complicated framework that i learnt through delving into the Android framework code

Fourth Guru: A professor of a USA University by the name Doug, from whom I got insights of how to study the Android OS difficult parts of the code like the AsyncTask Framework, and he showed  me the right approach of taking the OS study from an engineer’s perspective - delving into the difficult concurrent framework of the Android OS

And my all-time Guru: My family for giving me a sweet, caring, loving #home and my only son, #ridit, for becoming my best student from his early childhood. He is still driving me to burn the midnight oil in front of the laptop to explore the deep ocean of Computer science.

And I am sure, the way the whole professional world of technology is moving, very soon somebody will have to dismiss the book called

#theworldisflat  - The World Is Flat

and write a new one 

#theflatistheworld - The Flat Is The World


Now let's come back to the subject. Let's discuss the technical side of the State Design Pattern.

The state design pattern is a powerful tool in software design, allowing you to encapsulate an object's behavior based on its internal state.

The main participants in this design pattern are:

Context: This object represents the entire entity experiencing state changes. It holds a reference to the current state object and exposes an interface for external interaction.

State: These are concrete classes representing each possible state the object can be in. Each state class defines its own behavior for responding to events and transitions.

Transitions: These define how the object can move from one state to another. They can be triggered by events, user actions, or internal conditions.

In my example, I have cited the process of cooking chicken - how the chicken states alter from an uncleaned state to a cleaned state to a marinated state to a cooked state and finally served to the guests.

The Class Diagram


The Sequence Diagram


Runtime Behaviour

The runtime interaction follows these steps:

  1. The application creates the Context.
  2. The Context delegates execution to the current State.
  3. The State starts a Timer.
  4. The Timer expires.
  5. The Timer invokes the notification callback.
  6. The callback requests the Context to change its current State.
  7. The old state's callback instantiates the new state, triggers its operation, and then hands ownership to Mamma
  8. The Context delegates subsequent operations to the new State.

The source code...


Callback.h

#ifndef CALLBACK_H_

#define CALLBACK_H_


class Callback {

public:

virtual ~Callback() = default;

virtual void goToNextStateFromCallback() = 0;

};


#endif /* CALLBACK_H_ */



ChickenState.h

#ifndef CHICKENSTATE_H_

#define CHICKENSTATE_H_


#include <iostream>

#include "Callback.h"

#include "timer.h"


class Mamma;


class ChickenState : public Callback {

protected:

Mamma* mamma = nullptr;

Timer timer;


public:

explicit ChickenState(Mamma* inMamma) : mamma(inMamma) {

timer.setCallback(this);

}


virtual ~ChickenState() {

timer.justStop();

std::cout << "Destructor of ChickenState\n";

}


virtual void clean() {}

virtual void marinate() {}

virtual void cook() {}

virtual void serve() {}

void goToNextStateFromCallback() override {}

};


#endif /* CHICKENSTATE_H_ */



UncleanedState.h

#ifndef UNCLEANEDSTATE_H_

#define UNCLEANEDSTATE_H_


#include "ChickenState.h"

#include "CleanedState.h"

#include "Mamma.h"


class UncleanedState : public ChickenState {

public:

explicit UncleanedState(Mamma* inMamma) : ChickenState(inMamma) {}


~UncleanedState() override {

std::cout << "UncleanedState destructor\n";

}


void clean() override {

std::cout << "Chicken is getting cleaned (2s)...\n";

timer.setTimeout([this]() {

timer.stop();

}, 2000);

}


void goToNextStateFromCallback() override {

std::cout << "Cleaning complete. Moving to CleanedState...\n";

auto next = std::make_shared<CleanedState>(mamma);

next->marinate(); // Trigger next step in workflow

mamma->changeChickenState(next);

}

};


#endif /* UNCLEANEDSTATE_H_ */


CleanedState.h

#ifndef CLEANEDSTATE_H_

#define CLEANEDSTATE_H_


#include "ChickenState.h"

#include "MarinatedState.h"

#include "Mamma.h"


class CleanedState : public ChickenState {

public:

explicit CleanedState(Mamma* inMamma) : ChickenState(inMamma) {}


~CleanedState() override {

std::cout << "CleanedState destructor\n";

}


void marinate() override {

std::cout << "Chicken is getting marinated (3s)...\n";

timer.setTimeout([this]() {

timer.stop();

}, 3000);

}


void goToNextStateFromCallback() override {

std::cout << "Marinating complete. Moving to MarinatedState...\n";

auto next = std::make_shared<MarinatedState>(mamma);

next->cook();

mamma->changeChickenState(next);

}

};


#endif /* CLEANEDSTATE_H_ */



MarinatedState.h


#ifndef MARINATEDSTATE_H_

#define MARINATEDSTATE_H_


#include "ChickenState.h"

#include "CookedState.h"

#include "Mamma.h"


class MarinatedState : public ChickenState {

public:

explicit MarinatedState(Mamma* inMamma) : ChickenState(inMamma) {}


~MarinatedState() override {

std::cout << "MarinatedState destructor\n";

}


void cook() override {

std::cout << "Chicken is getting cooked (4s)...\n";

timer.setTimeout([this]() {

timer.stop();

}, 4000);

}


void goToNextStateFromCallback() override {

std::cout << "Cooking complete. Moving to CookedState...\n";

auto next = std::make_shared<CookedState>(mamma);

next->serve();

mamma->changeChickenState(next);

}

};


#endif /* MARINATEDSTATE_H_ */


CookedState.h

#ifndef COOKEDSTATE_H_

#define COOKEDSTATE_H_


#include "ChickenState.h"

#include "Mamma.h"


class CookedState : public ChickenState {

public:

explicit CookedState(Mamma* inMamma) : ChickenState(inMamma) {}


~CookedState() override {

std::cout << "CookedState destructor\n";

}


void serve() override {

std::cout << "Chicken is ready! Enjoy the food.\n";

}

};


#endif /* COOKEDSTATE_H_ */



Mamma.h

#ifndef MAMMA_H_

#define MAMMA_H_


#include <memory>

#include <iostream>

#include "ChickenState.h"


class Mamma {

private:

std::shared_ptr<ChickenState> chicken;


public:

Mamma() = default;

virtual ~Mamma();


void changeChickenState(std::shared_ptr<ChickenState> nextState);

void startPreparation();

};


#endif /* MAMMA_H_ */


Mamma.cpp

#include "Mamma.h"


Mamma::~Mamma() {

std::cout << "Inside Mamma destructor\n";

}


void Mamma::changeChickenState(std::shared_ptr<ChickenState> nextState) {

chicken = std::move(nextState);

}


void Mamma::startPreparation() {

if (chicken) {

chicken->clean();

}

}


Timer.h

#ifndef TIMER_H_

#define TIMER_H_


#include <thread>

#include <chrono>

#include <atomic>

#include <mutex>

#include <condition_variable>

#include "Callback.h"


class Timer {

private:

Callback* cb = nullptr;

std::atomic<bool> active{false};

std::mutex mtx;

std::condition_variable cv;

std::thread worker;


public:

Timer() = default;


~Timer() {

justStop();

}


void setCallback(Callback* callback) {

cb = callback;

}


template <typename Callable>

void setTimeout(Callable function, int delayMs) {

justStop();

active.store(true);


worker = std::thread([this, function, delayMs]() {

std::unique_lock<std::mutex> lock(mtx);

bool cancelled = cv.wait_for(lock, std::chrono::milliseconds(delayMs), [this]() {

return !active.load();

});


if (!cancelled && active.load()) {

function();

}

});

}


void stop() {

if (cb) {

cb->goToNextStateFromCallback();

}

}


void justStop() {

active.store(false);

cv.notify_all();

if (worker.joinable()) {

// Prevent self-join deadlock if stop() is triggered from within worker

if (worker.get_id() != std::this_thread::get_id()) {

worker.join();

} else {

worker.detach();

}

}

}

};


#endif /* TIMER_H_ */


Main.cpp

//============================================================================

// Name : Main.cpp

// Author : Som

// Version :

// Copyright : som-itsolutions

// Description : Hello World in C++, Ansi-style

//============================================================================


#include <thread>

#include <chrono>

#include "Mamma.h"

#include "UncleanedState.h"


int main() {

auto mamma = std::make_shared<Mamma>();

auto initialState = std::make_shared<UncleanedState>(mamma.get());


mamma->changeChickenState(initialState);

mamma->startPreparation();


// Keep main alive long enough for asynchronous timers (2s + 3s + 4s = 9s) to finish

std::this_thread::sleep_for(std::chrono::seconds(10));


return 0;

}

To compile the code above, we need to enable the -pthread option for the compiler and linker.

Enjoy...


No comments: