DEV Community

Jasper Oh/YJ Oh
Jasper Oh/YJ Oh

Posted on

🧑‍🎨 Helper Concept (some design pattern) - 1️⃣

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.

Image description

We can check,

  1. The wrapper has the same interface as the wrapee

  2. 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.

Image description

/*
Student Interface
*/

class StudentInterface {
public:
    virtual void study() = 0;
    virtual void procrastinate() = 0;
    virtual void print() = 0;
    virtual ~StudentInterface() = default;
};
Enter fullscreen mode Exit fullscreen mode
/*
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;
    }
};
Enter fullscreen mode Exit fullscreen mode
/*
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;}
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.)

Image description

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;
    }
};
Enter fullscreen mode Exit fullscreen mode
/*
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;
    }
};
Enter fullscreen mode Exit fullscreen mode
/*
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;
Enter fullscreen mode Exit fullscreen mode

🚨 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();
    }
};
Enter fullscreen mode Exit fullscreen mode

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
    }
};

Enter fullscreen mode Exit fullscreen mode

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;
    }
};
Enter fullscreen mode Exit fullscreen mode
  • 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());
    }
};
Enter fullscreen mode Exit fullscreen mode
  • Factory Pattern is a way of implementing Dependency Injection.

Top comments (0)