What is the bridge pattern?
The bridge pattern is a structural design pattern. In other words, its main job is to create a full-fledged structure out of classes and objects. A bridge does this by dividing one or more classes into separate hierarchies: abstraction and implementation. A change in functionality in one hierarchy does not entail a change in the other. That's all fine and good, but this definition is very broad and doesn't answer the most important question: "What is the bridge pattern?" I think it will be easier for you to understand its practical application. So right off, let's create a classic scenario for the bridge pattern. We have an abstractShape
class, which represents a generic geometric figure:
Shape.java
public abstract class Shape { public abstract void draw(); }
When we decide to add shapes like triangles and rectangles, we will make them inherit the
Shape
class:Rectangle.java:
public class Rectangle extends Shape { @Override public void draw() { System.out.println("Drawing rectangle"); } }
Triangle.java:
public class Triangle extends Shape { @Override public void draw() { System.out.println("Drawing triangle"); } }
draw()
method will depend on this color. To have different implementations of the draw()
method, then we need to create a class for each shape-color combination. If we have three colors, then we need six classes: TriangleBlack
, TriangleGreen
, TriangleRed
, RectangleBlack
, RectangleGreen
and RectangleRed
.
Six classes isn't such a big problem. But! If we need to add a new shape or color, then the number of classes grow exponentially.
How to get out of this situation? Storing color in a field and enumerating all the options using conditional statements is not the best solution. A good solution is to move color to a separate interface.
No sooner said than done: let's create a Color
interface with three implementations: BlackColor
, GreenColor
and RedColor
:
Color.java:
public interface Color { void fillColor(); }
BlackColor.java:
public class BlackColor implements Color { @Override public void fillColor() { System.out.println("Filling in black color"); } }
GreenColor.java
public class GreenColor implements Color { @Override public void fillColor() { System.out.println("Filling in green color"); } }
RedColor.java
public class RedColor implements Color { @Override public void fillColor() { System.out.println("Filling in red color"); } }
Now we add a
Color
field to theShape
class. We'll get its value in the constructor.Shape.java:
public abstract class Shape { protected Color color; public Shape(Color color) { this.color = color; } public abstract void draw(); }
We will use the
color
variable inShape
implementations. This means that shapes can now use the functionality of theColor
interface.Rectangle.java
public class Rectangle extends Shape { public Rectangle(Color color) { super(color); } @Override public void draw() { System.out.println("Drawing rectangle"); color.fillColor(); } }
Color color
field is a bridge that connects two separate class hierarchies.
How to build a bridge: abstraction and implementation
Let's look at a class diagram that depicts the bridge pattern: Here you can see two independent structures that can be modified without affecting each other's functionality. In our case:- Abstraction is the
Shape
class - RefinedAbstraction is the
Triangle
andRectangle
classes - Implementor is the
Color
interface - ConcreteImplementor is the
BlackColor
,GreenColor
andRedColor
classes.
Shape
class is an abstraction — a mechanism for managing the filling of shapes with various colors, which delegates to the Color
interface (Implementor).
The Triangle
and Rectangle
classes are concrete classes that use the mechanism made available by the Shape
class.
BlackColor
, GreenColor
and RedColor
are concrete implementations in the Implementation hierarchy. Where to use the bridge pattern
A huge benefit of using this pattern is that you can make changes to the functional classes in one hierarchy without breaking the logic of the other. Also, this approach helps to reduce coupling between classes. The main requirement when using this pattern is "follow the instructions" — don't ignore any of them! To that end, let's figure out the situations when you should definitely use the bridge pattern:If you need to expand the number of entities based on combinations of two concepts (e.g. shapes and colors).
If you want to divide a large class that does not meet the single-responsibility principle into smaller classes that have narrow functionality.
If it is necessary to make changes to the logic of certain entities while the program is running.
If it is necessary to hide an implementation from the clients of the class or library.
Pros and cons of the pattern
Like other patterns, a bridge has both advantages and disadvantages. Advantages of the bridge pattern:- It improves the scalability of code — you can add functionality without fear of breaking something in another part of the program.
- It reduces the number of subclasses when the number of entities would otherwise be based on combinations of two concepts (for example, shapes and colors).
- It makes it possible to separately work on two separate hierarchies — Abstraction and Implementation. Two different developers can make changes without delving into the details of each other's code.
- It reduces the coupling between classes — the only place where the two classes are coupled is the bridge (i.e. the
Color color
field).
- Depending on the specific situation and the overall structure of a project, it could negatively impact a program's performance (for example, if you need to initialize more objects).
- It makes code less readable due to the need to switch between the two classes.
GO TO FULL VERSION