Shortcut to seniority

Home
Go to main page

Section level: Junior
A journey into the programming realm

Section level: Intermediate
The point of no return

Section level: Senior
Leaping into the unknown
Go to main page
A journey into the programming realm
The point of no return
Leaping into the unknown
Software frameworks are abstractions that provide generic functionality which can be selectively overriden or specialized by the developers.
In frameworks, the control flow of the application is dictated by the framework itself, and not by the caller.
The framework provides the environment, or essentially a set of tools to work with, such as libraries or configuration files.
Usually, not even the developers have access to the source code of the framework, but build upon it by calling functions from a pre-built library.
In this chapter, I will talk about a few important classes that exist in a communication framework.
In our case, any message transmitted from one service to another is sent in the form of an event.
Events should have a default priority as normal and the developer should be able to change the priority of a message. The events should also contain an incrementally atomic index, which will be the counter, so we can diferentiate between them.
class IMessage {
public:
enum Priority : int {
PRIO_HIGHEST = 2,
PRIO_HIGH = 1,
PRIO_NORMAL = 0,
PRIO_LOW = -1,
PRIO_VERY_LOW = -2
};
virtual int getMessageID() const = 0;
int getMessageNumber() const;
Priority getPriority() const;
void setPriority(Priority new_prio);
protected:
IMessage() : msg_number(s_message_idx++) {}
int msg_id;
const int msg_number;
Priority msg_prio;
static int s_message_idx;
};
int IMessage::s_message_idx = 0;
Let’s create a class that inherits from this abstract base class, and create an event of a random type.
const int REQ_HELLO = 1;
class RegularMessage : public IMessage {
public:
static IMessage * create(int message_id, Priority priority = PRIO_NORMAL) {
return new RegularMessage(message_id, priority);
}
private:
RegularMessage(int _message_id, Priority _priority) : IMessage() {
msg_id = _message_id;
msg_prio = _priority;
}
int getMessageID() const override;
};
int main() {
IMessage* obj = RegularMessage::create(REQ_HELLO);
return 0;
}
The events queue is a wrapper over a priority queue that keeps all the events that are in line to be processed.
The priority_queue that you can see in the code refers to a max heap structure.
class MessageQueue {
bool push(IMessage*);
IMessage* pop();
bool isEmpty();
int size();
priority_queue<IMessage*> msgQueue;
};
Some logic is also needed to compare the priority of the events, based on their priority and message number.
bool operator < (const IMessage& left, const IMessage& right) {
return
(left.msg_prio < right.msg_prio) ||
(left.msg_prio == right.msg_prio && left.msg_number > right.msg_number);
}
bool operator >= (const IMessage& left, const IMessage& right) {
return !(left<right);
}
class IMessageCompare {
public:
bool operator() (IMessage* left, IMessage* right) {
return
(left->msg_prio < right->msg_prio) ||
(left->msg_prio == right->msg_prio && left->msg_number > right->msg_number);
}
};
Services are classes that can communicate with each other, and they are the base of our framework.
Each service could have a logger attached, so we can log messages from that service.
class Service {
public:
Service(string _rolename) : rolename(_rolename) {}
virtual void dispatch(IMessage*) = 0;
int getThreadID() const;
void setThreadID();
string getRolename() const;
int threadID;
const string rolename;
MessageQueue msgQueue;
};
The implementation could also contain a watchdog that will crash the thread if it doesn’t reply for some time.
The class should contain an event queue and be responsible for processing the next event within the queue.
Each service has their own event queue.
The dispatch function is the function that handles the messages received by this service.
Let’s create a real service now, one that handles messages.
class RealService : public Service {
public:
RealService(string _rolename) : Service(_rolename) {}
void dispatch(IMessage* msg) {
if (msg->getMessageID() == REQ_HELLO) {
std::cout << "Received Hello";
}
}
};
And we can use it like this, to call the function.
IMessage* msg = RegularMessage::create(REQ_HELLO);
RealService service("handler");
service.msgQueue.push(msg); // equivalent of calling dispatch(msg);
But that’s like sending a message to yourself.
What we basically did was to create a message and add it into our own message queue, which will be internally processed further. The queue should process the new message immediately if it’s not busy, or pick them one by one as the messages are handled (one message is finished, pop the next message from the queue and handle it).
All the services need to be orchestrated by a service broker.
The service broker is responsible for creating the services, and keeping the list of services (or a thread pool of objects, or similar), so the services can communicate with each another.
All the events sent from one service to another should go through the service broker, which is responsible for acting as a mediator by pushing the messages into the receiver’s event queue.
We also need some logic to retrieve a service from a rolename.
class ServiceBroker {
template<class T>
void createService(string role);
bool dispatch(thread_id id, IMessage* message);
bool dispatch(string role, IMessage* message);
void register(Service*);
void unregister(Service*);
Service* getService(thread_id);
Service* getService(string);
list<Service*> services;
};
A rolename is an identifier for a service, so that the services can identify who they want to send messages to. Now, we see that we don’t need to create services simply by constructing objects of such type.
Therefore, I would recommend to make the constructor of the service base class private, so we cannot create objects, but make ServiceBroker a friend of it, so we can access it and create objects from inside the ServiceBroker.
That’s how we could use the broker to send messages to different services.
ServiceBroker broker;
broker.createService<RealService>("sender");
broker.createService<AnotherService>("receiver");
IMessage* msg = RegularMessage::create(REQ_HELLO);
broker.dispatch("sender", msg);
broker.dispatch("receiver", msg);
We should keep in mind that the implementation will put each service in a different thread, so that processing one message will not block other services.
The lines 2-3 create two different services (RealService, AnotherService) with different rolenames each, and different implementations of the dispatch (message handle) of the message of type REQ_HELLO.
Timers are classes that raise an event after a given time.
We should have two classes:
class Timer {
public:
Timer(ITimerConsumer& _consumer) : consumer(_consumer) {}
bool start(unsigned int timeInMilliSeconds = 0, bool continuous = false);
void stop();
bool operator ==(Timer& other) { return *this == other; }
ITimerConsumer& getConsumer();
unsigned int getNextFireTime();
bool isActive();
protected:
void onTimerReady() { consumer.processEvent(*this); }
bool repeat = false;
int timeInMilliSeconds = 0;
ITimerConsumer& consumer;
};
The argument received should be comparable (so we can verify which timer expired).
The timer class should receive on constructor an object of type TimerConsumer, and the class that consumes that timer should inherit from it and override the virtual function.
class MyClass : public ITimerConsumer {
MyClass::MyClass() : myTimer(*this) {}
Timer myTimer;
void start();
void processEvent(Timer& timer) override;
};
void MyClass::start() {
myTimer.start(1);
}
void MyClass::processEvent(Timer& timer) {
if (timer == myTimer) {
cout << "myTimer fired!";
}
}
All frameworks need an implementation of a logger, so that they can output messages (errors, warnings, debug logging) in a flexible way, allowing the users of the framework to not reinvent the wheel and write their own logging classes. Other than that, there’s a big chance that a framework will have some custom variable types (even if they are only aliases) and specific structures that need to be logged in their own specific ways.
A logging implementation takes care of different threads, classes and object instances, writing to an output stream, compressing old log files, writing to a specific size and then starting from the beginning (circular buffer), while also having performance in mind, as it does have a tremendous impact on your application.
The data received by the logger is formatted in a specific format, and should at least contain the thread name, the file name, the line number and the message to be printed.
The logger could also contain the verbosity trace level (fatal, normal, warning, etc) and should be able to filter out messages when not interested in them (in order not to spam).
class LogInfo {
string filename;
string function;
int linenumber;
};
class LogFormatter {
virtual string format(LogInfo info, string message) = 0;
};
class SpecificFormatter : public LogFormatter {
string format(LogInfo info, string message) override {
return "[" + info.filename + " ( " + info.function + " ): "
+ info.linenumber + "]" + " " + message;
}
};
class Logger {
public:
enum LogLevel {
Info = 1,
Msg = 2,
Warning = 3,
Debug = 4,
Error = 5,
Fatal = 6
};
static Logger* get();
void log(LogLevel level, LogInfo info, string msg) {
string message = formatter.format(info, msg);
messages.push(message);
}
private:
void sync() {
// lock the queue
for (string msg in messages) {
output << msg;
}
messages.clear();
// unlock the queue
}
LogFormatter formatter;
vector<string> messages;
ostream & output;
};
First two were initially introduced in Chapter 5 – Problem solving
Priority queue (max heap): Elements are inserted based on their priority, thus the most important message is always the first one to be taken from the queue.