Detailed explanation of design pattern and Python implementation

Design pattern and Python implementation

What is the design pattern?

Christopher Alexander: "each pattern describes a recurring problem around us and the core of the solution to the problem * so that you can use the solution again and again without having to do repetitive work."

Design patterns are summarized and optimized to provide reusable solutions to some programming problems we often encounter. A design pattern does not directly affect our code like a class or a library. On the contrary, design pattern is more advanced. It is a method template that must be implemented in a specific situation. Design patterns do not bind to specific programming languages. A good design pattern should be able to be implemented in most programming languages (if not all, it depends on the language characteristics). Most importantly, design pattern is also a double-edged sword. If design pattern is used in inappropriate situations, it will cause disaster and endless trouble. However, if the design pattern is used in the right place at the right time, it will be your Savior.

At first, you will think that "pattern" is a wise move specially designed to solve a specific kind of problem. That's right. It seems that it is indeed the most general and flexible solution formed by many people working together and looking at problems from different angles. You may have seen or solved these problems, but your solution is likely not as complete as the model.

Although it is called "design pattern", However, they are not closely related to the field of "design". Design patterns are different from analysis, design and implementation in the traditional sense. In fact, design patterns root a complete concept in the program, so they may appear in the analysis stage or the higher design stage. It is interesting that because the specific embodiment of design patterns is program code, you may think that they will not appear before the specific implementation stage (in fact, you didn't realize that you were using specific design patterns before entering the specific implementation stage).

Patterns can be understood through the basic concepts of programming: adding an abstraction layer. Abstract a thing is to isolate any specific details. The purpose of doing so is to separate those unchanged core parts from other details. When you find that some parts of your program are often changed for some reason, and you don't want these changed parts to cause changes in other parts, you need to think about the design methods that won't change. This will not only make the code more maintainable, but also make the code easier to understand, thus reducing the development cost.

Three basic design modes:

  1. Creation mode: provides instantiation methods and corresponding object creation methods for appropriate situations.
  2. Structured pattern: it is usually used to deal with the relationship between entities, so that these entities can work together better.
  3. Behavior mode: used to communicate between different entities, providing easier and more flexible communication methods for the communication between entities.

Six principles of design mode

  • Opening and closing principle: a software entity such as class, module and function should be open to extension and closed to modification. That is, the software entity should try to expand without modifying the original code.
  • Liskov Substitution Principle: all references to the base class (parent class) must be able to use the objects of its subclasses transparently.
  • Dependency Inversion Principle: high-level modules should not rely on low-level modules, and both should rely on their abstraction; Abstractions should not rely on details; Details should rely on abstraction. In other words, program for the interface, not for the implementation.
  • Interface isolation principle: use multiple special interfaces instead of a single general interface, that is, the client should not rely on those interfaces it does not need.
  • Dimitri's Law: a software entity should interact with other entities as little as possible.
  • Single responsibility principle: there should be no more than one reason for class change. Generally speaking, a class is only responsible for one responsibility.

Interface

Interface: a special class that declares several methods. It is required that the class that inherits the interface must implement these methods.
Function: limit the name and calling method of the method of the class inheriting the interface; Hides the internal implementation of the class.

An interface is an abstract base class (parent class), which restricts that the class that inherits it must implement some methods defined in the interface.

Python uses the abstract classes and methods of ABCMeta and abstractmethod to realize the functions of the interface. The interface class defines a method, which is not specifically implemented. It is restricted that the subclass must have this method. Implement specific functions in interface subclasses.

# Use abstract classes and methods for abstraction
from abc import ABCMeta
from abc import abstractmethod  # Import abstract method


class Father(metaclass=ABCMeta):  # Create abstract class
    
    @abstractmethod
    def f1(self):
        pass

    @abstractmethod
    def f2(self):
        pass

class F1(Father):
    def f1(self):
        pass

    def f2(self):
        pass

    def f3(self):
        pass

obj = F1()

Error report definition interface

class Interface:
    def method(self, arg):
        raise NotImplementedError

1, Type creation mode

The creation pattern can provide developers with a better way to instantiate objects when it is inconvenient to directly use the class construction method to instantiate objects.

1. Simple factory mode

Content: instead of directly exposing the implementation details of object creation to the client, a factory class is responsible for creating an instance of the product class.

Role:

  • Factory role (Creator)
  • Abstract Product role
  • Concrete Product role

advantage:

  • Hides the implementation details of object creation
  • The client does not need to modify the code

Disadvantages:

  • Violating the principle of single responsibility, multiple creation logic are implemented in a factory class
  • When adding new products, the factory code needs to be modified, which violates the opening and closing principle

PaymentFactory simple factory

from abc import abstractmethod, ABCMeta


class Payment(metaclass=ABCMeta):
    @abstractmethod
    def pay(self, money):
        pass


class Alipay(Payment):
    def __init__(self, enable_yuebao=False):
        self.enable_yuebao = enable_yuebao

    def pay(self, money):
        if self.enable_yuebao:
            print("Yu'e Bao payment%s element" % money)
        else:
            print("Alipay payment%s element" % money)


class ApplePay(Payment):
    def pay(self, money):
        print("Apple pay%s element" % money)


class PaymentFactory:
    def create_payment(self, method):
        if method == "alipay":
            return Alipay()
        elif method == 'yuebao':
            return Alipay(enable_yuebao=True)
        elif method == "applepay":
            return ApplePay()
        else:
            raise NameError(method)


f = PaymentFactory()
p = f.create_payment("yuebao")
p.pay(100)

2. Factory Method

Content: define an interface (factory interface) for creating objects, and let subclasses decide which product class to instantiate.

Role:

  • Abstract factory role (Creator)
  • Specific factory role (Concrete Creator)
  • Abstract Product role
  • Concrete Product role

Compared with the simple factory mode, the factory method mode corresponds each specific product to a specific factory.

Applicable scenarios:

  • When it is necessary to produce a variety and a large number of complex objects.
  • When the coupling needs to be reduced.
  • When the product categories in the system need to be expanded frequently.

advantage:

  • Each specific product corresponds to a specific factory class, and there is no need to modify the factory class code
  • Hides the implementation details of object creation

Disadvantages:

  • Every time a specific product category is added, a corresponding specific factory category must be added

Factory method

from abc import abstractmethod, ABCMeta


class Payment(metaclass=ABCMeta):
    @abstractmethod
    def pay(self, money):
        pass


class Alipay(Payment):
    def pay(self, money):
        print("Alipay payment%s element" % money)


class ApplePay(Payment):
    def pay(self, money):
        print("Apple pay%s element" % money)


class PaymentFactory(metaclass=ABCMeta):
    @abstractmethod
    def create_payment(self):
        pass


class AlipayFactory(PaymentFactory):
    def create_payment(self):
        return Alipay()


class ApplePayFactory(PaymentFactory):
    def create_payment(self):
        return ApplePay()


af = AlipayFactory()
ali = af.create_payment()
ali.pay(120)

3. Abstract Factory method

Content: define a factory class interface and let the factory subclass create a series of related or interdependent objects.
For example, the production of a mobile phone requires the assembly of three types of objects: mobile phone shell, CPU and operating system, each of which has different types. For each specific factory, the corresponding one of the three objects required to produce a mobile phone.
Role:

  • Abstract factory role (Creator)
  • Specific factory role (Concrete Creator)
  • Abstract Product role
  • Concrete Product role
  • Client

Compared with the factory method pattern, each specific factory in the abstract factory pattern produces a set of products.
Applicable scenarios:

  • When the system is to be independent of the creation and combination of products
  • Emphasize the design of a series of related product objects for joint use
  • Provide a product class library to hide the specific implementation of the product

advantage:

  • Separate the client from the concrete implementation of the class
  • Each factory creates a complete product family, making it easy to exchange product families
  • It is conducive to the consistency of products (i.e. the constraint relationship between products)

Disadvantages:

  • It is difficult to support new kinds of (Abstract) products

Abstract factory

from abc import abstractmethod, ABCMeta


# ------Abstract product------
class PhoneShell(metaclass=ABCMeta):
    @abstractmethod
    def show_shell(self):
        pass


class CPU(metaclass=ABCMeta):
    @abstractmethod
    def show_cpu(self):
        pass


class OS(metaclass=ABCMeta):
    @abstractmethod
    def show_os(self):
        pass


# ------Abstract factory------

class PhoneFactory(metaclass=ABCMeta):
    @abstractmethod
    def make_shell(self):
        pass

    @abstractmethod
    def make_cpu(self):
        pass

    @abstractmethod
    def make_os(self):
        pass


# ------Specific products------

class SmallShell(PhoneShell):
    def show_shell(self):
        print("Ordinary mobile phone small mobile phone case")


class BigShell(PhoneShell):
    def show_shell(self):
        print("Large case of ordinary mobile phone")


class AppleShell(PhoneShell):
    def show_shell(self):
        print("Apple mobile phone shell")


class SnapDragonCPU(CPU):
    def show_cpu(self):
        print("snapdragon  CPU")


class MediaTekCPU(CPU):
    def show_cpu(self):
        print("MediaTek CPU")


class AppleCPU(CPU):
    def show_cpu(self):
        print("Apple CPU")


class Android(OS):
    def show_os(self):
        print("Android system")


class IOS(OS):
    def show_os(self):
        print("iOS system")


# ------Specific factory------

class MiFactory(PhoneFactory):
    def make_cpu(self):
        return SnapDragonCPU()

    def make_os(self):
        return Android()

    def make_shell(self):
        return BigShell()


class HuaweiFactory(PhoneFactory):
    def make_cpu(self):
        return MediaTekCPU()

    def make_os(self):
        return Android()

    def make_shell(self):
        return SmallShell()


class IPhoneFactory(PhoneFactory):
    def make_cpu(self):
        return AppleCPU()

    def make_os(self):
        return IOS()

    def make_shell(self):
        return AppleShell()


# ------Client------

class Phone:
    def __init__(self, cpu, os, shell):
        self.cpu = cpu
        self.os = os
        self.shell = shell

    def show_info(self):
        print("text message:")
        self.cpu.show_cpu()
        self.os.show_os()
        self.shell.show_shell()


def make_phone(factory):
    cpu = factory.make_cpu()
    os = factory.make_os()
    shell = factory.make_shell()
    return Phone(cpu, os, shell)


p1 = make_phone(HuaweiFactory())
p1.show_info()

4. Builder mode

Content: separate the construction of a complex object from its representation, so that the same construction process can create different representations.
Role:

  • Abstract Builder
  • Concrete Builder
  • Director
  • Product

Similar to the abstract factory pattern, the builder pattern is also used to create complex objects. The main difference is that the builder pattern focuses on constructing a complex object step by step, while the abstract factory pattern focuses on multiple series of product objects.

Applicable scenarios:

  • When the algorithm (Director) for creating a complex object should be independent of the components of the object and their assembly method (Builder)
  • When the construction process allows the constructed object to have different representations (different Builder s).

advantage:

  • It hides the internal structure and assembly process of a product
  • Separate construction code from presentation code
  • The construction process can be more finely controlled

Builder pattern

from abc import abstractmethod, ABCMeta

# ------Products------

class Player:
    def __init__(self, face=None, body=None, arm=None, leg=None):
        self.face = face
        self.arm = arm
        self.leg = leg
        self.body = body

    def __str__(self):
        return "%s, %s, %s, %s" % (self.face, self.arm, self.body, self.leg)


# ------Builder------

class PlayerBuilder(metaclass=ABCMeta):
    @abstractmethod
    def build_face(self):
        pass

    @abstractmethod
    def build_arm(self):
        pass

    @abstractmethod
    def build_leg(self):
        pass

    @abstractmethod
    def build_body(self):
        pass

    @abstractmethod
    def get_player(self):
        pass


class BeautifulWomanBuilder(PlayerBuilder):
    def __init__(self):
        self.player = Player()

    def build_face(self):
        self.player.face = "Pretty Face "

    def build_arm(self):
        self.player.arm = "Thin arm"

    def build_body(self):
        self.player.body = "slender waist"

    def build_leg(self):
        self.player.leg = "Long legged"

    def get_player(self):
        return self.player


class PlayerDirector:
    def build_player(self, builder):
        builder.build_body()
        builder.build_arm()
        builder.build_leg()
        builder.build_face()
        return builder.get_player()


director = PlayerDirector()
builder = BeautifulWomanBuilder()
p = director.build_player(builder)
print(p)

5. Singleton mode

Content: ensure that a class has only one instance, and provide a global access point to access it.
Role:

  • Singleton

Applicable scenario

  • When a class can only have one instance and clients can access it from a well-known access point

  • Singleton mode can also be used to restrict access to shared resources or functions in applications

    For example, database connection objects or log recording objects can be created in singleton mode and used globally, which not only avoids the waste of resources each time the object is re instantiated, but also facilitates the overall management of the application

advantage:

  • Controlled access to unique instances
  • A singleton is equivalent to a global variable, but prevents namespace contamination.

Concepts similar to the function of singleton mode: global variable, static variable (method)

Common misunderstandings and precautions

First of all, in order to ensure that there is only one object of a class in the application, we should not only restrict the instantiation process, but also prohibit the copying of such objects

In addition, a big mistake in using singleton mode in Python is to rely on rewriting__ new__ Method to implement the singleton mode This way of writing may not be a problem in general use, but there will be some unexpected effects when it is necessary to inherit and extend classes using singleton mode Although singleton classes should not be extended by inheritance, especially multi-layer inheritance, understanding possible errors and how to avoid them can make the code more robust

Sample code

1, Imperfect writing (rewrite _ new method)
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance


class Child(Singleton):
    pass


def identity_test():
    s1 = Singleton()
    s2 = Singleton()
    print(s1 is s2)  # True
    exit(0)


def create_parent_first():
    s1 = Singleton()
    c1 = Child()
    print(c1 is s1)  # True
    exit(0)


def create_child_first():
    c1 = Child()
    s1 = Singleton()
    print(c1 is s1)  # False
    exit(0)

From the execution results of the above example code, we can see that the rewriting__ new__ Some problems when the class implementing singleton mode is inherited:

  • When both parent and child classes are needed in the system, if the parent class is instantiated first and then the child class is instantiated, they actually point to the same object, which is obviously unreasonable
  • When the subclass is instantiated first and then the parent class is instantiated, they do not point to the same object In complex systems, it may be uncertain which parent and child classes are instantiated first, which brings unknown hidden dangers, and the bug s caused by this problem are also very difficult to check

Note: create is executed continuously in the interactive interpreter_ parent_ First () and create_child_first(), when executing the second function, the result of error will be displayed because there are already instantiated objects in memory

2, Perfect writing

1. The correct way to write a singleton pattern is to use metaclasses First define a singleton metaclass, and then specify the singleton metaclass as the metaclass when declaring the class that needs to use the singleton pattern

class SingletonType(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]
    
    
class DatabaseConnector(metaclass=SingletonType):
    pass


class MySQLConnector(DatabaseConnector):
    pass


def identity_test():
    d1 = DatabaseConnector()
    d2 = DatabaseConnector()
    m1 = MySQLConnector()
    m2 = MySQLConnector()
    print(d1 is d2) # True
    print(m1 is m2) # True
    exit(0)
    
    
def create_parent_first():
    d = DatabaseConnector()
    m = MySQLConnector()
    print(d is m) # False
    exit(0)
    
    
def create_child_first():
    m = MySQLConnector()
    d = DatabaseConnector()
    print(d is m) # False
    exit(0)

It can be seen from the execution results that the problems in the wrong writing do not exist when using metaclass to implement singleton. The subclass and parent class always point to different singleton objects, which is not affected by the instantiation order

2. Decorator method

def singleton(cls, *args, **kw):
    instances = {}

    def get_instance():
        if cls not in instances:
            instances[cls] = cls(*args, **kw)
        return instances[cls]

    return get_instance


@singleton
class MyClass2:
    a = 1


one = MyClass2()
two = MyClass2()

print(id(one))  # 31495472
print(id(two))  # 31495472
print(one == two)
print(one is two)

Python developers' different views on singleton mode

Singleton pattern is a controversial design pattern in Python Many developers think that the writing method of using singleton mode to ensure the uniqueness of some things in the program is too cumbersome. In Python, we can use some language features to realize similar functions in a more natural and python style writing method

At the same time, we should use or avoid singleton mode in some scenarios, such as distributed computing, automated testing, etc Using singleton mode in these scenarios may have negative effects

Whether to use singleton mode or not and how to use singleton mode depends on the specific situation In my opinion, the important thing is not the implementation method, but the idea of singleton mode behind the implementation method

Method to replace singleton mode in Python -- > Borg

Borg (also known as Monostate) is a writing method proposed by Alex Martelli to realize the behavior similar to singleton mode. It is not one of the traditional design patterns

The core idea of this writing method is: it does not limit the number of instances of a class, as long as the states of all instances of the class are shared, it can also achieve unity similar to that in the singleton mode

The concrete implementation is that when using Borg to instantiate a new object, all objects share one__ dict__.

Borg can also be used to solve the inheritance problem in the wrong implementation But Borg's writing also has its own hidden dangers When overridden in subclasses that inherit Borg__ getattr__ There may be a problem with the method

class Borg:
    _state = {}
    
    
    def __new__(cls, *args, **kwargs):
        obj = super().__new__(cls, *args, **kwargs)
        obj.__dict__ = cls._state
        return obj
Borg example
class DatabaseConnector:
    _state = {}
    
    
    def __new__(cls, db, *args, **kwargs):
        obj = super().__new__(cls, *args, **kwargs)
        obj.__dict__ = cls._state
        return obj
    
    
    def __init__(self, db):
        self.db = db
        
        
if __name__ == "__main__":
    d1 = DatabaseConnector("MySQL")
    d2 = DatabaseConnector("PostgreSQL")
    print(d1.db) # PostgreSQL
    print(d2.db) # PostgreSQL

Using modules to implement singleton mode

Modules in Python can be used to implement singleton mode instead of the above-mentioned writing methods, because modules in Python are singleton

The most common usage is to instantiate the object that needs to be used as a singleton in the module and assign it to the variable of the whole module. Then the developer can directly import this object from the module for use when needed instead of manually instantiating it

This is not a safe way to write, because we can re assign the variable pointing to the singleton object at any time, or we can still instantiate the object directly

But from another point of view, this approach also makes our code more flexible. We can write document comments to remind other developers that some objects should be used as single instances, but when they have other requirements, they can also directly instantiate and create multiple objects.

# Python's modules are natural singleton patterns.
# module_name.py
class MySingleton(object):
    def foo(self):
        print('danli')

        
my_singleton = MySingleton()

# to use
from .module_name import my_singleton
my_singleton.foo()

print(id(my_singleton))

from .module_name import my_singleton
my_singleton.foo()

print(id(my_singleton))

6. Prototype

Content:

Specify the type of objects to be created with prototype instances, and create new objects by copying these prototypes. In Python, the simplest prototype mode can be implemented using the deepcopy function in the copy module, which takes an object as a parameter and then returns the copy object of this object. It is precisely because copying is a built-in function in Python, which is very convenient and natural to use, so it is less known as a mode when used.

Usage scenario:

  • Through dynamic loading;
  • In order to avoid creating a factory class level parallel to the product class level;
  • When an instance of a class can only have one of several different state combinations. Building a corresponding number of prototypes and cloning them may be more convenient than manually instantiating the class with the appropriate state each time.

  • When there is an object in the system, if we need to modify some of its properties, but we don't want to modify it directly, we can use the prototype mode. Create a copy of an existing object as a prototype, and then modify and use the copied object.

    For example, in Django, GET and POST in the HttpRequest object are QueryDict type objects, which are immutable by default. As shown in the figure:

    Although QueryDict is set to be variable when it is created in the initialization method of HttpRequest, modify request. In the view An error will be reported during get.

    So when we need to modify request When you get, you can create a copy. The copied object is variable. We can modify the copied object and use it. A copy can be created using the copy method of the QueryDict object. As can be seen from the screenshot of the source code, the copy ` method is internally called by the Python built-in deep copy.

Prototype pattern example code:

import copy


class Prototype:
    def __init__(self):
        self._objects = {}

    def register_object(self, name, obj):
        """Register an object"""
        self._objects[name] = obj

    def unregister_object(self, name):
        """Unregister an object"""
        del self._objects[name]

    def clone(self, name, **attr):
        """Clone a registered object and update inner attributes dictionary"""
        obj = copy.deepcopy(self._objects.get(name))
        obj.__dict__.update(attr)
        return obj


def main():
    class A:
        def __str__(self):
            return "I am A"

    a = A()
    prototype = Prototype()
    prototype.register_object('a', a)
    b = prototype.clone('a', a=1, b=2, c=3)

    print(a)
    print(b.a, b.b, b.c)


if __name__ == '__main__':
    main()

Summary of creative mode

Designs using Abstract Factory, Prototype, or Builder are even more flexible than those of Factory Method, but they are also more complex. Typically, design begins with using the Factory Method. And when the designer finds that more flexibility is needed, the design will create fireworks like other patterns. When you weigh between design criteria, understanding multiple patterns can give you more choices.

Inheritance dependent creation pattern: Factory Method Pattern

Composition dependent creative patterns: abstract factory pattern, creator pattern

2, Structural model

These design patterns are very important in large-scale applications. They provide developers with a method to divide the code organization structure and how to make all parts of the application work together.

1. Adapter Class/Object

Content: convert the interface of a class into another interface desired by the customer. The adapter pattern allows classes that cannot work together because of interface incompatibility to work together.
Role:

  • Target interface
  • Class to be adapted (Adaptee)
  • Adapter

Two implementation methods:

  • Adapter: using multiple classes
  • Object adapters: using combinations

Applicable scenarios:

  • Want to use an existing class, and its interface does not meet your requirements
  • (object adapter) wants to use some existing subclasses, but it is impossible to subclass each to match their interface. An object adapter can adapt its parent class interface.

Class adapter:

  • Use a specific Adapter class to match Adaptee and Target. The result is that when we want to match a class and all its subclasses, class Adaptee will not be competent.
  • This allows the Adapter to redefine part of the behavior of the Adaptee, because the Adapter is a subclass of the Adaptee.
  • Only one object is introduced, and no additional pointer is required to get Adaptee indirectly.

Object adapter:

  • Allow an Adapter to work with multiple adaptees -- that is, the Adaptee itself and all its subclasses (if any) at the same time. The Adapter can also add functions to all adapters at once.
  • This makes it difficult to redefine the behavior of Adaptee. This requires generating a subclass of Adaptee and making the Adapter reference this subclass instead of the Adaptee itself.

Adapter mode example code:

from abc import abstractmethod, ABCMeta


class Payment(metaclass=ABCMeta):
    @abstractmethod
    def pay(self, money):
        raise NotImplementedError


class Alipay(Payment):
    def pay(self, money):
        print("Alipay payment%s element" % money)


class ApplePay(Payment):
    def pay(self, money):
        print("Apple pay%s element" % money)


# ------Class to be adapted------

class WechatPay:
    def cost(self, money):
        print("Wechat payment%s element" % money)


# Class Adapter 
class RealWechatPay(WechatPay, Payment):
    def pay(self, money):
        return self.cost(money)


# object adapter 
class RealWechatPay2(Payment):
    def __init__(self):
        self.payment = WechatPay()

    def pay(self, money):
        return self.payment.cost(money)


p = RealWechatPay2()
p.pay(111)

2. Composite mode

Content: combine objects into a tree structure to represent the "part whole" hierarchy. The combination mode enables users to use single objects and combined objects consistently.
Role:

  • Abstract Component
  • Leaf assembly
  • Composite
  • Client

Applicable scenarios:

  • In particular, the "hierarchy" of objects
  • Users are expected to ignore the difference between composite objects and single objects, and use all objects in the composite structure uniformly

advantage:

  • Defines a class hierarchy containing basic objects and composite objects
  • Simplify client code, that is, clients can use composite objects and single objects consistently
  • Easier to add new types of components

Disadvantages:

  • It is difficult to limit the components in a composition

Combined mode example code:

from abc import abstractmethod, ABCMeta


class Graphic(metaclass=ABCMeta):
    
    @abstractmethod
    def draw(self):
        pass

    @abstractmethod
    def add(self, graphic):
        pass

    def getchildren(self):
        pass

    
# Primitives
class Point(Graphic):
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def draw(self):
        print(self)

    def add(self, graphic):
        raise TypeError

    def getchildren(self):
        raise TypeError

    def __str__(self):
        return "spot(%s, %s)" % (self.x, self.y)


class Line(Graphic):
    
    def __init__(self, p1, p2):
        self.p1 = p1
        self.p2 = p2

    def draw(self):
        print(self)

    def add(self, graphic):
        raise TypeError

    def getchildren(self):
        raise TypeError

    def __str__(self):
        return "line segment[%s, %s]" % (self.p1, self.p2)


class Picture(Graphic):
    
    def __init__(self):
        self.children = []

    def add(self, graphic):
        self.children.append(graphic)

    def getchildren(self):
        return self.children

    def draw(self):
        print("------Composite graphics------")
        for g in self.children:
            g.draw()
        print("------END------")


pic1 = Picture()
point = Point(2,3)
pic1.add(point)
pic1.add(Line(Point(1,2), Point(4,5)))
pic1.add(Line(Point(0,1), Point(2,1)))

pic2 = Picture()
pic2.add(Point(-2,-1))
pic2.add(Line(Point(0,0), Point(1,1)))

pic = Picture()
pic.add(pic1)
pic.add(pic2)

pic.draw()
#pic1.draw()
#point.draw()

3. Decoration mode

Content:

Decoration mode allows us to use other objects to wrap objects that provide core functions, so as to provide additional functions or modify the behavior and function of the original object The decorated object should have the same interface with the original object, which allows us to choose whether to use the decorated object or the original object without affecting other parts of the code Decoration can be nested, and the core function is provided by the innermost object

Decoration mode and Python decorator:

First of all, it should be clear that decoration mode is a different concept from decorator in Python

This article only discusses decoration mode, and will also organize and publish Python decorator related articles for your reference in the future

Decoration pattern is a design pattern independent of specific language, while the decorator in Python language is a callable object, which receives a function as a parameter and returns a function as a result Generally, the returned function will be re assigned instead of the original function to extend the function, method or class

Functions in Python are also objects. Using decorator decorating functions in Python is actually applying decoration mode Python developers often use this usage, so that Python provides @ decorator syntax sugar for decorators to simplify this writing method

When decorating a function, it is usually to permanently expand or modify the function of the function, so it is decided whether to use the decorator when the function is defined rather than at run time The advantage of using syntax sugar is that you can clearly see that the function is decorated Syntax sugar can only be used for our own code. When we want to decorate the third-party code, we can only use the way of re assignment because we can't modify the source code

Applicable scenarios:

When you need to expand some general functions, you can consider using decoration mode, such as:

  • Verification (data, authority)
  • cache
  • Log
  • encryption
  • Program debugging

Django provides many view decorators, some of which can be used to verify the access rights of users or cache views to improve the efficiency of processing responses

Decoration mode and inheritance:

The following discussion applies to single inheritance and multiple inheritance

Because decoration mode can also add additional functions to objects, sometimes decoration mode can be used instead of inheritance

Compared with using inheritance, decoration mode has two major advantages. In other words, decoration mode should be preferred only when the following two conditions are met:

  • You can dynamically decide whether to decorate the function of the extended object according to some conditions
  • The same function can have multiple optional extensions

Decoration mode example code:

In the following simple example, the behavior of the final adder object will also change according to the value of debug and log conditions When both values are True, the final adder object is an object decorated with two layers. When the add method is called, it will be printed in the console and written to the log file

If we only need the additional function of writing logs, that is, we only need the LogAdder class, then the LogAdder class can also be implemented by inheriting the Adder class As mentioned above, when there are multiple optional extensions and / or the need to dynamically decide whether to use the extension function, it is easier to use the decoration mode

class Adder:
    def add(self, a, b):
        return a + b
    
    
class PrintAdder:
    def __init__(self, adder):
        self.adder = adder
    def add(self, a, b):
        res = self.adder.add(a, b)
        print(f"{a} + {b} = {res}")
        return res
    
    
class LogAdder:
    def __init__(self, adder):
        self.adder = adder
    def add(self, a, b):
        res = self.adder.add(a, b)
        with open("adder.log", "w") as f:
            f.write(f"{a} + {b} = {res}\n")
        return res
    
    
if __name__ == "__main__":
    debug = True
    log = True
    adder = Adder()
    if debug:
        adder = PrintAdder(adder)
    if log:
        adder = LogAdder(adder)
    adder.add(1, 1)
    print("from log: ", end="")
    with open("adder.log", "r") as f:
        print(f.read())
    """
    1 + 1 = 2
    from log: 1 + 1 = 2
    """

The above example is mainly to show the idea of decoration mode. In Python, you can use decorator to complete the above example more simply, and it is generally recommended to use that method

4. Bridging mode

Content:

Bridge mode looks very similar to adapter mode. The difference is that adapter mode is used to make existing code work together, while bridge mode is used before writing other code

Bridging mode allows us to separate the abstract part from the concrete implementation part and replace inheritance with composition, which makes it easier to adjust and expand the abstract part or implementation part later

There are two main types of roles in the bridging mode in Python:

  • Concrete implementation role: implement the interface called in the abstract role
  • Abstract role: it contains a concrete implementation role, and the business logic is realized by calling the interface it implements

When necessary, you can also define four types of roles according to the traditional bridging mode (refer to http://c.biancheng.net/view/1364.html):

  • Abstraction role
  • Extended abstraction role
  • Implementer role
  • Concrete implementer role

The above content may not be easy to understand. It's easy to understand by looking at the sample code

Applicable scenarios:

You can use bridge mode when you want to share the implementation among multiple objects

Bridge mode example code:

IOPrinter in the following code is an abstract role. It currently has two functions: reading all data from the specified I/O or previewing part of the I/O The realization of these two functions is shared by all concrete roles

StdinReader is a concrete implementation role. It internally implements the read() method that the abstract role needs to call. At the same time, there is also a read() method in Python's built-in file object, so it can also be used as a concrete implementation

class IOPrinter:
    
    def __init__(self, reader):
        self.reader = reader
        
    def print_all(self):
        print(self.reader.read())
        
    def preview(self):
        print(self.reader.read()[:5], "...")
        
        
class StdinReader:
    def read(self):
        return input("stdin: ")
    
    
if __name__ == "__main__":
    with open("file.txt", "r") as f:
        printer = IOPrinter(f)
        printer.print_all()
    printer = IOPrinter(StdinReader())
    printer.preview()

This code also shows the program extensibility brought by using the bridge mode. We can add new functions by extending the abstract role, such as adding reverse printing. At the same time, we can also add new functions by creating a new concrete implementation role, such as reading data from the network or database

5. Proxy mode

Content: provides a proxy for other objects to control access to this object.
Role:

  • Abstract entity (Subject)
  • Entity (RealSubject)
  • Proxy

Applicable scenarios:

  • Remote proxy: provides a proxy for remote objects
  • Virtual proxies: create large objects as needed
  • Protection agent: controls access to the original object. It is used when the object has different access rights

advantage:

  • Remote proxy: you can hide the fact that the object is in the remote address space
  • Virtual proxy: it can be optimized, such as creating objects according to requirements
  • Protection agent: allows some additional housekeeping when accessing an object

Proxy mode example code:

from abc import ABCMeta, abstractmethod


class Subject(metaclass=ABCMeta):
    @abstractmethod
    def get_content(self):
        pass

    def set_content(self, content):
        pass


class RealSubject(Subject):
    def __init__(self, filename):
        self.filename = filename
        print("read%s Document content" % filename)
        f = open(filename)
        self.__content = f.read()
        f.close()

    def get_content(self):
        return self.__content

    def set_content(self, content):
        f = open(self.filename, 'w')
        f.write(content)
        self.__content = content
        f.close()


# ---Remote agent

class ProxyA(Subject):
    def __init__(self, filename):
        self.subj = RealSubject(filename)

    def get_content(self):
        return self.subj.get_content()

    def set_content(self, content):
        return self.subj.set_content(content)


# ---Virtual agent

class ProxyB(Subject):
    def __init__(self, filename):
        self.filename = filename
        self.subj = None

    def get_content(self):
        if not self.subj:
            self.subj = RealSubject(self.filename)
        return self.subj.get_content()


x = ProxyB('abc.txt')
# print(x.get_content())

# ---Protection agent

class ProxyC(Subject):
    def __init__(self, filename):
        self.subj = RealSubject(filename)

    def get_content(self):
        self.subj.get_content()

    def set_content(self, content):
        raise PermissionError

# filename = "abc.txt"
# username = input()
# if username!="alex":
#     p = ProxyC(filename)
# else:
#     p = ProxyA(filename)
#
# print(p.get_content())


6. MVC mode

Content:

MVC is actually an architecture pattern rather than a design pattern. The difference between the two is that the former is more widely used Because this pattern is too important and related to design pattern, we decided to discuss it together

MVC pattern is an embodiment of the application of SoC(Separation of Concerns) principle in software design in object-oriented programming pattern SoC principle can be roughly understood as: divide the program into different parts, each part has its own clear division of labor, and each part only focuses on its own division of labor

MVC mode divides the application into three parts: model, view and controller The name of this model also comes from these three parts

  • The model is the core of these three parts. It contains the state, data and business logic of the program
  • The view is not responsible for processing data, it is only responsible for the display of the model (also responsible for user interaction when there is user interaction) Views can take many forms, such as graphical user interface, statistical charts, plain text, etc
  • The controller is the bridge between model and view, through which the interaction between model and view is completed

Using the controller as a microphone seems redundant, but it is necessary: using the controller can make it have multiple views without modifying the model In general, to do this, each view should have its own controller

Typical application flow of programs Applying MVC mode:

  1. Some user actions (such as clicking a button) trigger a view
  2. The view informs the controller of the user's actions
  3. The controller processes user input and interacts with the model
  4. The model verifies the data, performs relevant operations, and then informs the controller how to continue
  5. The controller passes the results to the view, which updates the displayed information and data according to the results and instructions

Advantages and applications:

advantage:

  • The separation of model and view allows developers responsible for different parts to develop at the same time without affecting each other
  • The coupling between model and view is low, and there will be no interference when modifying and expanding the existing model and view Adding a new view is also easy
  • Each part has a clear division of labor and is easy for later maintenance.

Application:

Generally speaking, we do not need to implement MVC architecture from scratch, but directly use the existing framework that applies MVC architecture or its variants

MVC or its variants are very common in frameworks. For example, Django framework uses MTV (model template view) mode evolved from MVC

Name comparison table:

Traditional MVC mode Django MTV mode
model model
view template
controller view

Django's designer believes that a view describes what controls what data can be seen by the user, so he uses the view to name the component responsible for this function The template is responsible for displaying data in Django, which determines how the data is displayed to users

Implementing MVC mode from scratch

When the existing framework can not meet our needs, we can also implement MVC mode ourselves

Design principles

When implementing MVC from scratch, models, views and controllers need to meet the following principles

Model

  • Independent of presentation
  • Including all verification logic and business logic
  • You can access the data in the program (database, file, etc.)
  • Responsible for updating the status of the application

view

  • Display data
  • Allow user interaction
  • It does not contain any data verification and business logic It contains only minimal other logic, such as loops and branches in template language
  • The data in the program cannot be obtained directly
  • No data is saved

controller

  • Responsible for updating the view when the model changes
  • When the view interacts with the user, it is responsible for updating the model
  • It does not contain any data verification and business logic
  • The data in the program cannot be obtained directly
  • No data is displayed
  • When needed, process the data before passing it between the model and the view

When we want to confirm whether MVC mode is implemented correctly, we can try to answer the following questions:

  • If the program has a graphical interface, can you change the skin of the graphical interface? Is it easy to change skin? Is it difficult to add a function that allows users to change the skin of the interface during operation? If not very simple, then MVC pattern is not implemented correctly
  • If the application does not have a graphical interface, is it easy to add a graphical interface to it? If there is no need to add a graphical interface, is it easy to add other forms of views? Such as files, pictures or charts If we can't just add new views and controllers without modifying the model, it will prove that there are some problems in the implementation

3, Behavior pattern

1. Chain of Responsibility

Content:

Make multiple objects have the opportunity to process the request, so as to avoid the coupling relationship between the sender and receiver of the request. Connect these objects into a chain and pass the request along the chain until an object processes it.
Role:

  • Abstract Handler
  • Concrete handler
  • Client

Example:

  • Approval of leave Department: leader - > Department Manager - > General Manager
  • Javascript event floating mechanism

Applicable scenarios:

  • There are multiple objects that can process a request, and which object processing is determined by the runtime
  • Submit a request to one of multiple objects without specifying the recipient

advantage:

  • Reduce coupling: one object does not need to know which other object handles its request

Disadvantages:

  • The request is not guaranteed to be received: the end of the chain is not processed or the chain configuration is wrong

Leave process:

from abc import ABCMeta, abstractmethod


class Handler(metaclass=ABCMeta):
    @abstractmethod
    def handle_leave(self, day):
        pass


class GeneralManagerHandler(Handler):
    def handle_leave(self, day):
        if day < 10:
            print("Approved by the general manager%d Day off" % day)
            return True
        else:
            print("ha-ha")
            return False


class DepartmentManagerHandler(Handler):
    def __init__(self):
        self.successor = GeneralManagerHandler()

    def handle_leave(self, day):
        if day < 7:
            print("Approved by Department Manager%d Day off" % day)
            return True
        else:
            print("The Department Manager has no right to grant leave")
            return self.successor.handle_leave(day)


class ProjectDirectorHandler(Handler):
    def __init__(self):
        self.successor = DepartmentManagerHandler()

    def handle_leave(self, day):
        if day < 3:
            print("Approved by project director%d Day off" % day)
            return True
        else:
            print("The project director has no right to grant leave")
            return self.successor.handle_leave(day)


day = 11
h = ProjectDirectorHandler()
print(h.handle_leave(day))

Imitation js event handling

# --Advanced example -- imitating js event processing
from abc import ABCMeta, abstractmethod

class Handler(metaclass=ABCMeta):
    @abstractmethod
    def add_event(self, func):
        pass

    @abstractmethod
    def handle(self):
        pass


class BodyHandler(Handler):
    def __init__(self):
        self.func = None

    def add_event(self, func):
        self.func = func

    def handle(self):
        if self.func:
            return self.func()
        else:
            print("It has reached the last level and cannot be processed")


class ElementHandler(Handler):
    def __init__(self, successor):
        self.func = None
        self.successor = successor

    def add_event(self, func):
        self.func = func

    def handle(self):
        if self.func:
            return self.func()
        else:
            return self.successor.handle()


# client

# <body><div><a>

body = {'type': 'body', 'name': 'body', 'children': [], 'father': None}

div = {'type': 'div', 'name': 'div', 'children': [], 'father': body}

a = {'type': 'a', 'name': 'a', 'children': [], 'father': div}

body['children'].append(div)
div['children'].append(a)

# print(body)


body['event_handler'] = BodyHandler()
div['event_handler'] = ElementHandler(div['father']['event_handler'])
a['event_handler'] = ElementHandler(a['father']['event_handler'])


def attach_event(element, func):
    element['event_handler'].add_event(func)


# test

def func_a():
    print("This is for a Function of")


def func_div():
    print("This is for div Function of")


def func_body():
    print("This is for body Function of")


attach_event(a, func_a)
attach_event(div, func_div)
attach_event(body, func_body)

a['event_handler'].handle()

2. Iterator mode

Content:

Provides a way to access the elements of an aggregate object sequentially without exposing the internal representation of the object.

Applicable scenarios:

  • Access the contents of an aggregate object without exposing its internal representation.
  • Supports multiple traversals of aggregate objects.
  • Provide a unified interface for traversing different aggregation structures (that is, support polymorphic iteration)

Implementation method: iter, next

Linked list:

class LinkList:
    """The chain header node saves the length of the linked list"""

    class Node:
        def __init__(self, item=None):
            self.item = item
            self.next = None

    class LinkListIterator:
        def __init__(self, node):
            self.node = node

        def __next__(self):
            if self.node:
                cur_node = self.node
                self.node = cur_node.next
                return cur_node.item
            else:
                raise StopIteration

        def __iter__(self):
            return self

    def __init__(self, iterable=None):
        self.head = LinkList.Node(0)
        self.tail = self.head
        self.extend(iterable)

    def append(self, obj):
        s = LinkList.Node(obj)
        self.tail.next = s
        self.tail = s
        self.head.item += 1

    def extend(self, iterable):
        for obj in iterable:
            self.append(obj)

    def __iter__(self):
        return self.LinkListIterator(self.head.next)

    def __len__(self):
        return self.head.item

    def __str__(self):
        return "<<" + ", ".join(map(str, self)) + ">>"


li = [i for i in range(100)]
lk = LinkList(li)
print(lk)

3. Observer mode

Content:

Defines a one to many dependency between objects. When the state of an object changes, all objects that depend on it are notified and automatically updated. The observer mode is also called "publish subscribe" mode
Role:

  • Abstract Subject
  • Concrete subject - Publisher
  • Abstract Observer
  • Concrete Observer - Subscriber

Applicable scenarios:

  • When an abstract model has two aspects, one of which depends on the other. Encapsulate the two in separate objects so that they can be changed and reused independently.
  • When changing one object requires changing other objects at the same time, we don't know how many objects need to be changed.
  • When an object must notify other objects, it cannot assume who the other objects are. In other words, you don't want these objects to be tightly coupled.

advantage:

  • The abstract coupling between the target and the observer is minimal
  • Support broadcast communication

Disadvantages:

  • Multiple observers do not know each other's existence, so an observer's modification of the topic may cause wrong updates.

Publisher - subscriber:

from abc import ABCMeta, abstractmethod


class Observer(metaclass=ABCMeta):
    @abstractmethod
    def update(self, notice):
        pass


class Notice:
    def __init__(self):
        self.observers = []

    def attach(self, obs):
        self.observers.append(obs)

    def detach(self, obs):
        self.observers.remove(obs)
        # obs.company_info=None

    def notify(self):
        for obj in self.observers:
            obj.update(self)


class ManagerNotice(Notice):
    def __init__(self, company_info=None):
        super().__init__()
        self.__company_info = company_info

    def detach(self, obs):
        super().detach(obs)
        obs.company_info = None

    @property
    def company_info(self):
        return self.__company_info

    @company_info.setter
    def company_info(self, info):
        self.__company_info = info
        self.notify()


class Manager(Observer):
    def __init__(self):
        self.company_info = None

    def update(self, noti):
        self.company_info = noti.company_info


notice = ManagerNotice()

alex = Manager()
wusir = Manager()

print(alex.company_info)
print(wusir.company_info)

notice.attach(alex)
notice.attach(wusir)
notice.company_info = "The company operates well"

print(alex.company_info)
print(wusir.company_info)

notice.company_info = "The company is going public"

print(alex.company_info)
print(wusir.company_info)

notice.detach(wusir)

notice.company_info = "The company is going bankrupt. Run away quickly"

print(alex.company_info)
print(wusir.company_info)

notice.company_info = "The company has gone bankrupt"

print(alex.company_info)
print(wusir.company_info)

4. Strategy

Content:

Define a series of algorithms, encapsulate them one by one, and make them interchangeable. This mode allows the algorithm to change independently of the customers using it.
Role:

  • Abstract Strategy
  • Concrete strategy
  • Context

Applicable scenarios:

  • Many related classes simply behave differently
  • Different variants of an algorithm need to be used
  • The algorithm uses data that the client does not need to know
  • Multiple behaviors in a class exist in the form of multiple conditional statements, which can be encapsulated in different policy classes.

advantage:

  • A series of reusable algorithms and behaviors are defined
  • Some conditional statements are eliminated
  • Different implementations of the same behavior can be provided

Disadvantages:

  • Customers must understand different strategies
  • Communication overhead between policy and context
  • Increased the number of objects

Policy mode example code:

from abc import ABCMeta, abstractmethod
import random


class Sort(metaclass=ABCMeta):
    @abstractmethod
    def sort(self, data):
        pass


class QuickSort(Sort):
    def quick_sort(self, data, left, right):
        if left < right:
            mid = self.partition(data, left, right)
            self.quick_sort(data, left, mid - 1)
            self.quick_sort(data, mid + 1, right)

    def partition(self, data, left, right):
        tmp = data[left]
        while left < right:
            while left < right and data[right] >= tmp:
                right -= 1
            data[left] = data[right]
            while left < right and data[left] <= tmp:
                left += 1
            data[right] = data[left]
        data[left] = tmp
        return left

    def sort(self, data):
        print("Quick sort")
        return self.quick_sort(data, 0, len(data) - 1)


class MergeSort(Sort):
    def merge(self, data, low, mid, high):
        i = low
        j = mid + 1
        ltmp = []
        while i <= mid and j <= high:
            if data[i] <= data[j]:
                ltmp.append(data[i])
                i += 1
            else:
                ltmp.append(data[j])
                j += 1

        while i <= mid:
            ltmp.append(data[i])
            i += 1

        while j <= high:
            ltmp.append(data[j])
            j += 1

        data[low:high + 1] = ltmp

    def merge_sort(self, data, low, high):
        if low < high:
            mid = (low + high) // 2
            self.merge_sort(data, low, mid)
            self.merge_sort(data, mid + 1, high)
            self.merge(data, low, mid, high)

    def sort(self, data):
        print("Merge sort")
        return self.merge_sort(data, 0, len(data) - 1)


class Context:
    def __init__(self, data, strategy=None):
        self.data = data
        self.strategy = strategy

    def set_strategy(self, strategy):
        self.strategy = strategy

    def do_strategy(self):
        if self.strategy:
            self.strategy.sort(self.data)
        else:
            raise TypeError


li = list(range(100000))
random.shuffle(li)

context = Context(li, MergeSort())
context.do_strategy()
# print(context.data)

random.shuffle(context.data)

context.set_strategy(QuickSort())
context.do_strategy()

5. Template Method

Content:

Define the skeleton of the algorithm in an operation and delay some steps to subclasses. Template method allows subclasses to redefine some specific steps of an algorithm without changing the structure of the algorithm.
Role:

  • Abstract class: defines Abstract atomic operations (hook operations); Implement a template method as the skeleton of the algorithm.
  • Concrete class: implements atomic operations

Applicable scenarios:

  • Implement the invariant part of an algorithm at one time
  • The common behaviors in each subclass should be extracted and concentrated in a common parent class to avoid code duplication
  • Control subclass extension

Template method example code:

from abc import ABCMeta, abstractmethod


class IOHandler(metaclass=ABCMeta):
    @abstractmethod
    def open(self, name):
        pass

    @abstractmethod
    def deal(self, change):
        pass

    @abstractmethod
    def close(self):
        pass

    def process(self, name, change):
        self.open(name)
        self.deal(change)
        self.close()


class FileHandler(IOHandler):
    def open(self, name):
        self.file = open(name, "w")

    def deal(self, change):
        self.file.write(change)

    def close(self):
        self.file.close()


f = FileHandler()
f.process("abc.txt", "Hello World")

Tags: Python Design Pattern

Posted by axo on Mon, 23 May 2022 10:49:13 +0300