7. Structural Pattern

  • Adapter Design Pattern
  • Bridge Design Pattern
  • Composite Design Pattern
  • Decorator Design Pattern
  • Flyweight Design Pattern
  • Proxy 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 and Object Adapter.
  • 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.

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

  1. Inherit from the required object (if possible)
  2. 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()