Helper Concepts (💡 First)
Some useful ideas & design pattern that support design patterns
1. Wrapper (a concept similar to Structural Patterns)
What is Wrapper?
A class that
wraps
around another class, It is used to implement a design pattern that has an instance of an object and presents its own interface or behaviour to that object, without changing the original class.
Number of design patterns that can be considered Wrappers:
- Adapter (Will not cover in this series)
- Proxy (Will cover in this series)
- Decorator (Will cover in this series)
Wrapper Classes' UML looks like below.
We can check,
The wrapper has the same interface as the wrapee
The wrapper has the same public method and attributes as the wrapee.
Base on the UML, the example of student, procrastinating Student and student Interface UML looks like below.
/*
Student Interface
*/
class StudentInterface {
public:
virtual void study() = 0;
virtual void procrastinate() = 0;
virtual void print() = 0;
virtual ~StudentInterface() = default;
};
/*
Student Class
*/
class Student : public StudentInterface {
private:
string name;
int age;
string school;
int knowledge;
int happiness;
public:
Student(string name, int age, string school) : name(name), age(age), school(school) {
knowledge = 0;
happiness = 100;
}
void study() {
cout << "I'm studying" << endl;
knowledge += 10;
happiness -= 5;
}
void procrastinate() {
cout << "I'm procrastinating" << endl;
happiness += 10;
}
void print() {
cout << "name: " << name
<< " Age: " << age
<< " School: " << school
<< " Knowledge: " << knowledge
<< " Happiness: " << happiness << endl;
}
};
/*
ProcrastinatingStudent class
*/
class ProcrastinatingStudent : public StudentInterface {
private:
Student *student; //wrapping around student class
public:
ProcrastinatingStudent(Student *student) : student(student) {}
void study() {
student->procrastinate();
student->study();
}
void procrastinate() {
student->procrastinate();
}
void print() {
student->print();
}
~ProcrastinatingStudent(){delete student;}
};
With above implementation, we can use the student object like below
StudentInterface* student = new Student("J", 100, "A");
student = new ProcrastinatingStudent((Student*)student);
student->study();
delete student;
End Wrapper
2. Lazy Initialization (a concept similar to Creational Patterns)
What is Lazy Initialization?
Mandates that we don't initialize an object/resource unless we need it. In easy words, We procrastinate on initializing an expensive resource until we need it.
Lazy Initialization's UML looks like below. (But it can be implemented in so many ways, there really isn't much point in a UML diagram.)
Base on the UML, this is the example that Create the Car object (Expensive Car...) with Lazy Initialization
/*
Car class -> Core
*/
struct Car {
Car() {
cout << "Building a car. This is a time consuming process" << endl;
sleep(2);
cout << "Wait for it..." << endl;
sleep(1);
cout << "Car has been made" << endl;
}
friend ostream& operator<<(ostream& os, const Car& c) {
return os << "I am an expensive car that uses a lot of resources" << endl;
}
};
/*
Client class -> service
*/
class Client {
private:
Car *car = nullptr;
public:
Client() {
//car = new Car; //normally instantiate all data members right away but we don't do in Lazy Initializaiton
}
Car* getCar() {
if(car == nullptr)
car = new Car;
return car;
}
~Client(){
delete car;
}
};
/*
Main
*/
Client client; //don't initialize the Car() inside client right away
cout << *client.getCar() << endl; //initialize the Car() inside client only the first time .car is called
cout << "End of program" << endl;
🚨 Not a good choice if the object needs to be initialized every time it is accessed
End Lazy Initialization
3. Dependency Injection (DI)
What is Dependency Injection?
Concept, Technique, Framework. To understand dependency injection, we first need to explore the meaning of "dependency." What does it mean when we say "A is dependent on B"? This implies that if B changes, it affects A. In other words, if the functionality of B is added or modified, it directly impacts A.
For example, consider the statement:
"A hamburger restaurant chef is dependent on a burger recipe."
This means that if the recipe changes, the chef must also adapt the cooking process accordingly.
class BurgerChef {
HamburgerRecipe* hamburgerRecipe;
public:
BurgerChef() {
hamburgerRecipe = new HamburgerRecipe();
}
};
In the example above, BurgerChef depends solely on HamburgerRecipe. However, if we want the chef to work with various types of burger recipes, we need to abstract the dependency using an interface.
class BurgerRecipe {
public:
virtual void prepareBurger() = 0; // Abstract method
virtual ~BurgerRecipe() {} // Virtual destructor for cleanup
};
class HamburgerRecipe : public BurgerRecipe {
public:
void prepareBurger() override {
// Implementation for hamburger
}
};
class BurgerChef {
BurgerRecipe* burgerRecipe;
public:
BurgerChef() {
burgerRecipe = new HamburgerRecipe(); // Direct dependency
}
~BurgerChef() {
delete burgerRecipe; // Ensure proper cleanup
}
};
By abstracting the dependency with an interface, we can support various burger recipes, reducing tight coupling and improving flexibility.
Introducing Dependency Injection
In the implementation above, the BurgerChef internally determines which BurgerRecipe to use.
Now, imagine a scenario where the burger restaurant owner decides the recipe instead of the chef.
In this case, the BurgerRecipe dependency is determined externally and "injected" into the BurgerChef
.
This concept of externalizing and injecting dependencies is known as Dependency Injection (DI).
How do we implement the DI?
- using Constructor
class BurgerChef {
BurgerRecipe* burgerRecipe;
public:
BurgerChef(BurgerRecipe* recipe) : burgerRecipe(recipe) {}
~BurgerChef() {
delete burgerRecipe; // Clean up the injected dependency
}
};
class BurgerRestaurantOwner {
BurgerChef* burgerChef;
public:
BurgerRestaurantOwner() {
burgerChef = new BurgerChef(new HamburgerRecipe());
}
void changeMenu() {
delete burgerChef; // Clean up the old chef
burgerChef = new BurgerChef(new CheeseburgerRecipe());
}
~BurgerRestaurantOwner() {
delete burgerChef;
}
};
- using Method (Setter)
class BurgerChef {
BurgerRecipe* burgerRecipe;
public:
BurgerChef() : burgerRecipe(new HamburgerRecipe()) {}
void setBurgerRecipe(BurgerRecipe* recipe) {
delete burgerRecipe; // Clean up the old recipe
burgerRecipe = recipe;
}
~BurgerChef() {
delete burgerRecipe;
}
};
class BurgerRestaurantOwner {
BurgerChef burgerChef;
public:
void changeMenu() {
burgerChef.setBurgerRecipe(new CheeseburgerRecipe());
}
};
- Factory Pattern is a way of implementing Dependency Injection.
Top comments (0)