Structural Design Patterns in Python

Introduction

Structural Design Patterns are used to assemble multiple classes into bigger working structures. Sometimes interfaces for working with multiple objects simply don't fit, or you're working with legacy code you can't change but need new functionality, or you just start to notice your structures seem untidy and excessive, but all elements seem necessary.

This is the second article in a short series dedicated to Design Patterns in Python.

They're very useful for creating readable, maintainable, layered code, especially when working with external libraries, legacy code, interdependent classes, or numerous objects.

The design patterns covered in this section are:

Adapter Design Pattern

In the real world, you may use an adapter to connect chargers to different sockets when traveling to other countries, or different models of phones. You may use them to connect an old VGA monitor to an HDMI socket on your new PC.

The design pattern got its name because its purpose is the same - adapting one input to a different predetermined output.

Say you're working on an image displaying software, and so far your clients only wanted to display raster images. You have a complete implementation for drawing, say, a .png file to the screen.

For simplicity's sake, this is how the functionality looks like:

from abc import ABC, abstractmethod

class PngInterface(ABC):
    @abstractmethod
    def draw(self):
        pass

class PngImage(PngInterface):
    def __init__(self, png):
        self.png = png
        self.format = "raster"
        
    def draw(self):
        print("drawing " + self.get_image())
            
    def get_image(self):
        return "png"

But you want to expand your target audience by offering more functionality, so you decide to make your program work for vector graphics as well.

As it turns out, there's a library for working with vector graphics that you can use instead of implementing all that completely new functionality yourself. However, the classes don't conform to your interface (they don't implement the draw() method):

class SvgImage:
    def __init__(self, svg):
        self.svg = svg
        self.format = "vector"
        
    def get_image(self):
        return "svg"

You don't want to check the type of each object before doing anything with it, you'd really like to use a uniform interface - the one you already have.

To solve this problem, we implement an Adapter class.

Like real-world adapters, our class will take the externally available resource (SvgImage class) and convert it into an output that suits us.

In this case, we do this by rasterizing the vector image so that we can draw it using the same functionality we have already implemented.

Note: Again, for simplicity's sake, we'll just print out "png", though that function would draw the image in real life.

Link: For a deeper dive into the Adapter Design Patter, check out our guide, Adapter Design Pattern in Python.

Object Adapter

An Object Adapter simply wraps the external (service) class, offering an interface that conforms to our own (client) class. In this case, the service provides us with a vector graphic, and our adapter performs the rasterization and draws the resulting image:

class SvgAdapter(png_interface):
    def __init__(self, svg):
        self.svg = svg
        
    def rasterize(self):
        return "rasterized " + self.svg.get_image()
    
    def draw(self):
        img = self.rasterize()
        print("drawing " + img)

So let's test how our adapter works:

regular_png = PngImage("some data")
regular_png.draw()

example_svg = SvgImage("some data")
example_adapter = SvgAdapter(example_svg)
example_adapter.draw()

Passing the regular_png works fine for our graphic_draw() function. However, passing a regular_svg doesn't work. Upon adapting the regular_svg object, we can use the adapted form of it just as we'd use a .png image:

drawing png
drawing rasterized svg

There's no need to change anything within our graphic_draw() function. It works the same as it did before. We just adapted the input to suit the already existing function.

Class Adapter

Class Adapters can only be implemented in languages that support multiple inheritance. They inherit both our class and the external class, thereby inheriting all of their functionalities. Because of this, an instance of the adapter can replace either our class or the external class, under a uniform interface.

To allow us to do this, we need to have some way of checking whether we need to perform a transformation or not. To check this, we introduce an exception:

class ConvertingNonVector(Exception):
    # An exception used by class_adapter to check
    # whether an image can be rasterized
    pass

And with that, we can make a class adapter:

class ClassAdapter(png_image, svg_image):
    def __init__(self, image):
        self.image = image
        
    def rasterize(self):
        if(self.image.format == "vector"):
            return "rasterized " + self.image.get_image()
        else:
            raise ConvertingNonVector
        
    def draw(self):
        try:
            img = self.rasterize()
            print("drawing " + img)
        except ConvertingNonVector as e:
            print("drawing " + self.image.get_image())

To test if it works well, let's test it out on both .png and .svg images:

example_png = PngImage("some data")
regular_png = ClassAdapter(example_png)
regular_png.draw()

example_svg = SvgImage("some data")
example_adapter = ClassAdapter(example_svg)
example_adapter.draw()

Running this code results in:

drawing png
drawing rasterized svg

Object or Class Adapter?

In general, you should prefer to use Object Adapters. There are two major reasons to favor it over its class version, and those are:

  • The Composition Over Inheritance Principle ensures loose coupling. In the example above, the assumed format field doesn't have to exist for the object adapter to work, while it is necessary for the class adapter.
  • Added complexity which can lead to problems accompanying multiple inheritance.

Bridge Design Pattern

A large class may violate the The Single Responsibility Principle and it may need to be split into separate classes, with separate hierarchies. This may be further extended to a big hierarchy of classes which needs to be divided into two separate, but interdependent, hierarchies.

For example, imagine we have a class structure including medieval buildings. We have a wall, tower, stable, mill, house, armory, etc. We now wanted to differentiate them based on which materials they're made out of. We could derive every class and make straw_wall, log_wall, cobblestone_wall, limestone_watchtower, etc...

Furthermore, a tower could be extended into a watchtower, lighthouse, and castle_tower.

However, this would result in exponential growth of the number of classes if we continued to add attributes in a similar manner. Furthermore, these classes would have a lot of repeated code.

Moreover, would limestone_watchtower extend limestone_tower and add specifics of a watchtower or extend watchtower and add material specifics?

To avoid this, we'll take out the fundamental information and make it a common ground upon which we'll build variations. In our case, we'll separate a class hierarchy for Building and Material.

We'll want to have a bridge between all Building subclasses and all Material subclasses so that we can generate variations of them, without having to define them as separate classes. Since a material can be used in many things, the Building class will contain Material as one of its fields:

from abc import ABC, abstractmethod

class Material(ABC):
    @abstractmethod
    def __str__(self):
        pass
        
class Cobblestone(Material):
    def __init__(self):
        pass
    
    def __str__(self):
        return 'cobblestone'
        
class Wood(Material):
    def __init__(self):
        pass
    
    def __str__(self):
        return 'wood'

And with that, let's make a Building class:

from abc import ABC, abstractmethod       
        
class Building(ABC):
    @abstractmethod
    def print_name(self):
        pass
        
class Tower(Building):
    def __init__(self, name, material):
        self.name = name
        self.material = material
        
    def print_name(self):
        print(str(self.material) + ' tower ' + self.name)
        
class Mill(Building):
    def __init__(self, name, material):
        self.name = name
        self.material = material
        
    def print_name(self):
        print(str(self.material) + ' mill ' + self.name)

Now, when we'd like to create a cobblestone mill or a wooden tower, we don't need the CobblestoneMill or WoodenTower classes. Instead, we can instantiate a Mill or Tower and assign it any material we'd like:

cobb = Cobblestone()
local_mill = Mill('Hilltop Mill', cobb)
local_mill.print_name()

wooden = Wood()
watchtower = Tower('Abandoned Sentry', wooden)
watchtower.print_name()

Running this code would yield:

cobblestone mill Hilltop Mill
wooden tower Abandoned Sentry

Composite Design Pattern

Imagine you're running a delivery service, and suppliers send big boxes full of items via your company. You'll want to know the value of the items inside because you charge fees for high-valued packages. Of course, this is done automatically, because having to unwrap everything is a hassle.

This isn't as simple as just running a loop, because the structure of each box is irregular. You can loop over the items inside, sure, but what happens if a box contains another box with items inside? How can your loop deal with that?

Sure, you can check for the class of each looped element, but that just introduces more complexity. The more classes you have, the more edge cases there are, leading to an un-scalable system.

What's notable in problems like these is that they have a tree-like, hierarchical structure. You have the biggest box, at the top. And then you have smaller items or boxes inside. A good way to deal with a structure like this is to have the object directly above controlling the behavior of those below it.

The Composite Design Pattern is used to compose tree-like structures and treat collections of objects in a similar manner.

In our example, we could make every box contain a list of its contents, and make sure all boxes and items have a function - return_price(). If you call return_price() on a box, it loops through its contents and adds up its prices (also calculated by calling their return_price()), and if you have an item it just returns its price.

We have created a recursion-like situation where we solve a big problem by dividing it into smaller problems and invoking the same operation on them. We're, in a sense, doing a depth-first search through the hierarchy of objects.

We'll define an abstract item class, that all of our specific items inherit from:

from abc import ABC, abstractmethod

class Item(ABC):
    @abstractmethod
    def return_price(self):
        pass

Now, let's define some products that our suppliers can send via our company:

class Box(Item):
    def __init__(self, contents):
        self.contents = contents
        
    def return_price(self):
        price = 0
        for item in self.contents:
            price = price + item.return_price()
        return price

class Phone(Item):
    def __init__(self, price):
        self.price = price
        
    def return_price(self):
        return self.price

class Charger(Item):
    def __init__(self, price):
        self.price = price
        
    def return_price(self):
        return self.price

class Earphones(Item):
    def __init__(self, price):
        self.price = price
        
    def return_price(self):
        return self.price

The Box itself is an Item as well and we can add a Box instance inside a Box instance. Let's instantiate a few items and put them in a box before getting their value:

phone_case_contents = []
phone_case_contents.append(Phone(200))
phone_case_box = Box(phone_case_contents)

big_box_contents = []
big_box_contents.append(phone_case_box)
big_box_contents.append(Charger(10))
big_box_contents.append(Earphones(10))
big_box = Box(big_box_contents)

print("Total price: " + str(big_box.return_price()))

Running this code would result in:

Total price: 220

Decorator Design Pattern

Imagine you're making a video game. The core mechanic of your game is that the player can add different power-ups mid-battle from a random pool. Those powers can't really be simplified and put into a list you can iterate through, some of them fundamentally overwrite how the player character moves or aims, some just add effects to their powers, some add entirely new functionalities if you press something, etc.

You might initially think of using inheritance to solve this. After all, if you have basic_player, you can inherit blazing_player, bouncy_player, and bowman_player from it.

But what about blazing_bouncy_player, bouncy_bowman_player, blazing_bowman_player, and blazing_bouncy_bowman_player?

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

As we add more powers, the structure gets increasingly complex, we have to use multiple inheritance or repeat the code, and every time we add something to the game it's a lot of work to make it work with everything else.

The Decorator Pattern is used to add functionality to a class without changing the class itself. The idea is to create a wrapper that conforms to the same interface as the class we're wrapping but overrides its methods.

It can call the method from the member object and then just add some of its own functionality on top of it, or it can completely override it. The decorator (wrapper) can then be wrapped with another decorator, which works exactly the same.

This way, we can decorate an object as many times as we'd like, without changing the original class one bit. Let's go ahead and define a PlayerDecorator:

from abc import ABC, abstractmethod

class PlayerDecorator(ABC):
    @abstractmethod
    def handle_input(self, c):
        pass

And now, let's define a BasePlayer class, with some default behavior and its subclasses, specifying different behavior:

class BasePlayer:
    def __init__(self):
        pass
    
    def handle_input(self, c):
        if   c=='w':
            print('moving forward')
        elif c == 'a':
            print('moving left')
        elif c == 's':
            print('moving back')
        elif c == 'd':
            print('moving right')
        elif c == 'e':
            print('attacking ')
        elif c == ' ':
            print('jumping')
        else:
            print('undefined command')
            
class BlazingPlayer(PlayerDecorator):
    def __init__(self, wrapee):
        self.wrapee = wrapee
        
    def handle_input(self, c):
        if c == 'e':
            print('using fire ', end='')
        
        self.wrapee.handle_input(c)
        
class BowmanPlayer(PlayerDecorator):
    def __init__(self, wrapee):
        self.wrapee = wrapee
        
    def handle_input(self, c):
        if c == 'e':
            print('with arrows ', end='')
            
        self.wrapee.handle_input(c)
        
class BouncyPlayer(PlayerDecorator):
    def __init__(self, wrapee):
        self.wrapee = wrapee
        
    def handle_input(self, c):
        if c == ' ':
            print('double jump')
        else:
            self.wrapee.handle_input(c)

Let's wrap them one by one now, starting off with a BasePlayer:

player = BasePlayer()
player.handle_input('e')
player.handle_input(' ')

Running this code would return:

attacking 
jumping

Now, let's wrap it with another class that handles these commands differently:

player = BlazingPlayer(player)
player.handle_input('e')
player.handle_input(' ')

This would return:

using fire attacking 
jumping

Now, let's add BouncyPlayer characteristics:

player = BouncyPlayer(player)
player.handle_input('e')
player.handle_input(' ')
using fire attacking 
double jump

What's worth noting is that the player is using a fire attack, as well as double-jumping. We're decorating the player with different classes. Let's decorate it some more:

player = BowmanPlayer(player)
player.handle_input('e')
player.handle_input(' ')

This returns:

with arrows using fire attacking 
double jump

Facade Design Pattern

Say you're making a simulation of a phenomenon, perhaps an evolutionary concept like the equilibrium between different strategies. You're in charge of the back-end and have to program what specimens do when they interact, what their properties are, how their strategies work, how they come to interact with one another, which conditions cause them to die or reproduce, etc.

Your colleague is working on the graphic representation of all of this. They don't care about the underlying logic of your program, various functions that check who the specimen is dealing with, save information about previous interactions, etc.

Your complex underlying structure isn't very important to your colleague, they just want to know where each specimen is and what they are supposed to look like.

So, how do you make your complex system accessible to someone who might know little of Game Theory and less about your particular implementation of some problem?

The Facade Pattern calls for a facade of your implementation. People don't need to know everything about the underlying implementation. You can create a big class that will fully manage your complex subsystem and just provide the functionalities your user is likely to need.

In the case of your colleague, they'd probably want to be able to move to the next iteration of the simulation and get information about object coordinates and appropriate graphics to represent them.

Let's say the following code snippet is our "complex system." You can, naturally, skip reading it, as the point is that you don't have to know the details of it to use it:

class Hawk:
    def __init__(self):
        self.asset = '(`A´)'
        self.alive = True
        self.reproducing = False
    
    def move(self):
        return 'deflect'
    
    def reproduce(self):
        return hawk()
    
    def __str__(self):
        return self.asset
    
class Dove:
    def __init__(self):
        self.asset = '(๑•́ω•̀)'
        self.alive = True
        self.reproducing = False
    
    def move(self):
        return 'cooperate'
    
    def reproduce(self):
        return dove()
    
    def __str__(self):
        return self.asset
        
 def iteration(specimen):
    half = len(specimen)//2
    spec1 = specimen[:half]
    spec2 = specimen[half:]
    
    for s1, s2 in zip(spec1, spec2):
        move1 = s1.move()
        move2 = s2.move()
        
        if move1 == 'cooperate':
            # Both survive, neither reproduce
            if move2 == 'cooperate':
                pass
            # s1 dies, s2 reproduces
            elif move2 == 'deflect':
                s1.alive = False
                s2.reproducing = True
        elif move1 == 'deflect':
            # s2 dies, s1 reproduces
            if move2 == 'cooperate':
                s2.alive = False
                s1.reproducing = True
            # Both die
            elif move2 == 'deflect':
                s1.alive = False
                s2.alive = False
                
    s = spec1 + spec2
    s = [x for x in s if x.alive == True]
    
    for spec in s:
        if spec.reproducing == True:
            s.append(spec.reproduce())
            spec.reproducing = False
                
    return s

Now, giving this code to our colleagues will require them to get familiar with the inner workings before trying to visualize the animals. Instead, let's paint a facade over it and give them a couple of convenience functions to iterate the population and access individual animals from it:

import random

class Simulation:
    def __init__(self, hawk_number, dove_number):
        self.population = []
        for _ in range(hawk_number):
            self.population.append(hawk())
        for _ in range(dove_number):
            self.population.append(dove())
        random.shuffle(self.population)
            
    def iterate(self):
        self.population = iteration(self.population)
        random.shuffle(self.population)
        
    def get_assets(self):
        return [str(x) for x in population]

A curious reader can play with calling iterate() and see what happens to the population.

Flyweight Design Pattern

You're working on a video game. There are a lot of bullets in your game, and each bullet is a separate object. Your bullets have some unique info such as their coordinates and velocity, but they also have some shared information - like shape and texture:

class Bullet:
    def __init__(self, x, y, z, velocity):
        self.x = x
        self.y = y
        self.z = z
        self.velocity = velocity
        self.asset = '■■►'

Those would take up considerable memory, especially if there are a lot of bullets in the air at one time (and we won't be saving a Unicode emoticon instead of assets in real life).

It would definitely be preferable to just fetch the texture from memory once, have it in the cache, and have all the bullets share that single texture, instead of copying it dozens or hundreds of times.

If a different type of bullet was fired, with a different texture - we'd instantiate both and return them. However, if we're dealing with duplicate values - we can hold the original value in a pool/cache and just pull from there.

The Flyweight Pattern calls for a common pool when many instances of an object with the same value could exist. A famous implementation of it is the Java String Pool - where if you try to instantiate two different strings with the same value, only one is instantiated and the other one just references the first one.

Some parts of our data are unique to each individual bullet. Those are called extrinsic traits. On the other hand, data all bullets share, like the aforementioned texture and shape, are called intrinsic traits.

What we can do is separate these traits, so that intrinsic traits are all stored in a single instance - a Flyweight class. Extrinsic traits are in separate instances called Context classes. The Flyweight class usually contains all of the methods of the original class and works by passing them an instance of the Context class.

To ensure that the program works as intended, the Flyweight class should be immutable. That way, if it's invoked from different contexts, there'll be no unexpected behavior.

For practical usage, a Flyweight factory is often implemented. This is a class which, when passed an intrinsic state, checks if an object with that state already exists, and returns it if it does. If it does not, it instantiates a new object and returns it:

class BulletContext:
    def __init__(self, x, y, z, velocity):
        self.x = x
        self.y = y
        self.z = z
        self.velocity = velocity
        
 class BulletFlyweight:
    def __init__(self):
        self.asset = '■■►'
        self.bullets = []
        
    def bullet_factory(self, x, y, z, velocity):
        bull = [b for b in self.bullets if b.x==x and b.y==y and b.z==z and b.velocity==velocity]
        if not bull:
            bull = bullet(x,y,z,velocity)
            self.bullets.append(bull)
        else:
            bull = bull[0]
            
        return bull
        
    def print_bullets(self):
        print('Bullets:')
        for bullet in self.bullets:
            print(str(bullet.x)+' '+str(bullet.y)+' '+str(bullet.z)+' '+str(bullet.velocity))

We've made our contexts and flyweight. Every time we try to add a new context (bullet) through the bullet_factory() function - it generates a list of existing bullets that are essentially the same bullet. If we find such a bullet, we can just return it. If we do not, we generate a new one.

Now, with that in mind, let's use the bullet_factory() to instantiate a few bullets and print their values:

bf = BulletFlyweight()

# Adding bullets
bf.bullet_factory(1,1,1,1)
bf.bullet_factory(1,2,5,1)

bf.print_bullets()

This results in:

Bullets:
1 1 1 1
1 2 5 1

Now, let's try adding more bullets via the factory, that already exists:

# Trying to add an existing bullet again
bf.bullet_factory(1,1,1,1)
bf.print_bullets()

This results in:

Bullets:
1 1 1 1
1 2 5 1

Proxy Design Pattern

A hospital uses a piece of software with a PatientFileManager class to save data on their patients. However, depending on your access level, you may not be able to view some patient's files. After all, the right to privacy prohibits the hospital from spreading that information further than necessary for them to provide their services.

This is just one example - the Proxy Pattern can actually be used in pretty diverse circumstances, including:

  • Handling access to an object which is expensive, such as a remote server or database
  • Standing in for objects whose initialization can be expensive until they're actually needed in a program, such as textures which would take a lot of RAM space or a big database
  • Managing access for security purposes

In our hospital example, you can make another class, such as an AccessManager, which controls which users can or cannot interact with certain features of the PatientFileManager. The AccessManager is a proxy class and the user communicates with the underlying class through it.

Let's make a PatientFileManager class:

class PatientFileManager:
    def __init__(self):
        self.__patients = {}
        
    def _add_patient(self, patient_id, data):
        self.__patients[patient_id] = data
        
    def _get_patient(self, patient_id):
        return self.__patients[patient_id]

Now, let's make a proxy for that:

class AccessManager(PatientFileManager):
    def __init__(self, fm):
        self.fm = fm
    
    def add_patient(self, patient_id, data, password):
        if password == 'sudo':
            self.fm._add_patient(patient_id, data)
        else:
            print("Wrong password.")
            
    def get_patient(self, patient_id, password):
        if password == 'totallytheirdoctor' or password == 'sudo':
            return self.fm._get_patient(patient_id)
        else:
            print("Only their doctor can access this patients data.")

Here, we've got a couple of checks. If the password provided to the proxy is right, the AccessManager instance can add or retrieve patient info. If the password is wrong - it can't.

Now, let's instantiate an AccessManager and add a patient:

am = AccessManager(PatientFileManager())
am.add_patient('Jessica', ['pneumonia 2020-23-03', 'shortsighted'], 'sudo')

print(am.get_patient('Jessica', 'totallytheirdoctor'))

This results in:

['pneumonia 2020-23-03', 'shortsighted']

Note: It's important to note that Python doesn't have true private variables - the underscores are just an indication to other programmers not to touch things. So in this case, implementing a Proxy would serve more to signal your intention about access management rather than really managing access.

Conclusion

With this, all Structural Design Patterns in Python are fully covered, with working examples. A lot of programmers start using these as common-sense solutions, but knowing the motivation and the kind of problem for using some of these, you can hopefully start to recognize situations in which they may be useful and have a ready approach to solving the problem.

Last Updated: November 22nd, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms