7. Structural Pattern
Adapter
Design PatternBridge
Design PatternComposite
Design PatternDecorator
Design PatternFlyweight
Design PatternProxy
Design Pattern- Facade Design Pattern
7.1 Adapter Design Pattern
The Adapter Design Pattern is a structural design pattern and is also known as the Wrapper Design Pattern
.
This design pattern acts as a bridge
between two different interfaces.
It can convert the interface of a class, to make it compatible
with a client who is expecting a different interface,
without changing the source code of the class.
- Which design problems does the adapter design pattern solve?
- The adapter design pattern solves problems like:
Reusing a class that does not have an interface
that a client requires and making classes that have incompatible interfaces work together.
- What are the two variations of the adapter design pattern?
- Two Variants of Adapter Pattern:
Class Adapter
andObject Adapter
.
- Two Variants of Adapter Pattern:
- What is the difference between Class Adapter and Object Adapter?
- Class Adapter as the name suggests can
only wrap classes and not interfaces
. It uses inheritance to achieve this. - Object Adapter can
wrap both classes and objects
and it uses composition to do this.
- Class Adapter as the name suggests can
Example - 1
# Legacy Code - gives XML data
class XML:
def get_xml_data(self): pass
class XMLData(XML):
def get_xml_data(self):
return 'XML data'
# New Code - Takes JSON data
class DataAnalyticsTool:
@staticmethod
def analyse_data(json_data):
print('Analysing ' + json_data)
# Adapter - Convert XML to JSON
class XMLToJSONAdapter:
def __init__(self, xml_data):
self.xml_data_obj = xml_data
def get_json_data(self):
xml_data = self.xml_data_obj.get_xml_data()
# Logic to convert XML to JSON
return 'XML converted to JSON'
# Adding Factory Design as well
class DataAnalyticsToolFactory:
@staticmethod
def process_data(data):
if data == 'json':
DataAnalyticsTool.analyse_data('JSON data')
elif data == 'xml':
xml_data_obj = XMLData()
adapter = XMLToJSONAdapter(xml_data_obj)
data = adapter.get_json_data()
DataAnalyticsTool.analyse_data(data)
# Client
if __name__ == "__main__":
DataAnalyticsToolFactory().process_data('json')
DataAnalyticsToolFactory().process_data('xml')
Example - 2
https://www.geeksforgeeks.org/adapter-pattern/
class Bird:
def make_sound(self): pass
class Sparrow(Bird):
def make_sound(self):
print("Sparrow - make sound")
# Above are OLD code
class ToyDuck:
def squeak(self): pass
class PlasticToyDuck(ToyDuck):
def squeak(self):
print("Plastic toy duck - Squeak")
# Above are new CODE
# Bird to ToyDuck
class BirdAdapter(ToyDuck):
def __init__(self, bird):
self.bird = bird
def squeak(self):
self.bird.make_sound()
if __name__ == "__main__":
sparrow = Sparrow()
sparrow.make_sound()
toy_duck = PlasticToyDuck()
toy_duck.squeak()
# Wrap a bird in a birdAdapter so that it behaves like toy duck
bird_adapter = BirdAdapter(sparrow)
# Toy duck behaving like a bird
bird_adapter.squeak()
7.2 Bridge Design Pattern
The Bridge design pattern allows you to separate the abstraction from the implementation
. It is a structural design pattern.
A mechanism that decouples an interface
(hierarchy) from an implementation(hierarchy)
Adapter vs Bridge Pattern
- Adapter design pattern is commonly used with an
existing app
to allow objects with incompatible interfaces to collaborate, - whereas the Bridge Pattern is
implemented up-front
as a precautionary measure to allow us to develop various parts of an application independently of each other.
Example - 1
https://www.udemy.com/course/design-patterns-python/learn/lecture/13648674
# Draws shapes like - Circle
# Render shapes in two form - Vector or Raster
# -- Raster graphics are composed of pixels, while vector graphics are composed of paths
# # Option - 1
class Shape: pass
class VectorCircle(Shape): pass
class RasterCircle(shape): pass
# scaling of this will be an big issue
class Renderer:
def render_circle(self): pass
class VectorRenderer(Renderer):
def render_circle(self):
print('Vector rendering - Circle')
class RasterRenderer(Renderer):
def render_circle(self):
print('Raster rendering - Circle')
# Connect Rander and Shape class - by passing renderer as shape args
# Acting as a Bridge - Connects two hierarchy of different classes
class Shape:
def __init__(self, renderer):
self.renderer = renderer
def draw(self): pass
class Circle(Shape):
def __init__(self, renderer):
super().__init__(renderer)
def draw(self):
self.renderer.render_circle()
if __name__ == '__main__':
vector = VectorRenderer()
circle = Circle(vector)
circle.draw()
raster = RasterRenderer()
circle = Circle(raster)
circle.draw()
7.3 Composite Design Pattern
Also known as Object Tree
Composite is a structural design pattern that lets you compose objects into tree structures
and then work with these structures as if they were individual objects.
The composite pattern describes a group of objects
that are treated the same way as a single instance
of the same type of object.
Implementing the composite pattern lets clients treat
individual objects and compositions uniformly
.
Example - 1
class GraphicObject:
def __init__(self, color=None):
self.color = color
self.children = []
class Circle(GraphicObject):
def __str__(self):
return self.color + ' Circle'
class Square(GraphicObject):
def __str__(self):
return self.color + ' Square'
if __name__ == '__main__':
graphic1 = GraphicObject()
graphic1.children.append(Square('Red'))
graphic1.children.append(Circle('Yellow'))
graphic2 = GraphicObject()
graphic2.children.append(Circle('Blue'))
graphic2.children.append(Square('Blue'))
graphic1.children.append(graphic2)
# 'graphic1' group contains another group 'graphic2' - Composite
print('drawing ==> ', graphic1.__dict__)
print('drawing.children[2] ==>', graphic1.children[2].__dict__)
print('drawing.children[2].children ==>', graphic1.children[2].children[0])
- Each class holds a list of objects of its child class.
class Star: pass
class Constellation:
starts = []
class Galaxy:
stars = [] # list of Start objs
constellations = []
class Universe:
galaxies = [] # list of Galaxy objs
7.4 Decorator Design Pattern
Decorator is a structural design pattern that lets you attach new behaviors
to objects by placing these
objects inside special wrapper objects
that contain the behaviors.
Because it offers a wrapper to an existing class, the decorator design pattern is also known as Wrapper
.
Two ways - to augment an object with additional functionality without re-writing the existing code
- Inherit from the required object (if possible)
- Build a Decorator, which simply references the decorator object(s)
Example - 1
def make_pretty(func):
def inner():
print("Inner function")
func()
return inner
# Without decorator
def ordinary():
print("Ordinary function")
pretty = make_pretty(ordinary)
pretty()
# With decorator(Using Closure)
@make_pretty
def ordinary():
print("Ordinary function")
ordinary()
# OUTPUT
# Inner function
# Ordinary function
Example - 2
https://www.udemy.com/course/design-patterns-python/learn/lecture/13649104
import time
def time_it(func):
def wrapper():
start = time.time()
result = func()
end = time.time()
print(f'{func.__name__} took {int((end - start) * 1000)}ms')
return result
return wrapper
@time_it
def some_operation():
print('Starting operation')
time.sleep(1)
print('We are done')
return 123
if __name__ == '__main__':
some_operation()
7.5 Flyweight Design Pattern
Flyweight design patterns are structural design patterns that help to reduce the memory usage
when
creating numerous objects by sharing their common states
.
Flyweight is an optimization pattern
.
- Avoid redundancy when storing data
https://www.udemy.com/course/design-patterns-python/learn/lecture/13661084
import time
class User:
def __init__(self, name):
self.name = name
class User2:
strings = []
def __init__(self, full_name):
def get_or_add(name): # return: name position in strings
if name in self.strings:
return self.strings.index(name)
else:
print('Creating Object')
time.sleep(2)
self.strings.append(name)
return len(self.strings)-1
names = [get_or_add(x) for x in full_name.split(' ')]
self.names = str(names) + ' --> ' + self.strings[names[0]] + ' ' + self.strings[names[1]]
print(self.names)
def __str__(self):
return ' '.join([self.strings[x] for x in self.names])
if __name__ == '__main__':
u1 = User2('Amrit Singh')
u2 = User2('Rakesh Prasad')
u3 = User2('Amrit Prasad')
u4 = User2('Rakesh Singh')
What are intrinsic and extrinsic states in flyweight design pattern?
Intrinsic states are shared
across all the objects, whereas extrinsic states are unique
to each object.
For example, a red car can have the intrinsic state color and the extrinsic state speed because
the color is red for all red cars, but the speed may vary.
How are intrinsic states shared across flyweight objects?
We can use a factory class in the flyweight design pattern to create the flyweight objects.
The factory class will have an internal cache
that stores the first created flyweight object.
The flyweight object is returned from the cache for successive requests instead of being created again.
import time
# intrinsic
class Tree:
def __init__(self, color):
print('Creating Object')
time.sleep(2)
self.color = color
self.height = 6
# extrinsic
class TreePosition:
def __init__(self, x, y):
self.x = x
self.y = y
class TreeFactory:
treeMap = None
def __init__(self):
TreeFactory.treeMap = dict()
def get_tree(self, color):
if color in TreeFactory.treeMap.keys():
return TreeFactory.treeMap.get(color)
tree = Tree(color)
TreeFactory.treeMap[color] = tree
return tree
class Game:
def __init__(self):
self.trees = []
self.treePositions = []
self.treeFactory = TreeFactory()
def add_tree(self, x, y, color):
tree = self.treeFactory.get_tree(color)
self.trees.append(tree)
self.treePositions.append(TreePosition(x, y))
self.render_tree()
def render_tree(self):
treeId = len(self.trees) - 1
print("Tree " + str(treeId) + " with " + self.trees[treeId].color + " color" +
" rendered at " + str(
self.treePositions[treeId].x) + ", " + str(self.treePositions[treeId].y))
class Client :
@staticmethod
def main(args) :
game = Game()
game.add_tree(1, 3, "green")
game.add_tree(2, 5, "brown")
game.add_tree(4, 8, "green")
game.add_tree(4, 9, "green")
game.add_tree(5, 3, "brown")
if __name__=="__main__":
Client.main([])
7.6 Proxy Design Pattern
Proxy Design Pattern is a type of structural design pattern which is used whenever we need a placeholder or representational object
that can work in place of the real object.
The proxy acts as an intermediatory layer
between the client and the real object and hence can control the access
to the real object, add additional functionality, and even restrict client access. It is also known as Surrogate Pattern
.
Example - 1 : Protection Proxy
https://www.udemy.com/course/design-patterns-python/learn/lecture/13674026
Any one can drive using Car
but only adult can drive using CarProxy
class Car:
def __init__(self, driver):
self.driver = driver
def drive(self):
print(f'Car being driven by {self.driver.name}({self.driver.age})')
class CarProxy:
def __init__(self, driver):
self.driver = driver
self.car = Car(driver)
def drive(self):
if self.driver.age >= 16:
self.car.drive()
else:
print(f'Driver({self.driver.name}) is too young')
class Driver:
def __init__(self, name, age):
self.name = name
self.age = age
if __name__ == '__main__':
# Without Proxy
car = Car(Driver('Amrit', 15))
car.drive()
# With Proxy
car = CarProxy(Driver('John', 15))
car.drive()
Example - 2 : Virtual Proxy
https://www.udemy.com/course/design-patterns-python/learn/lecture/13674028
- Bitmap - Image get loaded even we don’t draw the images
- LazyBitmap(Proxy) - OCP concept, load only if you want to draw and load image only once
class Bitmap:
def __init__(self, filename):
self.filename = filename
print(f'Loading image from {filename}')
def draw(self):
print(f'Drawing image {self.filename}')
class LazyBitmap:
def __init__(self, filename):
self.filename = filename
self.bitmap = None
def draw(self):
if not self.bitmap:
self.bitmap = Bitmap(self.filename)
self.bitmap.draw()
def draw_image(image):
print('About to draw image')
image.draw()
print('Done drawing image')
if __name__ == '__main__':
# Image get loaded even we don't draw the images
bmp = Bitmap('facepalm.jpg')
draw_image(bmp)
# Using OCP - load only if you want to draw and load image only once
bmp = LazyBitmap('facepalm.jpg') # Bitmap
draw_image(bmp)
draw_image(bmp)
7.7 Facade Design Pattern
Provides a simple, easy to understand interface over a large and sophisticated body of code
So, As the name suggests, it means the face of the building
.
The people walking past the road can only see this glass face of the building.
They do not know anything about it, the wiring, the pipes and other complexities.
It hides all the complexities
of the building and displays a friendly face
.
Example - 1
class PlaceOrder:
def order_status(self):
return "Order is Placed!"
class Payment:
def pay_status(self):
return "Payment is Done"
class Delivering:
def delivery_status(self):
return "Delivering..."
# FACADE
class Zomata:
def __init__(self, item):
self.item = item.upper()
self.place_order = PlaceOrder()
self.payment = Payment()
self.delivering = Delivering()
def process_order(self):
print(self.item, self.place_order.order_status())
print(self.item, self.payment.pay_status())
print(self.item, self.delivering.delivery_status())
if __name__ == "__main__":
op = Zomata('Maggi')
op.process_order()