Design Patterns in Python
Intro
- Becasue of Python's dynamic nature, there are some differences in the implementation with comaprison to other static typed laguages for which design patterns were originally made up.
- Design patterns apply to object-oriented software design that appears in different scenarios where we model logic or problem that we try to solve.
- In other words, design patterns are abstract ideas on objects layout, how they interact with each other in a particular scenario.
- Once again, due to Pythonic nautre, some of desing patterns can't find their implementation but some of them are already embedded in Python itself.
- The best thing about them is that they provide a common language to efficiently communicate design ideas among developers.
Features
App includes following features:
Demo
We can encapsulate design patterns with 3 groups:
- Creational Desing Patterns
- Behavioral Desing Patterns
- Structural Desing Patterns
Creational Desing Patterns
Factory:
- Everything in Python is an object and can be treated equally no metther if it's a class, a function or a custom object attached to variable. They can be simply created, passed as parameter or assigned.
- Due to this fact factory pattern is not really needed. We can simply create function that accepts a class as a parameter to create a set of objets.
- Code example:
class ExampleClass: def __init__(self, x, y): self.x = x self.y = y def main(): list_of_objects = [ExampleClass(x, x*2) for x in range(10)] print(list_of_objects[2].y) -------------- # output: # 4
Singleton:
- It is a type of creational pattern that restrics to have only one object of a particular type.
- No matter how many instantiation I want to make:
s1 = Singleton()
s2 = Singleton()
there will be always one object that variabless1
ands2
points to.
Builder:
- Without builder pattern:
- Typical apporach would be creating base class and then creating a set of subclasses that extend the base class. Each subclass would bring some extenstions to fulfil its own specialized requirement.
- The danger of this approch is ending up with a big amount of subclasses and each additional requirement that comes in grows that hierarchy even more. - With Builder pattern:
Builder makes creating complex objects from a baisc object much easier
- It's very useful when we have a basic model and based on that model we want to create multiple modified models.
- For example a plain house is a basic model, however a house with swimming pool and 3 floors is a modification of the basic model of house. - Builder organizes the code into series of steps that are executed one by one when building an object. Importantly, we do not have to call all of the steps. We have to call only these steps that are necessary for a specific model of house from example above. We can call each step in the sequence we want on our own or we can use a special class called director which has a specific step sequence prepared up front.
- Code example:
from abc import ABC class Builder(ABC): @abstractproperty def product(self) -> None: pass @abstractmethod def produce_part_a(self) -> None: pass @abstractmethod def produce_part_b(self) -> None: pass @abstractmethod def produce_part_c(self) -> None: pass class ConcreteBuilder1(Builder): def __init__(self) -> None: self.reset() def reset(self) -> None: self._product = Product1() @property def product(self) -> Product1: product = self._product self.reset() return product def produce_part_a(self) -> None: self._product.add("PartA1") def produce_part_b(self) -> None: self._product.add("PartB1") def produce_part_c(self) -> None: self._product.add("PartC1")
- Builder abstract class is the interface with abstract methods which are the building steps.
- Actual builder class follows builder's interface and provides implementation for building steps:
    >  produce_part_a,
    >  produce_part_b,
    >  produce_part_c.
- Each newly-created builder instance should be a blank object which will be extending with specific building steps as per needs.
class Product1(): def __init__(self) -> None: self.parts = [] def add(self, part) -> None: self.parts.append(part) def list_parts(self) -> None: print(f"Product parts: {', '.join(self.parts)}", end="")
- Final object of Product1 class can be a basic product (no additional parts) or very extended protduct (multiple parts).
- How many parts an object inlcudes, it depends on how many and what building methods client, using the code, calls.
def main(): builder = ConcreteBuilder1() builder.produce_part_a() builder.produce_part_b() builder.product.list_parts() if __name__ == "__main__": main() ------------------ # output: # Product parts: PartA1, PartB1
- Methods (building steps) that are very specialized for that builder's objects should be declared in that builder instead of interface. It's not so good practice to keep methods in the interface that wont be used by some sub-classes.
- We can have multiple builders for building different kind of objects, as to our house example, houses can have different walls: wooden or concrete.
- We can use director class that orchestrate building specific objects by ordered sequence of steps.
Behavioral Desing Patterns
State:
- Without state pattern:
- The way withou state pattern is implementing a lot of if statements that select appropriate behaviour depending on the current object's state.
- In that case a state is basically a set of values of the object's properties.
- The biggest disadvantage of this solution is once we start adding more states and state-dependent bahaviours the code becomes diffictul to maintain.
- This is because the whole logic may require changing state condtions in every method it applies.
- And this is pretty certain that at the app's desing stage we may be not able to predict all the possible states and bahaviours. Hence, the problem gets bigger as project evolves. - With state pattern:
It allows object to alter ist behaviour when its internal state changes
- The main idea is that there is a set of finite number of predetermined states that the object can be in at any give moment. Depending on the current state, the object can be behaving differently.
- The solution that state pattern provides is to create new classes for all possible states the object can take and input all state-specific behavior into these classes.
- The main obejct, whose state we observe, stores reference to one of the state objet representing its current state and delegates state-related behaviour to that object.
- In the State pattern, the particular states are aware of each other and on states we can initiate transition to another one. - Code example:
from abc import ABC, abstractmethod class State(ABC): @abstractmethod def handle(self): pass class Context: def __init__(self, state): self.transition_to(state) def transition_to(self, state): print(f"Context: Transition to {type(state).__name__}") self._state = state # current state self._state.context = self # current context that got a specifc state def request(self): self._state.handle() class StateA(State): def handle(self): print("StateA handles request and wants to change the state of the context.") self.context.transition_to(StateB()) class StateB(State): def handle(self): print("StateB handles request and wants to change the state of the context.") self.context.transition_to(StateA()) def main(): context = Context(StateA()) context.request() context.request() if __name__ == "__main__": # The client code. main()
Structural Design Patterns
Facade:
- Facade pattern provides a single interface to more complex classes, systems, libraries or frameworks.
- The Facade delegates the client requests to the objects within the subsystem.
- Code example:
class Facade: def __init__(self, subsystem1: Subsystem1, subsystem2: Subsystem2) -> None: self._subsystem1 = subsystem1 # or Subsystem1() self._subsystem2 = subsystem2 # or Subsystem2() def operation(self) -> str: results = [] results.append("Facade initializes subsystems:") results.append(self._subsystem1.operation1()) results.append(self._subsystem2.operation1()) results.append("Facade orders subsystems to perform the action:") results.append(self._subsystem1.operation_n()) results.append(self._subsystem2.operation_z()) return "\n".join(results) class Subsystem1: def operation1(self) -> str: return "Subsystem1: Ready!" def operation_n(self) -> str: return "Subsystem1: Go!" class Subsystem2: def operation1(self) -> str: return "Subsystem2: Ready!" def operation1(self) -> str: return "Subsystem2: Go!" def main(): subsystem1 = Subsystem1() subsystem2 = Subsystem2() facade = Facade(subsystem1, subsystem2) facade.operation() if __name__ == "__main__": main() ------------------ # output: # Facade initializes subsystems: # Subsystem1: Ready! # Subsystem2: Ready! # Facade orders subsystems to perform the action: # Subsystem1: Go! # Subsystem2: Go!
- The Subsystem can accept requests either from the facade or client directly.
- The Facade is yet another client, and it's not a part of the Subsystem.
- Client in main function passes objects: subsystem1 and subsysem2 into facade.
- Facade as middleman includes all methods that can be pefromed on both subsystems - they are groupped in facade's operation method.
Setup
No specific installation required.