Bridge mode (Bridge)

Definition

Bridging is a structural design pattern that can split a large class or a series of closely related classes into two independent hierarchies, abstraction and implementation, so that they can be used separately during development.

Foreword

1. Question

If you have a Shape class, you can extend it into two subclasses: Circle and Square. You want to extend this class hierarchy to include colors, so you create shape subclasses named Red and Blue. However, since you already have two subclasses, a total of four classes need to be created to cover all combinations, such as BlueCircle and RedSquare.

Adding new shapes and colors to the hierarchy leads to an exponential increase in code complexity. For example, to add a triangle shape, you need to add two subcategories, one for each color; then to add a new color, you need to add three subcategories, one for each shape. If this continues, the situation will get worse.

2. Solution

The root cause of the problem is that we are trying to extend the shape class in two separate dimensions: shape and color. This is a very common problem when dealing with class inheritance.

The bridge pattern solves this problem by changing inheritance to composition. Specifically, it is to extract one of the dimensions and make it an independent class hierarchy, so that the objects of this new level can be referenced in the initial class, so that one class does not have to have all the state and behavior.

According to this method, we can extract the color-related code into a color class with two subclasses, red and blue, and then add a reference member variable pointing to a certain color object in the shape class. Shape classes can now delegate all color-related work to connected color objects. Such references become the bridge between shape and color. Thereafter, adding a new color will no longer require modifying the shape’s class hierarchy, and vice versa.

Structure

  1. The abstract part (Abstraction) provides high-level control logic and relies on implementation objects to complete the underlying actual work.

  2. The implementation part declares a common interface for all concrete implementations. The abstract part can interact with the implementation object only through the methods declared here. The abstract part can list the same methods as the implementation part, but the abstract part usually declares some complex behaviors that rely on a variety of primitive operations declared by the implementation part.

  3. Concrete Implementations include platform-specific code.

  4. Refined Abstraction provides variations of control logic. Like their parent classes, they interact with different implementations through common implementation interfaces.

  5. Normally, the client only cares about working with the abstract part. However, the client needs to connect the abstract object with an implementation object.

Applicable scenarios

  • If you want to split or reorganize a complex class with multiple functions (such as a class that interacts with multiple database servers), you can use the bridge pattern.

The more lines of code a class has, the harder it is to figure out how it works, and the longer it takes to modify it. A change in functionality may require class-wide modifications and often creates bugs or even serious side effects. Bridge pattern can split complex classes into several class hierarchies. Thereafter, you can modify any class hierarchy without affecting other class hierarchies. This approach simplifies code maintenance and minimizes the risk of modifying existing code.

  • Use this pattern if you wish to extend a class in several independent dimensions.

Bridging proposes extracting each dimension into a separate class hierarchy. The initial class delegates the relevant work to objects belonging to the corresponding class hierarchy, without having to do all the work itself.

  • If you need to switch between different implementations at runtime, use bridge mode.

Of course, this does not mean that this must be achieved. The bridge mode can replace the implementation object in the abstract part. The specific operation is as simple as assigning new values to member variables. By the way, this last point is the main reason why many people confuse Bridge Pattern and Strategy Pattern. Remember, design patterns are not just a way to organize classes; they can also be used to communicate intent and solve problems.

Implementation

  1. Identify independent dimensions within a class. Independent concepts might be: abstraction/platform, domain/infrastructure, frontend/backend or interface/implementation.

  2. Understand the client’s business requirements and define them in an abstract base class.

  3. Identify businesses that can be executed on all platforms. And declare the business required for the abstract part in the general implementation interface.

  4. Create implementation classes for all platforms in your domain, but make sure they follow the implementation’s interface.

  5. Add reference member variables to the implementation type in the abstract class. The abstract part delegates most of the work to the implementation object pointed to by the member variable.

  6. If you have multiple variants of your high-level logic, you can create a precise abstraction for each variant by extending the abstract base class.

  7. Client code must pass the implementation object to the constructor of the abstract part so that it can be related to each other. From then on, the client only needs to interact with the abstract object and does not need to deal with the implementation object.

Advantages

  • You can create platform-independent classes and programs.

  • Client code only interacts with high-level abstractions and is not exposed to the details of the platform.

  • Opening and closing principle. You can add abstract parts and implementation parts without affecting each other.

  • Single responsibility principle. The abstract part focuses on handling high-level logic, and the implementation part handles platform details.

Disadvantages

Using this pattern with highly cohesive classes may make the code more complex.

Relationship with other modes

  • Bridging is usually designed early in development, allowing you to isolate various parts of the program for development. Adapters, on the other hand, are often used within existing programs to allow mutually incompatible classes to work well together.

  • The interfaces for the Bridge, Stateful, and Policy (and to some extent Adapter) patterns are very similar. In fact, they are both based on the composition pattern – that is, delegating work to other objects, but each solves different problems. Patterns aren’t just recipes for organizing code in a specific way; you can also use them to discuss the problems they solve with other developers.

  • You can use abstract factories with bridges. This pattern combination is useful if the abstraction defined by the bridge can only work with a specific implementation. In this case, an abstract factory can encapsulate these relationships and hide their complexity from client code.

  • You can use a combination of generators and bridge patterns: the supervisor class does the abstraction, and the various generators do the implementation.

Code

Abstraction.h:

#ifndef ABSTRACTION_H_
#define ABSTRACTION_H_
?
#include <string>
#include "Implementation.h"
?
//Abstract class: Pen
class Pen {
 public:
    virtual void draw(std::string name) = 0;
    void set_color(Color* color) {
        color_ = color;
    }
?
 protected:
    Color* color_;
};
?
#endif // ABSTRACTION_H_

RefinedAbstraction.h:

#ifndef REFINED_ABSTRACTION_H_
#define REFINED_ABSTRACTION_H_
?
#include <string>
#include "Abstraction.h"
?
// Exact abstract class: BigPen
class BigPen : public Pen {
 public:
    void draw(std::string name) {
        std::string pen_type = "Large pen drawing";
        color_->bepaint(pen_type, name);
    }
};
?
// Exact abstract class: SmallPencil
class SmallPencil : public Pen {
 public:
    void draw(std::string name) {
        std::string pen_type = "Small pencil drawing";
        color_->bepaint(pen_type, name);
    }
};
?
#endif // REFINED_ABSTRACTION_H_

Implementation.h:

#ifndef IMPLEMENTATION_H_
#define IMPLEMENTATION_H_
?
#include <string>
#include <iostream>
?
// Implement class interface: color
class Color {
 public:
    virtual void bepaint(std::string pen_type, std::string name) = 0;
};
?
#endif // IMPLEMENTATION_H_

ConcreteImplementation.h:

#ifndef CONCRETE_IMPLEMENTATION_H_
#define CONCRETE_IMPLEMENTATION_H_
?
#include <string>
#include "Implementation.h"
?
//Concrete implementation class: Red
class Red : public Color {
 public:
    void bepaint(std::string pen_type, std::string name) override {
        std::cout << pen_type << "red" << name << "." << std::endl;
    }
};
?
//Concrete implementation class: Green
class Green : public Color {
 public:
    void bepaint(std::string pen_type, std::string name) override {
        std::cout << pen_type << "green" << name << "." << std::endl;
    }
};
?
?
#endif // CONCRETE_IMPLEMENTATION_H_

main.cpp:

#include "ConcreteImplementation.h"
#include "RefinedAbstraction.h"
?
int main() {
    //The client obtains the corresponding Color and Pen based on the runtime parameters
    Color* color = new Red();
    Pen* pen = new SmallPencil();
?
    pen->set_color(color);
    pen->draw("sun");
?
    delete color;
    delete pen;
}

Compile and run:

$g + + -g main.cpp -o bridge -std=c + + 11
$./bridge
Small pencil drawing of the red sun.