Plato Data Intelligence.
Vertical Search & Ai.

Structural Design Patterns in Python

Date:


Overview

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

Structural Design Patterns

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 a new functionality, or you just start to notice your structures seem untidy and excessive, but all elements seem necessary.

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 article are:

Adapter

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.

Problem

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.

Solution

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.

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

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 ensuring loose coupling. In the example above, the assumed format field doesn’t have to exist for object adapter to work, while it is necessary for the class adapter.
  • Added complexity which can lead to problems accompanying multiple inheritance.

Bridge

Problem

A large class may violate 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.

bad class structure

But 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?

Solution

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 a 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 a 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

Problem

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 unscalable system.

Solution

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 boxed inside. A good way to deal with a structure like this is having 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 their 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 its 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

Problem

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?

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 into the game it’s a lot of work to make it work with everything else.

Solution

The Decorator Pattern is used to add functionality to a class without changing the class itself. The idea is to create a wrapper which 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

Problem

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 specimen do when they interact, what their properties are, how their strategies work, how do 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 how are they 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?

Solution

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 colleague 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 seeing what happens to the population.

Flyweight

Problem

You’re working on a video game. There’s 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’s 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. Though, if we’re dealing with duplicate values – we can hold the original value in a pool/cache and just pull from there.

Solution

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 that. 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 exist:

# 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

Problem

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

Solution

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']

It’s important to note here 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 a 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.

Source: https://stackabuse.com/structural-design-patterns-in-python/

spot_img

Latest Intelligence

spot_img

Chat with us

Hi there! How can I help you?