DEV Community

Jasper Oh/YJ Oh
Jasper Oh/YJ Oh

Posted on

🧑‍🎨 Structural Patterns - 3️⃣

Structural Patterns (🏗 Third)

Patterns that focus on the structure of objects


1. Proxy

What is Proxy Design Pattern?

Proxy is a wrapper, and it controls access to the wrapee.

Lets check the UML of Proxy Design Pattern.

Image description

It looks exact same structure as a wrapper.

And lets just dive in to the code.

First, Interface and Service. As you can see in the below, Service and Proxy must fully implement all abstract methods

class ServiceInterface {
public:
    virtual void doSomething() = 0;
};

//The actual service where the work is done
class Service : public ServiceInterface {
public:
    void doSomething() override {
        cout << "doing service code" << endl;
    }
};


Enter fullscreen mode Exit fullscreen mode

Second, Our Proxy code is here.

class Proxy : public ServiceInterface {

    Service* service;

public:
    Proxy(Service* s) : service(s) {}

    void doSomething() override {
        cout << "doing proxy code before" << endl;
        service->doSomething();
        cout << "doing proxy code after" << endl;
    }
};
Enter fullscreen mode Exit fullscreen mode

Proxy contains a pointer to the actual service. Any calls made to the proxy are directed to the actual service. Some pre and post code may be done before
and after the call to service (checking the security or logging...)

And Client, the client can use real service or proxy version, client doesn't know

void client(ServiceInterface *si) {
    si->doSomething();
}
Enter fullscreen mode Exit fullscreen mode

At last, in main method, instantiate real and proxy services, pass pointer of real service to proxy

    Service *service = new Service;
    Proxy *proxy_youtube = new Proxy(service);

    // Directly use the service
    client(youtube);

    cout << endl;

    // use the proxy service
    client(proxy_youtube);

    delete youtube;
    delete proxy_youtube;
Enter fullscreen mode Exit fullscreen mode

As we can see, proxy implements the same interface as the service, it can be passed to any object that expects a Service Interface.

And proxy can work even if the original service is not available.


2. Facade

What is Facade Design Pattern?

The Facade pattern encapsulates a complex system or API. Used when we need to provide a simple interface to a complex system. Abstracts away the details of the complex system or API.

Lets check the UML of the Facade

Image description

I want to highlight in the UML, that Facade provides convenient access to a particular part of the subsystem's functionality. And Subsystem classes aren't aware of the facade's existence. They operate within the system and work with each other directly. At the last, Client uses the facade instead of calling the subsystem objects directly

With these information, lets dive in to the code.

There will be lots(?) of subsystem called ClassA, B, C

class Class1 {
public:
    void doSomething() {
        cout << "class 1 doing something" << endl;
    }
};

class Class2 {
public:
    void doSomething() {
        cout << "class 2 doing something" << endl;
    }
};

class Class3 {
public:
    void doSomething() {
        cout << "class 3 doing something" << endl;
    }
};
Enter fullscreen mode Exit fullscreen mode

And we need a Facade class to provides a simple interface for the client to access in order to begin the action of the complicated code

class Facade {
private:
    Class1 c1;
    Class2 c2;
    Class3 c3;

public:
    void execute1() {
        c1.doSomething();
        c2.doSomething();
        c3.doSomething();
    }

    void execute2() {
        c3.doSomething();
        c1.doSomething();
        c2.doSomething();
        c3.doSomething();
        c1.doSomething();
        c1.doSomething();
    }
};

Enter fullscreen mode Exit fullscreen mode

And in main we can just simply instantiate the Facade to execute subsystems

    Facade f;

    //f.execute1();
    f.execute2();
Enter fullscreen mode Exit fullscreen mode
  • But we need to aware that this pattern can become epicentre of coupling. And become an extremely complex and large class to maintain

3. Bridge

What is Bridge Design Pattern?

When we want to separate a large class or a set of highly closely related classes into two separate hierarchies, an "Abstraction" and an "Implementation"

Lets jump to the UML of bridge pattern,

Image description

Lets say we want to create the shapes - circle, square ... There will be lots of thing to considerate like colour of the shape, scale..etc. With this Bridge pattern, we can divide these with abstraction and implementation.

This can be hard to understand, but lets check the code with the UML + example of shapes

First, lets jump to implementation box.

In this case, our color class can be a implementation context as a interface.

struct Color {
    string color;
    virtual string get_color() = 0;
};
Enter fullscreen mode Exit fullscreen mode

And for the concrete Implementation, There would be a concrete Color like red, blue... with implement the Color interface.

struct Red : Color {
    Red() {
        color = "#FF0000";
    }

    string get_color() override {
        return "Red";
    }
};

struct Blue : Color {
    Blue() {
        color = "#0000FF";
    }

    string get_color() {
        return "Blue";
    }
};
Enter fullscreen mode Exit fullscreen mode

And we need Abstraction. Shape that have Color as data member.


struct Shape {
    Color* my_color;

    Shape(Color *color) {
        my_color = color; //here is the BRIDGE to the color object/class
    }

    virtual void draw() {
        cout << "I'm a Shape with color " << my_color->get_color() << endl;
    }

    virtual ~Shape() {
        delete my_color;
    }
};

Enter fullscreen mode Exit fullscreen mode

With this abstraction, we can create circle and square.

struct Square : Shape {
    Square(Color *color) : Shape(color) {};
    void draw() override {
        cout << "I'm a Square with color " << my_color->get_color() << endl;
    }
};

struct Circle : Shape {
    Circle(Color *color) : Shape(color) {};
    void draw() override {
        cout << "I'm a Circle with color " << my_color->get_color() << endl;
    }
};
Enter fullscreen mode Exit fullscreen mode

And in main, we can create the each shape with color!

    cout << "---- COMBO 1: Red Square ----" << endl;
    Square red_square{new Red()};
    red_square.draw();

    cout << "\n---- COMBO 2: Blue Circle ----" << endl;
    Circle blue_circle{new Blue()};
    blue_circle.draw();

    cout << "\n---- COMBO 3: Red Circle ----" << endl;
    Circle red_circle{new Red()};
    red_circle.draw();

Enter fullscreen mode Exit fullscreen mode

So, When we want to divide and conquer a huge class that has many variation, we can use bridge pattern. And plus, if we want to switch implementations at run-time, we can use the bridge pattern as well.


4. Decorator

What is Decorator Design Pattern?

To understand the Decorator Design pattern, we need to recap the wrapper concept Recap-Wrapper. And Decorator is We 'wrap' our base object using decorative pieces. In other words, Decorator lets you attach new behaviours to objects by replacing these objects inside special wrapper objects that contain the behaviours.

Lets say we want to create a donut with toppings.

Lets check the UML.

Image description

As you can see, WheatDonut and RiceDonut implement the "Component" interface and the "Decorator" as well. And few Sprinkles, Gummies, Chocolate implement the Decorator, which means we can "decorate" donuts with decorator pattern.

Now, lets dive into the code.

First, check the component

class Component
{
public:
    virtual std::string Description()
    {
        return "Unknown component";
    }
    virtual int getCost() = 0;
    virtual ~Component() {};
};
Enter fullscreen mode Exit fullscreen mode

And with this component, create the wheat-donut, and rice-donut

class WheatDonut : public Component
{
public:
    std::string Description() override
    {
        return "Wheat donut";
    }
    int getCost() override {  return 200;  }
    //~WheatDonut() { std::cout << "deleted WheatDonut" << std::endl;  };
};

class RiceDonut : public Component
{
public:
    std::string Description() override
    {
        return "Rice donut";
    }
    int getCost() override {  return 300; }
    //~RiceDonut() { std::cout << "deleted RiceDonut" << std::endl;  };
};
Enter fullscreen mode Exit fullscreen mode

And now, we realize we need a decorator which can enable to put on the topping in donut. We can put the data-member in each donuts, but rather than that, create the decorator which implement the component.

class Decorator : public Component
{
protected:
    Component *component;
    virtual ~Decorator() {
        delete component;
    }
};
Enter fullscreen mode Exit fullscreen mode

Can then, create the toppings with implementing decorator.

class Sprinkles : public Decorator
{
public:
    Sprinkles(Component *donut) { this->component = donut; }
    std::string Description() override {
        return component->Description() + ", Sprinkles";
    }
    int getCost() override { return 50 + component->getCost(); }
    //~Sprinkles() { std::cout << "deleted Sprinkles" << std::endl;  };
};

class Gummies : public Decorator
{
public:
    Gummies(Component *donut) {  this->component = donut;  }
    std::string Description() override {
        return component->Description() + ", Gummies";
    }
    int getCost() override {
        return 90 + component->getCost(); }
    //~Gummies() { std::cout << "deleted Gummies" << std::endl;  };
};
class Chocolate : public Decorator
{
public:
    Chocolate(Component *donut)  {  this->component = donut; }
    std::string Description() override {
        return component->Description() + ", Chocolate";
    }
    int getCost() override {  return 75 + component->getCost(); }
    //~Chocolate() { std::cout << "deleted Chocolate" << std::endl;  };
};
Enter fullscreen mode Exit fullscreen mode

And after that, in main we can "decorate" each donut with decorators

    Component *riceDonut = new RiceDonut();

    riceDonut = new Sprinkles(riceDonut);

    riceDonut = new Gummies(riceDonut);

    riceDonut = new Chocolate(riceDonut);

    cout << riceDonut->Description() << " Cost: $" << riceDonut->getCost() << endl;

    // create new WheatDonut component
    Component *wheatDonut = new WheatDonut();

    // decorate it with Sprinkles topping
    wheatDonut = new Sprinkles(wheatDonut);

    //decorate it with Gummies topping
    wheatDonut = new Gummies(wheatDonut);

    cout << wheatDonut->Description() << " Cost: $" << wheatDonut->getCost() << endl << endl;

    delete riceDonut;
    delete wheatDonut
Enter fullscreen mode Exit fullscreen mode

This pattern, adhere to the single responsibility principle - which mean each decorator is responsible for one thing, and also open close principle.

This enables add or remove responsibilities at run-time.


Top comments (0)