Road to C++ Programmer #22 - Structural Design Patterns

Last Edited: 1/25/2025

The blog post introduces structural design patterns in C++.

CPP Structural

In the last article, we introduced some creational design patterns, which are responsible for making object creation more flexible and efficient. In this article, we will introduce you to some structural design patterns, which aim to manage relationships between objects in a flexible, reusable, and maintainable way.

Adapter Pattern

The adapter pattern, as the name suggests, is a pattern for making two incompatible objects work together by creating an adapter that bridges them. For example, if we have a legacy printer that only accepts strings in all capital letters and a modern computer that sends strings in lowercase, we can prepare an adapter to convert the string from the modern computer to uppercase before sending it to the legacy printer.

class LegacyPrinter {
public:
    void print(string upperCaseString) {
        cout << upperCaseString << endl;
    };
};
 
class ModernComputer {
public:
    string sendMessage() {
        return "message in lower case.";
    }
};
 
class PrinterAdopter {
public:
    string convertMessage(string message) {
        string upperCaseMessage = message;
        for (char& c : upperCaseMessage) {
            c = toupper(c);
        }
        return upperCaseMessage;
    }
};
 
int main() {
    LegacyPrinter printer;
    ModernComputer computer;
    PrinterAdopter adopter;
 
    string message = computer.sendMessage();
    printer.print(adopter.convertMessage(message));
    return 0;
};

The PrinterAdapter converts the message from the computer into uppercase so the printer can print the message. The adapter pattern is an intuitive design that makes it easier to utilize various libraries and objects in a flexible, maintainable, and reusable way.

Facade Pattern

The facade pattern, as the name suggests, is a pattern that uses a facade object to act as an interface for interacting with objects behind it. It strictly hides the composed classes as private attributes so that users interact only with the methods of the facade class.

class Car {
private:
    Engine engine;
    Lights lights;
 
public:
    void StartCar()
    {
        engine.Start();
        lights.TurnOn();
        std::cout << "Car is ready to drive" << std::endl;
    }
 
    void StopCar()
    {
        lights.TurnOff();
        engine.Stop();
        std::cout << "Car has stopped" << std::endl;
    }
};
int main()
{
    Car car;
    car.StartCar();
    car.StopCar();
    return 0;
}

The above implementation of the Car class is an example of the facade pattern that encapsulates the Engine and Lights objects, allowing users to interact with the car without needing to consider the implementations of the engine and lights.

Proxy Pattern

The proxy pattern, as the name suggests, is a pattern that makes use of a proxy object for handling lazy initialization, access control, monitoring, and similar tasks. The proxy acts as a middleman, deciding whether it should perform the work itself or consult the real object it wraps.

// Image Interface
class IImage {
public:
    void display() = 0;
};
 
// Real Image
class RealImage: public IImage {
public:
    RealImage(const std::string& filename) : filename(filename) {
        cout << "Loading image: " << filename << endl; // Heavy operation
    };
 
    void display() override {
        cout << "Displaying image: " << filename << endl;
    };
};
 
// Image Proxy
class ImageProxy: public IImage {
private:
    RealImage *realImage;
    string filename;
public:
    // Do not initialize real image yet
    ImageProxy(const std::string& filename) : filename(filename), realImage(nullptr) {}
 
    // Initialize real object when necessary
    void display() override {
        if (realImage == nullptr) {
            realImage = new RealImage(filename);
        } // If real image has not been loaded, initialize it
        realImage->display();
    }
};

The above example implements lazy initialization, loading the image only when necessary for display using ImageProxy. The image proxy can also include logic regarding access control for the real image object and log access information using a logger object.

Conclusion

In this article, we discussed some examples of structural design patterns: the adapter pattern, facade pattern, and proxy pattern. These patterns are intuitive to understand from their names, and their benefits are clear. However, it is important to acknowledge that not all situations are appropriate for using these patterns. We must carefully design classes based on the problem at hand. In the next article, we will wrap up the discussion of design patterns by introducing some behavioral design patterns.

Resources