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):
(
"API 1 is drawing a circle at ({}, {}) with radius {}"
.
format
(x, y, radius))
class
DrawingAPITwo:
'''Implementation-specific abstraction'''
def
drawCircle(
self
, x, y, radius):
(
"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
3
When 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):
(
"API 1 is drawing a circle at ({}, {}) with radius {}"
.
format
(x, y, radius))
class
DrawingAPITwo:
'''Implementation-specific abstraction'''
def
drawCircle(
self
, x, y, radius):
(
"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):
(
"API 1 is drawing a circle at ({}, {}) with radius {}"
.
format
(x, y, radius))
class
DrawingAPITwo:
'''Implementation-specific abstraction'''
def
drawCircle(
self
, x, y, radius):
(
"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
3
AFTER 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):
(
"API 1 is drawing a circle at ({}, {}) with radius {}"
.
format
(x, y, radius))
class
DrawingAPITwo:
'''Implementation-specific abstraction'''
def
drawCircle(
self
, x, y, radius):
(
"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)