Bridge Design Pattern in Python
What is it?
The Bridge Design Pattern is adopted when implementation-specific classes are mixed together with implementation-independent classes.
It is classified under Structural Design Patterns as it offers one of the best methods to organize class hierarchy.
Consider the following class Circle, which has three attributes: radius, x & y coordinates of its center, and 3 methods: drawWithAPIOne(), drawWithAPITwo() and scale(). Of these, the drawing methods are implementation specific as we have two APIs classes inside the Circle class to draw a circle, and it depends on the user which one to use. This is represented by the following code:
# BEFORE BRIDGE PATTERN # We have a Circle class having 3 attributes and 3 methods. # Attributes are radius, coordinates on a 2-D plane. # Methods are drawWithAPIOne(), drawWithAPITwo() and scale(). Of these, the drawing methods are implementation specific as we have two APIs to draw a circle, and it depends on the user which one to use. class Circle: class DrawingAPIOne: '''Implementation-specific abstraction''' def drawCircle(self, x, y, radius): print("API 1 is drawing a circle at ({}, {}) with radius {}".format(x, y, radius)) class DrawingAPITwo: '''Implementation-specific abstraction''' def drawCircle(self, x, y, radius): print("API 2 is drawing a circle at ({}, {}) with radius {}".format(x, y, radius)) def __init__(self, x, y, radius): '''Implementation-independent abstraction; Initialize the necessary attributes''' self._x = x self._y = y self._radius = radius def drawWithAPIOne(self): '''Implementation-specific abstraction''' objectOfAPIone = self.DrawingAPIOne() objectOfAPIone.drawCircle(self._x, self._y, self._radius) def drawWithAPITwo(self): '''Implementation-specific abstraction''' objectOfAPItwo = self.DrawingAPITwo() objectOfAPItwo.drawCircle(self._x, self._y, self._radius) def scale(self, percent): '''Implementation-independent abstraction''' self._radius *= percent # Instantiate a circle circle1 = Circle(0, 0, 2) # Draw it using API One circle1.drawWithAPIOne() # Instantiate another circle circle2 = Circle(1, 3, 3) # Draw it using API Two circle2.drawWithAPITwo() ## OUTPUT ## API 1 is drawing a circle at (0, 0) with radius 2 API 2 is drawing a circle at (1, 3) with radius 3When the Bridge Pattern is in effect, it resolves this complicated class hierarchy. It separates the implementation-independent class(es) from the implementation-specific class(es).
Why the need for it: Problem Statement
To segregate implementation-specific classes from implementation-independent classes, to facilitate cleaner design.
Terminology
- Implementation-independent abstraction: functionality that is most likely to remain constant in all client requests.
- Implementation-specific abstraction: functionality that is chosen in a particular client request.
Pseudo Code
class DrawingAPIOne: '''Implementation-specific abstraction''' def drawCircle(x, y, radius): draws a circle using arguments provided class DrawingAPITwo: '''Implementation-specific abstraction''' def drawCircle(x, y, radius): draws a circle using arguments provided class Circle: def __init__(self, x, y, radius, drawingAPI): '''Implementation-independent abstraction; Initialize the necessary attributes''' assigns supplied arguments to local variables def draw(self): '''Implementation-specific abstraction''' calls the drawCircle() method of the drawingAPI object provided as argument. def scale(self, percent): '''Implementation-independent abstraction''' increases the radius of the circle by a given percentage ### USING THE ABOVE CODE ### Instantiate a circle and pass to it an object of DrawingAPIOne in its 4rth argument Draw it using API One Instantiate another circle and pass to it an object of DrawingAPITwo in its 4rth argument Draw it using API Two
How to implement it ??
# We have a Circle class having 3 attributes and 3 methods. # Attributes are radius, coordinates on a 2-D plane. # Methods are drawWithAPIOne(), drawWithAPITwo() and scale(). Of these, the drawing methods are implementation specific as we have two APIs to draw a circle, and it depends on the user which one to use. # Our purpose here is to separate the Implementation Specific Abstraction and Implementaion Independent Abstraction into two different class hierarchies. # We wish to keep the Circle class confined to defining its properties and implementation-independent scale() method AND have the draw functionality (which is implementation specific) # delegated to a separate class hierarchy. class DrawingAPIOne: '''Implementation-specific abstraction''' def drawCircle(self, x, y, radius): print("API 1 is drawing a circle at ({}, {}) with radius {}".format(x, y, radius)) class DrawingAPITwo: '''Implementation-specific abstraction''' def drawCircle(self, x, y, radius): print("API 2 is drawing a circle at ({}, {}) with radius {}".format(x, y, radius)) class Circle: def __init__(self, x, y, radius, drawingAPI): '''Implementation-independent abstraction; Initialize the necessary attributes''' self._x = x self._y = y self._radius = radius self._drawingAPI = drawingAPI def draw(self): '''Implementation-specific abstraction''' self._drawingAPI.drawCircle(self._x, self._y, self._radius) def scale(self, percent): '''Implementation-independent abstraction''' self._radius *= percent # Instantiate a circle and pass to it an object of DrawingAPIOne in its 4rth argument circle1 = Circle(0, 0, 2, DrawingAPIOne()) # Draw it using API One circle1.draw() # Instantiate another circle and pass to it an object of DrawingAPITwo in its 4rth argument circle2 = Circle(1, 3, 3, DrawingAPITwo()) # Draw it using API Two circle2.draw()
Walkthrough of implementation
- First, we create a circle object with x = 0, y = 0, radius = 2 and drawingAPI = DrawingAPIOne().
- We then call its draw() method, which calls the drawCircle() method of the passed DrawingAPIOne object.
- Then, to test the functionality of the second drawing API, we create another circle object, with x = 1, y = 3, radius = 3 & drawingAPI = DrawingAPITwo().
- We then call its draw() method, which calls the drawCircle() method of the passed DrawingAPITwo object.
Comparison of code when it is implemented and when it is not
BEFORE BRIDGE PATTERN:
# We have a Circle class having 3 attributes and 3 methods. # Attributes are radius, coordinates on a 2-D plane. # Methods are drawWithAPIOne(), drawWithAPITwo() and scale(). Of these, the drawing methods are implementation specific as we have two APIs to draw a circle, and it depends on the user which one to use. class Circle: class DrawingAPIOne: '''Implementation-specific abstraction''' def drawCircle(self, x, y, radius): print("API 1 is drawing a circle at ({}, {}) with radius {}".format(x, y, radius)) class DrawingAPITwo: '''Implementation-specific abstraction''' def drawCircle(self, x, y, radius): print("API 2 is drawing a circle at ({}, {}) with radius {}".format(x, y, radius)) def __init__(self, x, y, radius): '''Implementation-independent abstraction; Initialize the necessary attributes''' self._x = x self._y = y self._radius = radius def drawWithAPIOne(self): '''Implementation-specific abstraction''' objectOfAPIone = self.DrawingAPIOne() objectOfAPIone.drawCircle(self._x, self._y, self._radius) def drawWithAPITwo(self): '''Implementation-specific abstraction''' objectOfAPItwo = self.DrawingAPITwo() objectOfAPItwo.drawCircle(self._x, self._y, self._radius) def scale(self, percent): '''Implementation-independent abstraction''' self._radius *= percent # Instantiate a circle circle1 = Circle(0, 0, 2) # Draw it using API One circle1.drawWithAPIOne() # Instantiate another circle circle2 = Circle(1, 3, 3) # Draw it using API Two circle2.drawWithAPITwo() ## OUTPUT ## API 1 is drawing a circle at (0, 0) with radius 2 API 2 is drawing a circle at (1, 3) with radius 3AFTER BRIDGE PATTERN:
# We have a Circle class having 3 attributes and 3 methods. # Attributes are radius, coordinates on a 2-D plane. # Methods are drawWithAPIOne(), drawWithAPITwo() and scale(). Of these, the drawing methods are implementation specific as we have two APIs to draw a circle, and it depends on the user which one to use. # Our purpose here is to separate the Implementation Specific Abstraction and Implementaion Independent Abstraction into two different class hierarchies. # We wish to keep the Circle class confined to defining its properties and implementation-independent scale() method AND have the draw functionality (which is implementation specific) # delegated to a separate class hierarchy. class DrawingAPIOne: '''Implementation-specific abstraction''' def drawCircle(self, x, y, radius): print("API 1 is drawing a circle at ({}, {}) with radius {}".format(x, y, radius)) class DrawingAPITwo: '''Implementation-specific abstraction''' def drawCircle(self, x, y, radius): print("API 2 is drawing a circle at ({}, {}) with radius {}".format(x, y, radius)) class Circle: def __init__(self, x, y, radius, drawingAPI): '''Implementation-independent abstraction; Initialize the necessary attributes''' self._x = x self._y = y self._radius = radius self._drawingAPI = drawingAPI def draw(self): '''Implementation-specific abstraction''' self._drawingAPI.drawCircle(self._x, self._y, self._radius) def scale(self, percent): '''Implementation-independent abstraction''' self._radius *= percent # Instantiate a circle and pass to it an object of DrawingAPIOne in its 4rth argument circle1 = Circle(0, 0, 2, DrawingAPIOne()) # Draw it using API One circle1.draw() # Instantiate another circle and pass to it an object of DrawingAPITwo in its 4rth argument circle2 = Circle(1, 3, 3, DrawingAPITwo()) # Draw it using API Two circle2.draw() ## OUTPUT ## API 1 is drawing a circle at (0, 0) with radius 2 API 2 is drawing a circle at (1, 3) with radius 3
Related to: Abstract Factory & Adapter
- Creational Patterns
- Factory
- Abstract Factory
- Prototype
- Singleton
- Builder
- Architectural Pattern
- Model View Controller (MVC)