- S – SRP – Single Responsibility Principle
- O – OCP – Open Close Principle (Code open for extension but close for modification)
- L – LSP – Liskov Substitution Principle
- I – IS – Interface Segregation
- D – DI – Dependency Inversion
All the above pricipal tends to achieve extensibility, reusability, and maintainability of an application.
The best way to learn design pattern is to use it. For that, we can start with a calculator program.
Implement a calculator program, which has a number of operations. Later on, those operational functionality needs to be extended. In other words, the calculator program should be able to link to a library which contains more operations. This linking does need the calculator program to be compiled again.
#include <stdafx.h>
#include <iostream>
#include <string>
using namespace std;
int apply_op(string op_name, int val, int *result){
int sig;
if (op_name == "add"){
*result += val;
sig = 1;
}
else if (op_name == "mul"){
*result *= val;
sig = 1;
}
else{
sig = -1;
}
return sig;
}
int main(){
string operation;
int result = 0, op_val, sig=1;
while (1){
if (sig == -1){
cout << "Command not supported" << endl;
}
else{
cout << result << endl;
}
cin >> operation >> op_val;
sig = apply_op(operation, op_val, &result);
}
return 0;
}
The above program cannot be extended to new operations without compiling the calculator program. The above program does not follow OCP.
This principal says that the code is open for extension but closes for modification Example: Once the app has been shipped, new features can be added without changing the main application.
Many times code has conditions that may VARY in future, this makes our code tightly coupled with conditions and hence needs MODIFICATION whenever new conditions have to added or existing have to be removed.
Hiding VARYING Implementations behind Abstraction
Anything that varies. In calculator program, the operation were the varying implementation. To hide varying implementation, we create classes. Classes containing specific logic to perform work and theses classes objects can be created.
To abstract means to show only the usage and not the mechanism. Abstract classes are the classes whose object should not be created, only pointer should be created. The implementation class must declare operations to be implemented by VARYING implementations. The pointer can hold address of VARYING Implementation objects.
#include <stdafx.h>
#include <iostream>
//String class provides the overloaded operators like "==", "<<" and ">>"
#include <string>
#include <vector>
using namespace std;
// parent class or the base class of "Command" implementation
// This encapsulates the "Command" implementation classes
class Command
{
public:
// commands contains all varying implementation
// This is static, available Command namespace
static vector<Command*> commands;
// protected has code accessible to only derived class
protected:
//This constructor is invoked after invocation of derived class constructor
// Here we keep all derived class addresses
Command(/* Command *const this = & of derived class obj (example: &ac, &mc) */)
{
commands.push_back(this);
}
public:
// Virtual keyword is neccessary for late binding calls
virtual void process(const string& commandName, double commandData, double& value){
}
};
// static definition
vector<Command*> Command::commands;
// ConsoleUI is a presentation layer, anything that interacts with the user
class ConsoleUI
{
public:
void show(/* const this = &cui */){
double value = 0;
double commandData;
string commandName;
while (true){
cout << value << endl;
cin >> commandName >> commandData; // example: add 2
// Here implementation varies, so we do the encapsulation
for (Command *command : Command::commands){
// Function calls are bound in two ways
// Early binding: during compilation (jump instruction is generated at compilation)
// Late binding: (virtual keyword in base class)
// Late Binding uses VTABLE to reach the correct function (LB are 5 times slower)
command->process(commandName, commandData, value);
}
}
}
};
//Here we have varying implementations, deriving a base class
class AddCommand : public Command
{
// Address always comes to its constructor
// Even though the constructor is not defined, they are auto generated. Below commented code is present
// AddCommand (AddCommand * const this = &object) { do nothing, then call the constrctor of the base class}
void process(/* const this = &object*/const string& commandName, double commandData, double& value){
// string are big object, so pass it by reference, but we dont want to modify it, so declare it as const
if (commandName == "add"){
value += commandData;
}
}
};
/* When a global variable is created outside a main program,
the creation is done by stub code (that calls the main program).
So, the global variables are a part of stub program stack.
These variables are destroyed after main is finished and stub is return to OS */
class MulCommand : public Command
{
void process(/* const this = &object*/const string& commandName, double commandData, double& value){
if (commandName == "mul"){
value *= commandData;
}
}
};
MulCommand mc;
/*
The following stub code is called by OS using main thread
Compiler generated Stub Code - {
Create Global Objects // Variables are created on main thread stack
main();
}
*/
int main(){
ConsoleUI cui; // Created on main thread stack
cui.show(/* &cui*/); //Compiler passes object's address (which is implicit)
/* In a static function, this pointer is not passed, because static function does
not belong to the class, they belong to the class namespace*/
return 0; // Main thread returns to stub code, which return to OS
}
You don't call me, I will call you. In the above program, we had a dependency inversion. This was achieved through the usage of static member in out Cosole program. Whenever the implementation class creates an object of it's self, the call goes to constructor of parent class with the derived class object pointer. With this information, parent class registers the derived class object. The access is done through pointer, and late binding on the derived class functions.
#include <stdafx.h>
#include <iostream>
#include <string>
#include <map>
using namespace std;
class Command
{
public:
static map<string, Command*> commands;
protected:
Command(string command_name)
{
commands[command_name] = this;
}
public:
virtual void process(double commandData, double& value){
}
};
map<string, Command*> Command::commands;
class ConsoleUI
{
public:
void show(){
double value = 0;
double commandData;
string commandName;
while (true){
cout << value << endl;
cin >> commandName >> commandData;
if (Command::commands.find(commandName) != Command::commands.end()){
Command::commands[commandName]->process(commandData, value);
}
else{
cout << "Command not supported" << endl;
}
}
}
};
int main(){
ConsoleUI cui;
cui.show();
return 0;
}
//-------------Extensible classes implemented below, independent of main implementation--------------//
class AddCommand : public Command
{
public:
AddCommand(const string &cmd_name) : Command(cmd_name)
{
}
void process(double commandData, double& value){
value += commandData;
}
};
AddCommand ac("add");
class MulCommand : public Command
{
public:
MulCommand(const string &cmd_name) : Command(cmd_name)
{
}
void process(double commandData, double& value){
value *= commandData;
}
};
MulCommand mc("mul");
class SubCommand : public Command
{
public:
//Define the constructor with command name as input
SubCommand(const string &cmd_name) :Command(cmd_name)
{
}
void process(double commandData, double& value){
value -= commandData;
}
};
SubCommand sb("sub");