In software development, design patterns are proven solutions to common problems in a particular context.

Their main goal is to show us good ways to program and explain why other options don’t work.

Using common design patterns, you can:

  • Accelerate the development process;
  • Reduce lines of code;
  • Make sure your code is well designed;
  • Anticipate future problems caused by small problems.

Design patterns can significantly improve the life of a software developer, no matter what programming language he or she uses.

I spoke to _ _Roman Latyshenko, founder and CTO of Jellyfish tech, a Python developer and software architect with over nine years of experience, about his top design patterns.

I mostly use Python/Django, so here’s a list of design patterns in Python that I use every day at work.

Behavior type

Iterator pattern

Iterators allow you to traverse the elements of a collection without exposing internal details.

Usage scenarios. Most of the time, I use it to provide a standard way to traverse collections.

➕ Clean client code (single responsibility principle). ➕ can introduce iterators in collections without changing client code (open/closed principle). ➕ Each iterator has its own iteration state, so you can delay and continue iterating. ➖ Using iterators for simple collections can overload the application.

structure

Code sample

from __future__ import annotations
from collections.abc import Iterable, Iterator
from typing import Any.List


class AlphabeticalOrderIterator(Iterator) :
    _position: int = None
    _reverse: bool = False

    def __init__(self, collection: WordsCollection, 
                 reverse: bool = False) :
        self._collection = collection
        self._reverse = reverse
        self._position = -1 if reverse else 0

    def __next__(self) :
        try:
            value = self._collection[self._position]
            self._position += -1 if self._reverse else 1
        except IndexError:
            raise StopIteration()
        return value


class WordsCollection(可迭代) :
    def __init__(self, collection: List[Any] = []) :
        self._collection = collection

    def __iter__(self) -> AlphabeticalOrderIterator:
        return AlphabeticalOrderIterator(self._collection)

    def get_reverse_iterator(self) -> AlphabeticalOrderIterator:
        return AlphabeticalOrderIterator(self._collection, True)

    def add_item(self, item: Any) :
        self._collection.append(item)


if __name__ == "__main__":
    collection = WordsCollection()
    collection.add_item("First")
    collection.add_item("Second")
    collection.add_item("Third")

    print("Straight traversal:")
    print("\n".join(collection))

    print("Reverse traversal:")
    print("\n".join(collection.get_reverse_iterator()))
Copy the code

The state pattern

The state pattern helps an object change its behavior when its internal state changes.

Usage scenarios. State mode helps me

  • Changes a large number of object states.
  • Reduce the number of lines of duplicate code in similar transitions and states.
  • Avoid lots of conditions.

➕ follows the single responsibility principle: separate classes of code associated with different states. ➕ Add a new state without changing the context or state of the class (open/close principle). ➖ With little change in the state machine, using state can be too much.

structure

The sample code

from __future__ import annotations
from abc import ABC, abstractmethod


class Context(ABC) :
    _state = None
    def __init__(self, state: State) :
        self.transition_to(state)
        
    def transition_to(self, state: State) :
        print(f"Context: Transition to {type(state).__name__}")
        self._state = state
        self._state.context = self
        
    def request1(self) :
        self._state.handle1()
        
    def request2(self) :
        self._state.handle2()
        
        
class State(ABC) :
    @property
    def context(self) -> Context:
        return self._context
    
    @context.setter
    def context(self, context: Context) :
        self._context = context
        
    @abstractmethod
    def handle1(self) :
        pass
    
    @abstractmethod
    def handle2(self) :
        pass
    
    
class ConcreteStateA(State) :
    def handle1(self) :
        print("ConcreteStateA handles request1.")
        print("ConcreteStateA wants to change the state of the context.")
        self.context.transition_to(ConcreteStateB())
        
    def handle2(self) :
        print("ConcreteStateA handles request2.")
        
        
class ConcreteStateB(State) :
    def handle1(self) :
        print("ConcreteStateB handles request1.")
        
    def handle2(self) :
        print("ConcreteStateB handles request2.")
        print("ConcreteStateB wants to change the state of the context.")
        self.context.transition_to(ConcreteStateA())
        
        
if __name__ == "__main__":
    context = Context(ConcreteStateA())
    context.request1()
    context.request2()
Copy the code

Observer model

Observers are notified of events they observe in other objects without coupling to their classes.

Usage scenarios. I use the observer pattern every time I need to add a subscription mechanism to let objects subscribe/unsubscribe to notifications of events occurring in a particular publisher class.

A good example is simply subscribing to news from any online magazine, usually in a selection of areas of interest to you (science, digital technology, etc.). Or the “let me know when it’s available” button on an e-commerce platform is another example.

➕ You don’t have to change the publisher’s code to add a subscriber’s class. ➖ subscribers are notified in random order.

structure

The sample code

from __future__ import annotations
from abc import ABC, abstractmethod
from random import randrange
from typing import List


class Subject(ABC) :
    @abstractmethod
    def attach(self, observer: Observer) :
        pass
    
    @abstractmethod
    def detach(self, observer: Observer) :
        pass
    
    @abstractmethod
    def notify(self) :
        pass
    
    
class ConcreteSubject(Subject) :
    _state: int = None
    _observers: List[Observer] = []
   
    def attach(self, observer: Observer) :
        print("Subject: Attached an observer.")
        self._observers.append(observer)

    def detach(self, observer: Observer) :
        self._observers.remove(observer)
        
    def notify(self) :
        print("Subject: Notifying observers...")
        for observer in self._observers:
            observer.update(self)
            
    def some_business_logic(self) :
        print("Subject: I'm doing something important.")
        self._state = randrange(0.10)
        print(f"Subject: My state has just changed to: {self._state}")
        self.notify()
        
        
class Observer(ABC) :
    @abstractmethod
    def update(self, subject: Subject) :       
        pass
    
    
class ConcreteObserverA(Observer) :
    def update(self, subject: Subject) :
        if subject._state < 3:
            print("ConcreteObserverA: Reacted to the event")
            
            
class ConcreteObserverB(Observer) :
    def update(self, subject: Subject) :
        if subject._state == 0 or subject._state >= 2:
            print("ConcreteObserverB: Reacted to the event")
            
            
if __name__ == "__main__": subject = ConcreteSubject() observer_a = ConcreteObserverA() subject.attach(observer_a) observer_b = ConcreteObserverB()  subject.attach(observer_b) subject.some_business_logic() subject.some_business_logic() subject.detach(observer_a) subject.some_business_logic()Copy the code

structured

The appearance model

Facade mode provides a simplified but limited interface to reduce application complexity. Facade patterns can “mask” complex subsystems with multiple moving parts.

Usage scenarios. I created the appearance pattern classes in case I had to use complex libraries and apis and/or I only needed some of their functionality.

➕ System complexity separated from code ➖ Using appearance mode, you can create a God object.

structure

The sample code

class Addition:
    def __init__(self, field1: int, field2: int) :
        self.field1 = field1
        self.field2 = field2
        
    def get_result(self) :
        return self.field1 + self.field2
    
    
class Multiplication:
    def __init__(self, field1: int, field2: int) :
        self.field1 = field1
        self.field2 = field2
        
    def get_result(self) :
        return self.field1 * self.field2
    
    
class Subtraction:
    def __init__(self, field1: int, field2: int) :
        self.field1 = field1
        self.field2 = field2
        
    def get_result(self) :
        return self.field1 - self.field2
    
    
class Facade:
    @staticmethod
    def make_addition(*args) -> Addition:
        return Addition(*args)
    
    @staticmethod
    def make_multiplication(*args) -> Multiplication:
        return Multiplication(*args)
    
    @staticmethod
    def make_subtraction(*args) -> Subtraction:
        return Subtraction(*args)
    
    
if __name__ == "__main__":
    addition_obj = Facade.make_addition(5.5)
    multiplication_obj = Facade.make_multiplication(5.2)
    subtraction_obj = Facade.make_subtraction(15.5)
    print(addition_obj.get_result())
    print(multiplication_obj.get_result())
    print(subtraction_obj.get_result())
Copy the code

Decorator mode

Decorators attach new behavior to objects without modifying their structure.

This pattern generates a decorator class to wrap the original class and add new functionality.

Usage scenarios. I use decorator mode every time I need to add additional behavior to an object without going into the code.

➕ Changes the behavior of objects without creating subclasses. ➕ You can combine multiple behaviors by wrapping an object in multiple decorators. ➖ A particular decorator is difficult to remove from the wrapper stack.

structure

The sample code

class my_decorator:
    def __init__(self, func) :
        print("inside my_decorator.__init__()")
        func() # Prove that function definition has completed
        
    def __call__(self) :
        print("inside my_decorator.__call__()")
        
        
@my_decorator
def my_function() :
    print("inside my_function()")
    
    
if __name__ == "__main__":    
    my_function()
Copy the code

Adapter mode

The adapter pattern acts as a mid-tier class to connect the functionality of separate or incompatible interfaces.

Usage scenarios. To set up collaboration between interfaces, I used the adapter pattern to resolve format incompatibilities.

For example, adapters can help convert XML data formats to JSON for further analysis.

➕ allows interface separation from business logic. ➕ Adding a new adapter does not break the client code ➖ increases the code complexity

structure

The sample code

class Target:
    def request(self) :
        return "Target: The default target's behavior."
    
    
class Adaptee:
    def specific_request(self) :
        return ".eetpadA eht fo roivaheb laicepS"
    
    
class Adapter(Target, Adaptee) :
    def request(self) :
        return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"
    
    
def client_code(target: "Target") :
    print(target.request())
    
    
if __name__ == "__main__":
    print("Client: I can work just fine with the Target objects:")
    target = Target()
    client_code(target)
    adaptee = Adaptee()
    
    print("Client: The Adaptee class has a weird interface. "
          "See, I don't understand it:")
    print(f"Adaptee: {adaptee.specific_request()}")
    print("Client: But I can work with it via the Adapter:")
    
    adapter = Adapter()
    client_code(adapter)
Copy the code

Create a type

The singleton pattern

The singleton pattern limits a class to having multiple instances and ensures a global access point for that instance.

Usage scenarios. Singletons help me

  • Manage shared resources: that is, a single database, file manager, or printer spooler shared by multiple parts of the application.
  • Stores global state (help file path, user language, application path, and so on).
  • Create a simple Logger.

The ➕ class has only one instance ➖ and it is difficult to unit test your code because most testing frameworks use inheritance when creating mock objects.

structure

Code sample

class Singleton:
    def __new__(cls) :
        if not hasattr(cls, 'instance'):
            cls.instance = super(Singleton, cls).__new__(cls)
        return cls.instance
    
    
if __name__ == "__main__":
    s = Singleton()
    print("Object created:", s)
    s1 = Singleton()
    print("Object created:", s1)
Copy the code

When to use Python design patterns?

Appearance patterns are useful when you need to provide a uniform interface for multiple API options. For example, you should integrate a payment system into your application, leaving open the possibility of changing it. In this case, you can use appearance mode, where you just create a new appearance without rewriting the entire application.

This is where the problem arises if the API is completely different, because it is not easy to design a common interface for the facade pattern.

State is used to manage multiple independent components of an application, provided that the initial architecture implies their independence. Therefore, it might be a good idea to create a separate module for state management and to use the observer pattern.

Decorators are probably the most commonly used Python pattern due to built-in support for decorators. Decorators, for example, provide a convenient and unambiguous way to use certain libraries and create increasingly rich opportunities for application design and management. The pattern also ensures a wide range of possibilities for functional composition and uncovers new opportunities for functional programming.

Adapters are designed to handle large amounts of data in different formats. This pattern allows you to use one algorithm for each data format instead of multiple algorithms.

Iterators have similar benefits, so they can be used together. In addition, one of the iterator variants called generators (introduced long ago in Python) allows for more efficient use of memory, which can be valuable for certain types of projects when processing large amounts of data.

Finally, the importance of singletons cannot be overstated: database connections, apis, files… These are times when developers should be clear about how the process can avoid errors. The singleton pattern works fine here, not to mention the possibility of using the same instance each time instead of copying it to reduce memory consumption.

Translation of Top Design Patterns in Python