In software development , Design patterns are solutions to common problems in a specific context , These schemes have been verified .
Their main goal is to show us a good way to program and explain why other options don't work .
Use common design patterns , You can :
- Speed up the development process ;
- Reduce the number of lines of code ;
- Make sure your code is well designed ;
- Foresee the problems caused by small problems in the future .
Design patterns can significantly improve the lives of software developers , Regardless of him ( she ) What programming language is used .
I interviewed Jellyfish.tech Founder and CTO of 、 Have 9 More than years of experience Python Developers and software architects Roman Latyshenko About his top design patterns .
I mainly use Python/Django, So here is what I use every day at work Python List of design patterns in .
Behavior type
Iterator pattern
Iterators allow you to iterate over the elements of a collection without exposing internal details .
Use scenarios . Most of the time , I use it to provide a standard way to traverse a collection .
Clean client code ( Principle of single responsibility ).
You can introduce iterators into the collection without changing the client code ( to open up / Closed principle ).
Each iteration object has its own iteration state , So you can postpone and continue iterations .
Using iterators for simple collections can overload the application .
structure
Code example
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(Iterable):
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()))
The state pattern
State patterns help objects change their behavior when their internal state changes .
Use scenarios . State mode helps me
- Change a large number of object states .
- Reduce the number of lines of repetitive code in similar transitions and states .
- Avoid a large number of conditions .
Follow the principle of single responsibility : Separate the classes of code related to different states .
Adding a new state does not change the context or state of the class ( open / Closed principle ).
With little change in the state machine , There may be too many usage States .
structure
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()
Observer mode
Observers will inform them of events that occur in other objects they observe , Without having to couple to their classes .
Use scenarios . Every time I need to add a subscription mechanism to make objects subscribe / When unsubscribing notifications of events that occur for a particular publisher class , I use observer mode .
A good example is simply subscribing to news from any online magazine , You can usually choose your field of interest ( science 、 Digital technology, etc ). perhaps , Of e-commerce platform “ Let me know when you have goods ” Buttons are another example .
You don't have to change the publisher's code to add the subscriber's class .
Subscribers receive notifications in random order .
structure
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()
Structural type
Appearance mode
The facade pattern provides a simplified but limited interface to reduce the complexity of the application . Appearance mode can “ shielding ” A complex subsystem with multiple moving parts .
Use scenarios . I created the appearance pattern class , In case I have to use complex libraries and API and ( or ) I only need some of their functions .
System complexity and code separation
Use appearance mode , You can create a god object .
structure
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())
Decorator mode
Decorators attach new behaviors to objects without modifying their structure .
This pattern generates a decorator class to wrap the original class and add new functionality .
Use scenarios . Every time I need to add additional behavior to an object without entering the code , I can use decorator mode .
Change object behavior without creating subclasses .
You can combine multiple behaviors by wrapping an object into multiple decorators .
A particular decorator is difficult to remove from the wrapper stack .
structure
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()
Adapter pattern
The adapter pattern serves as an intermediate class to connect functions of independent or incompatible interfaces .
Use scenarios . Set collaboration between interfaces , I use adapter mode to solve the problem of format incompatibility .
for example , The adapter can help to XML Data format converted to JSON For further analysis .
Allows separation of interfaces from business logic .
Adding a new adapter won't break the client's code
Increase code complexity
structure
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)
Creation type
The singleton pattern
The singleton pattern limits a class to having multiple instances , And ensure that the global access point of the instance .
Use scenarios . Singleton mode helps me
- Managing shared resources : That is, a single database shared by multiple parts of the application 、 File manager or printer spooler .
- Store global status ( Help file path 、 User language 、 Application path, etc ).
- Create a simple logger.
Class has only one instance
It's hard to unit test code , Because most test frameworks are created mock Object using inheritance .
structure
Code example
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)
When to use Python Design mode of ?
When you need to work for multiple API Option provides a unified interface , Appearance mode is very useful . for example , You should integrate a payment system into your application , Leave the possibility of changing it . under these circumstances , You can use appearance mode , You just need to create a new look , Without rewriting the entire application .
If API Completely different , The problem here will arise , Because it's not easy to design a common interface for appearance patterns .
State is used to manage multiple independent components of an application , The premise is that the initial architecture means their independence . therefore , It may be a good idea to create a separate module for state management and use the observer pattern .
Due to the built-in support for decorators , Decorators are probably the most commonly used Python Pattern . for example , Decorators provide a convenient and explicit way to use some libraries , And create more and more rich opportunities for application design and management . This pattern also ensures a wide range of possibilities for function combinations , And found new opportunities for functional programming .
The adapter is suitable for processing a large number of data in different formats . This pattern allows one algorithm to be used for each data format instead of multiple algorithms .
Iterators have similar benefits , So they can be used together . Besides , One of the iterator variants called generators ( A long time ago in Python Introduction in ) Allow more efficient use of memory , It is very valuable for some types of projects when dealing with large amounts of data .
Last , The importance of the singleton pattern cannot be underestimated : Database connection 、API、 file …… These are the times when developers should know how to avoid mistakes in the process . Singleton mode can do well here , Not to mention the possibility of using the same instance every time instead of copying it to reduce memory consumption .
translate
Implementation of Top Design Patterns in Python