Bridge Design Pattern in Modern C++
Bridge Design Pattern is a Structural Design Pattern used to decouple a class into two parts so that both can be developed independently.
Join the DZone community and get the full member experience.
Join For FreeBridge Design Pattern is a Structural Design Pattern used to decouple a class into two parts — abstraction and it's implementation — so that both can be developed independently. This promotes the loose coupling between class abstraction and its implementation. You get this decoupling by adding one more level of indirection i.e. an interface that acts as a bridge between your original class and functionality. Insulation is another name of the Bridge Design Pattern in the C++ world.
"All problems in computer science can be solved by another level of indirection." — David Wheeler
The code snippets you see throughout this series of articles are simplified not sophisticated. So you often see me not using keywords like override
, final
, public
(while inheritance) just to make code compact and consumable(most of the time) in a single standard screen size.
I also prefer struct
instead of class
just to save line by not writing "public:
" sometimes and also miss virtual destructor, constructor, copy constructor, prefix std::
, deleting dynamic memory, intentionally. I also consider myself a pragmatic person who wants to convey an idea in the simplest way possible rather than the standard way or using Jargon.
Note:
- If you stumbled here directly, then I would suggest you go through What is a design pattern? first, even if it is trivial. I believe it will encourage you to explore more on this topic.
- All of this code you encounter in this series of articles are compiled using C++20(though I have used Modern C++ features up to C++17 in most cases). So if you don't have access to the latest compiler you can use https://wandbox.org/ which has preinstalled boost library as well.
Intent
To separate the interface from its implementation.
- In other words, It's all about connecting components together through flexible abstractions using aggregation/composition rather than inheritance/generalization.
- This pattern involves an interface that acts as a bridge. That makes the functionality of concrete classes independent from interface implementer classes. Both types of classes can alter structurally without affecting each other.
The Motivation for Bridge Design Pattern
- Bridge Design Pattern prevents the Cartesian Product complexity explosion. Don't be scared with this mathematical term, I have simplified it with an example below.
- So, for example, let's suppose that you have some base class called
Shape
and then theShape
can beCircle
orSquare
and it can also be drawn by API 1 or API 2.
struct DrawingAPI_1 { };
struct DrawingAPI_2 { };
struct Shape { virtual void draw() = 0; };
/* 2 x 2 scenario */
struct Circle : Shape, DrawingAPI_1 { };
struct Circle : Shape, DrawingAPI_2 { };
struct Square : Shape, DrawingAPI_1 { };
struct Square : Shape, DrawingAPI_2 { };
- This way you end up having a two by two(2×2) scenario. So if you decide to implement it you have to implement four classes. One for you know the
Circle
withAPI_1
,Circle
withAPI_2
and so on. - The Bridge Design Pattern is precisely the pattern that avoids this whole entity explosion.
- So instead of having something like above, what we can do is we design the DrawingAPI interface(which later on used to derive API 1 and 2) and aggregate it in
Circle
andSquare
.
Bridge Design Pattern C++ Example
- So the following is the typical implementation of the Bridge Design Pattern. We are not going to look at anything quite so complicated here. But essentially a bridge is a mechanism that decouples the interface or the hierarchy from the implementation.
xxxxxxxxxx
struct DrawingAPI {
virtual void drawCircle() = 0;
};
struct DrawingAPI_1 : DrawingAPI {
void drawCircle() { cout << "Drawn by API 1"<< endl; }
};
struct DrawingAPI_2 : DrawingAPI {
void drawCircle() { cout << "Drawn by API 2"<< endl; }
};
struct Shape {
Shape(DrawingAPI anddrawingAPI) : m_drawingAPI{drawingAPI} {}
virtual void draw() = 0;
protected:
DrawingAPI andm_drawingAPI; // Now Shapes does not need to worry about drawing APIs
};
struct Circle : Shape {
Circle(DrawingAPI anddrawingAPI) : Shape{drawingAPI} {}
void draw() { m_drawingAPI.drawCircle(); }
};
int main() {
DrawingAPI_1 API_1;
DrawingAPI_2 API_2;
Circle(API_1).draw();
Circle(API_2).draw();
return EXIT_SUCCESS;
}
- This way you don't rely as much as on inheritance and aggregation. Rather you rely on the interface.
Bridge Design Pattern Using C++ Idiom: Pointer to Implementation(PIMPL)
- How can we forget the PIMPLE idiom while we are discussing the Bridge Design Pattern! PIMPLE is the manifestation of the bridge design pattern albeit a slightly different one.
- PIMPL idiom is all about hiding the implementation details of a particular class by sticking it into separate implementation pointed by pointer just as the name suggests. Let me show you how this works:
Person.h
xxxxxxxxxx
#pragma once
#include <string>
#include <memory>
struct Person {
/* PIMPL ------------------------------------ */
class PersonImpl;
unique_ptr<PersonImpl> m_impl; // bridge - not necessarily inner class, can vary
/* ------------------------------------------ */
string m_name;
Person();
~Person();
void greet();
private:
// secret data members or methods are in `PersonImpl` not here
// as we are going to expose this class to client
};
Person.cpp <— will be turned into a shared library(.so/.dll), to hide the business logic
xxxxxxxxxx
#include "Person.h"
/* PIMPL Implementation ------------------------------------ */
struct Person::PersonImpl {
void greet(Person *p) {
cout << "hello "<< p->name.c_str() << endl;
}
};
/* --------------------------------------------------------- */
Person::Person() : m_impl(new PersonImpl) { }
Person::~Person() { delete m_impl; }
void Person::greet() { m_impl->greet(this); }
- OK, so this is the pimple idiom in it's kind of concise form shall we say. And the question is Well why would you want to do this in the first place.
- Security purpose, you might have a question that any way we are going to expose a header file to the client which contains the API of a class, then how do we get security here? Well, just think about the data members and private methods. If you have trade secrets and having a data member which contains critical information. Why do you even let the client know the name of the object?
- One more benefit of PIMPL is compilation time which is critical for C++ as it is widely criticized for it. But this is becoming less and less relevant as the compilers become more and more incremental. And they are fantastic nowadays.
Secure and Faster PIMPL
- As we have to use all the API using indirection provided by std::unique_ptr which accounts for some run-time overhead as we have to dereference the pointer every time for access.
- Plus we also have construction and destruction overhead of std::unique_ptr because it creates a memory in a heap which involves many other functions calling along with system calls.
- Moreover, we also have to bear some indirection if we want to access the data member of
Person
inPersonImpl
like passingthis
pointer or so.
Person.h
xxxxxxxxxx
#pragma once
#include <string>
#include <cstddef>
#include <type_traits>
struct Person {
Person();
~Person();
void greet();
private:
static constexpr size_t m_size = 1024;
using pimpl_storage_t = aligned_storage<m_size, alignment_of_v<max_align_t>>::type;
string m_name;
pimpl_storage_t m_impl;
};
Person.cpp <— Will be turned into a shared library(.so/.dll), to hide the business logic
xxxxxxxxxx
#include "Person.h"
#include <iostream>
struct PersonImpl {
void greet(string andname) {
cout << "hello "<< name << endl;
}
};
Person::Person() {
static_assert(sizeof(impl) >= sizeof(PersonImpl)); // Compile time safety
new(andimpl) PersonImpl;
}
Person::~Person() { reinterpret_cast<PersonImpl*>(andimpl)->~PersonImpl(); }
void Person::greet() { reinterpret_cast<PersonImpl*>(andimpl)->greet(name); }
- So let's address this issue with the placement new operator and preallocated aligned memory buffer. reinterpret_cast is just a compile-time substitute so there won't be any other indirection.
Benefits of Bridge Design Pattern
- Bridge Design Pattern provides flexibility to develop abstraction(i.e. interface) and the implementation independently. And the client/API-user code can access only the abstraction part without being concerned about the Implementation part.
- It preserves the Open-Closed Principle, in other words, improves extensibility as client/API-user code relies on abstraction only so implementation can modify or augmented any time.
- By using the Bridge Design Pattern in the form of PIMPL. We can hide the implementation details from the client as we did in the PIMPL idiom example above.
- The Bridge Design Pattern is an application of the old advice, "prefer composition over inheritance" but more smartly. It comes handy when you must subclass different times in ways that are orthogonal with one another(say 2×2 problem discuss earlier).
- A compile-time binding between an abstraction and its implementation should be avoided. So that an implementation can select at run-time.
Summary by FAQs
What is the practical use case of the Bridge Design Pattern?
Plugins in any internet browser leverage this pattern directly where the browser specifies only abstraction and implementation varies by different types of plugins.
When to use Bridge Design Pattern?
— When you are unsure of implementation or its variations and still you want to move forward with development.
— In case of a behavior permutation problem i.e. Cartesian Product Complexity Explosion.
What are the differences between the Adapter and Bridge Design Pattern?
— The adapter is commonly used with an existing app to make some otherwise-incompatible classes work together nicely.
— The bridge is usually designed up-front, letting you develop parts of an application independently of each other.
What are the differences between Strategy and Bridge Design Pattern?
— The strategy is a single dimension problem like a Multi-bit screwdriver.
— The bridge is a multi-dimension problem like communication types and devices.
Published at DZone with permission of Vishal Chovatiya. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Avoiding Pitfalls With Java Optional: Common Mistakes and How To Fix Them [Video]
-
Writing a Vector Database in a Week in Rust
-
How To Approach Java, Databases, and SQL [Video]
-
Structured Logging
Comments