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

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
- GeeksforGeeks. 2024. Adapter Pattern | C++ Design Patterns. GeeksforGeeks.
- GeeksforGeeks. 2023. Facade Method - C++ Design Patterns. GeeksforGeeks.
- GeeksforGeeks. 2023. Proxy Pattern | C++ Design Patterns. GeeksforGeeks.