Design Patterns -- the basis of Reusable Object-Oriented Software
Design pattern is a set of code design experience that is repeatedly used, known by most people, classified and catalogued. The purpose of using design patterns is to reuse the code, make the code easier to be understood by others and ensure the reliability of the code. There is no doubt that design patterns are win-win for themselves, others and the system. Design patterns make code preparation truly engineering. Design patterns are the cornerstone of software engineering, just like bricks and stones of a building.
The rational use of design patterns in the project can perfectly solve many problems. Each pattern has corresponding principles to correspond to it. Each pattern describes a recurring problem around us and the core solution of the problem, which is also the reason why it can be widely used.
The classic book "design patterns" summarizes 23 design patterns, which can be divided into three categories: creative, structural and behavioral
Six principles of design mode
1. Open Close Principle
The opening and closing principle means that it is open to extensions and closed to modifications. When the program needs to be expanded, you can't modify the original code to achieve a hot plug effect. So in a word, in order to make the program extensible, easy to maintain and upgrade. To achieve this effect, we need to use interfaces and abstract classes, which will be mentioned in the following specific design.
2. Liskov Substitution Principle
Liskov Substitution Principle (LSP) is one of the basic principles of object-oriented design. Richter's substitution principle says that where any base class can appear, subclasses must appear. LSP is the cornerstone of inheritance reuse. Only when the derived class can replace the base class and the function of the software unit is not affected, the base class can be truly reused, and the derived class can also add new behavior on the basis of the base class. Richter's substitution principle is a supplement to the "open close" principle. The key step in realizing the "open close" principle is abstraction. The inheritance relationship between base class and subclass is the concrete implementation of abstraction, so the Richter substitution principle is the specification of the specific steps to realize abstraction—— From Baidu Encyclopedia
3. Dependency Inversion Principle
This is the basis of the opening and closing principle. The specific content is interface programming, which depends on abstraction rather than concrete.
4. Interface Segregation Principle
This principle means that using multiple isolated interfaces is better than using a single interface. It also means to reduce the coupling between classes. From here, we can see that in fact, the design pattern is a software design idea, starting from the large-scale software architecture for the convenience of upgrading and maintenance. So it appears many times above: reduce dependence and reduce coupling.
5. Demeter Principle
Why is it called the least known principle, that is, an entity should interact with other entities as little as possible to make the system functional modules relatively independent.
6. Composite Reuse Principle
The principle is to try to use composition / aggregation instead of inheritance.
1. Create mode
As mentioned earlier, the division of labor of socialization is becoming more and more detailed, which is also true in software design. Therefore, it has become an inevitable trend to separate the creation and use of objects. Because the creation of objects will consume a lot of resources of the system, it is a problem to study the creation of objects separately so as to create objects efficiently. Here are six specific creative models to study, which are:
Singleton mode
Singleton Pattern is a common software design pattern. The main purpose of this pattern is to ensure that only one instance of a class exists. Singleton objects come in handy when you want only one instance of a class in the whole system.
For example, the configuration information of a server program is stored in a file, and the client reads the configuration file information through an AppConfig class. If the contents of the configuration file need to be used in many places during the running of the program, that is, instances of AppConfig objects need to be created in many places, which will lead to multiple AppConfig instance objects in the system, which will seriously waste memory resources, especially when there are many contents of the configuration file. In fact, for classes like AppConfig, we want only one instance object to exist during the program running
1 class Singleton(object): 2 def __init__(self): 3 pass 4 5 def __new__(cls, *args, **kwargs): 6 if not hasattr(Singleton, "_instance"): # reflex 7 Singleton._instance = object.__new__(cls) 8 return Singleton._instance 9 10 obj1 = Singleton() 11 obj2 = Singleton() 12 print(obj1, obj2) #<__main__.Singleton object at 0x004415F0> <__main__.Singleton object at 0x004415F0>
Factory mode
Factory pattern is a design pattern used to create objects in software development.
The factory pattern contains a superclass. This superclass provides an abstract interface to create a specific type of object, rather than determining which object can be created.
To implement this method, you need to create a factory class and return it.
When the program runs and inputs a "type", the corresponding object needs to be created. This uses the factory model. In this case, the implementation code is based on the factory mode, which can achieve scalable and maintainable code. When adding a new type, you no longer need to modify the existing class, but only add subclasses that can generate new types.
Briefly, factory mode can be used when:
1. I don't know what kind of object the user wants to create
2. When you want to create an extensible association between the creation class and the class that supports the creation of objects.
An example can better understand the above contents:
- We have a base class, Person, which includes methods to obtain names and genders. There are two subclasses, male and female, which can say hello. There is also a factory class.
- The factory class has a method name. getPerson has two input parameters, name and gender.
- The user uses the factory class by calling the getPerson method.
During the program running, the user passes the gender to the factory, and the factory creates an object related to gender. Therefore, the factory class determines which object should be created at run time
class Person: def __init__(self): self.name = None self.gender = None def getName(self): return self.name def getGender(self): return self.gender class Male(Person): def __init__(self, name): print "Hello Mr." + name class Female(Person): def __init__(self, name): print "Hello Miss." + name class Factory: def getPerson(self, name, gender): if gender == 'M': return Male(name) if gender == 'F': return Female(name) if __name__ == '__main__': factory = Factory() person = factory.getPerson("Chetan", "M")
Builder pattern
Separate the construction of a complex object from its representation, so that the same construction process can create different representations.
Related modes: the idea is very similar to the template method mode. The template method encapsulates the algorithm process. For some details, the interface is provided and modified by the subclass. The builder mode is higher-level, and all details are implemented by the subclass
An example can better understand the above contents:
- There is an interface class that defines methods for creating objects. A commander class that accepts the creator object as a parameter. The two creator classes have the same method of creating objects, and the internal creation can be customized
- One commander, two creators (thin and fat), the commander can specify which Creator will create
from abc import ABCMeta, abstractmethod class Builder(): __metaclass__ = ABCMeta @abstractmethod def draw_left_arm(self): pass @abstractmethod def draw_right_arm(self): pass @abstractmethod def draw_left_foot(self): pass @abstractmethod def draw_right_foot(self): pass @abstractmethod def draw_head(self): pass @abstractmethod def draw_body(self): pass class Thin(Builder): def draw_left_arm(self): print 'Draw left hand' def draw_right_arm(self): print 'Draw the right hand' def draw_left_foot(self): print 'Draw the left foot' def draw_right_foot(self): print 'Draw the right foot' def draw_head(self): print 'Draw head' def draw_body(self): print 'Draw a thin body' class Fat(Builder): def draw_left_arm(self): print 'Draw left hand' def draw_right_arm(self): print 'Draw the right hand' def draw_left_foot(self): print 'Draw the left foot' def draw_right_foot(self): print 'Draw the right foot' def draw_head(self): print 'Draw head' def draw_body(self): print 'Draw a fat body' class Director(): def __init__(self, person): self.person=person def draw(self): self.person.draw_left_arm() self.person.draw_right_arm() self.person.draw_left_foot() self.person.draw_right_foot() self.person.draw_head() self.person.draw_body() if __name__=='__main__': thin=Thin() fat=Fat() director_thin=Director(thin) director_thin.draw() director_fat=Director(fat) director_fat.draw()
Prototype mode
Specify the type of objects to be created with prototype instances, and create new objects by copying these prototypes.
The essence of prototype mode is to clone objects. Therefore, when the object initialization operation is complex, it is very practical, which can greatly reduce the time-consuming and improve the performance, because "you do not need to reinitialize the object, but dynamically obtain the running state of the object".
Shallow Copy: the field of an object is copied, but the object referenced by the field will not be copied. The copied object and the source object have the same name, but they share the same entity.
deep copy: copy the object referenced by the field in the object instance.
import copy from collections import OrderedDict class Book: def __init__(self, name, authors, price, **rest): '''rest Examples are: publisher, length, label, publication date''' self.name = name self.authors = authors self.price = price # Unit: USD self.__dict__.update(rest) def __str__(self): mylist = [] ordered = OrderedDict(sorted(self.__dict__.items())) for i in ordered.keys(): mylist.append('{}: {}'.format(i, ordered[i])) if i == 'price': mylist.append('$') mylist.append('\n') return ''.join(mylist) class Prototype: def __init__(self): self.objects = dict() def register(self, identifier, obj): self.objects[identifier] = obj def unregister(self, identifier): del self.objects[identifier] def clone(self, identifier, **attr): found = self.objects.get(identifier) if not found: raise ValueError('Incorrect object identifier: {}'.format(identifier)) obj = copy.deepcopy(found) obj.__dict__.update(attr) return obj def main(): b1 = Book('The C Programming Language', ('Brian W. Kernighan', 'Dennis M.Ritchie'), price=118, publisher='Prentice Hall', length=228, publication_date='1978-02-22', tags=('C', 'programming', 'algorithms', 'data structures')) prototype = Prototype() cid = 'k&r-first' prototype.register(cid, b1) b2 = prototype.clone(cid, name='The C Programming Language(ANSI)', price=48.99, length=274, publication_date='1988-04-01', edition=2) for i in (b1, b2): print(i) print("ID b1 : {} != ID b2 : {}".format(id(b1), id(b2))) if __name__ == '__main__': main()
2. Structural mode
After solving the problem of object creation, the composition of objects and the dependencies between objects have become the focus of developers, because how to design the structure, inheritance and dependencies of objects will affect the maintenance of subsequent programs, code robustness, coupling and so on. The design of object structure can easily reflect the level of designers. Here are seven specific structural models for research, which are:
Adapter mode
The so-called adapter mode refers to an interface adaptation technology. It can use another class with incompatible interfaces through one class. Using this mode, the interfaces of the two classes do not need to be changed.
The adapter pattern is mainly used when you want to reuse some existing classes, but the interface is inconsistent with the requirements of the reuse environment. For example, it is of great practical value in applications that need to reuse some functions of early code.
Explanation 2:
Adapter Pattern: convert the interface of a class into another interface desired by the customer. Adapter Pattern enables those classes that cannot work together due to incompatible interfaces to work together
Application scenario: when the system data and behavior are correct, but the interface is inconsistent, the purpose is to match an original object outside the control range with an interface. The adapter mode is mainly used when you want to reuse some existing classes, but the interface is inconsistent with the reuse environment
class Target(object): def request(self): print "General request" class Adaptee(object): def specific_request(self): print "Special request" class Adapter(Target): def __init__(self): self.adaptee = Adaptee() def request(self): self.adaptee.specific_request() if __name__ == "__main__": target = Adapter() target.request()
Modifier mode
Add responsibilities to a single object in a dynamic and transparent manner without affecting other objects
This pattern is called decorator, but that doesn't mean it should only be used to make the product look more beautiful. Decorator patterns are often used to extend the functionality of an object. Practical examples of such extensions include adding a silencer to a gun and using different camera lenses
__author__ = "Burgess Zheng" #!/usr/bin/env python #-*- coding:utf-8 -*- ''' Decorator ''' class foo(object): def f1(self): print("original f1") def f2(self): print("original f2") class foo_decorator(object): def __init__(self, decoratee): self._decoratee = decoratee def f1(self): print("decorated f1") self._decoratee.f1() def __getattr__(self, name): return getattr(self._decoratee, name) u = foo() v = foo_decorator(u) v.f1() v.f2()
Appearance mode
Appearance mode is also called facade mode. Decoupling is a highly respected concept in object-oriented programming. But in fact, some systems are too complex, which increases the coupling between the client and the subsystem. For example, when watching a multimedia theater at home, I prefer to press a button to realize the cooperative work of DVD player, TV and audio, rather than saying that each machine should be operated once. In this case, the appearance mode can be adopted, that is, a class is introduced to wrap the subsystem and let the client interact with it.
Facade pattern: external communication with a subsystem must be conducted through a unified appearance object to provide a consistent interface for a group of interfaces in the subsystem. The facade pattern defines a high-level interface, which makes the subsystem easier to use. Appearance mode, also known as facade mode, is an object structure mode.
Suppose there is a set of fire alarm system, which is composed of three sub components: an alarm, a sprinkler and an automatic telephone dialing device.
class AlarmSensor: def run(self): print("Alarm Ring...") class WaterSprinker: def run(self): print("Spray Water...") class EmergencyDialer: def run(self): print("Dial 119...") class EmergencyFacade: """ The appearance class encapsulates the operations on the subsystem """ def __init__(self): self.alarm_sensor=AlarmSensor() self.water_sprinker=WaterSprinker() self.emergency_dialer=EmergencyDialer() def runAll(self): self.alarm_sensor.run() self.water_sprinker.run() self.emergency_dialer.run() if __name__=="__main__": emergency_facade=EmergencyFacade() emergency_facade.runAll()
According to the "single responsibility principle", dividing a system into several subsystems in the software is conducive to reducing the complexity of the whole system. A common design goal is to minimize the communication and interdependence between subsystems, and one of the ways to achieve this goal is to introduce an appearance object, It provides a simple and single entry for subsystem access. The appearance pattern is also the embodiment of "Demeter's law". By introducing a new appearance class, the complexity of the original system can be reduced, and the coupling between customer class and subsystem class can be reduced.
Appearance mode requires that the communication between the outside of a subsystem and its inside is carried out through a unified appearance object. Appearance class separates the internal complexity of the client and the subsystem, so that the client only needs to deal with appearance objects, rather than many objects inside the subsystem. The purpose of appearance mode is to reduce the complexity of the system. The appearance mode greatly improves the convenience of the client, so that the client does not need to care about the working details of the subsystem, and can call relevant functions through the appearance role.
advantage:
The main advantage lies in shielding subsystem components from customers, reducing the number of objects handled by customers and making the subsystem easier to use. It realizes the loose coupling relationship between subsystems and customers, reduces the compilation dependency in large software systems, and simplifies the migration process of the system between different platforms;
Disadvantages:
Its disadvantage is that it can not well restrict customers from using subsystem classes, and adding a new subsystem may need to modify the appearance class or the source code of the client without introducing Abstract appearance classes, which violates the "opening and closing principle".
Sharing element mode
Using sharing technology to effectively support a large number of fine-grained objects.
Internal state: the shared part of the shared meta object that will not change with the environment. For example, the color of go pieces.
External state: a state that changes with the environment and cannot be shared is an external state. For example, the position of go pieces.
Application scenario: a large number of objects are used in the program. If the external state of the object is deleted, many groups of objects can be replaced with relatively few shared objects, and the shared element mode can be considered.
1 import random 2 from enum import Enum 3 TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree') 4 5 class Tree: 6 pool = dict() 7 def __new__(cls, tree_type): 8 obj = cls.pool.get(tree_type, None) 9 if not obj: 10 obj = object.__new__(cls) 11 cls.pool[tree_type] = obj 12 obj.tree_type = tree_type 13 return obj 14 15 def render(self, age, x, y): 16 print('render a tree of type {} and age {} at ({}, {})'.format(self.tree_type, age, x, y)) 17 18 19 def main(): 20 rnd = random.Random() 21 age_min, age_max = 1, 30 # Unit: year 22 min_point, max_point = 0, 100 23 tree_counter = 0 24 for _ in range(10): 25 t1 = Tree(TreeType.apple_tree) 26 t1.render(rnd.randint(age_min, age_max), 27 rnd.randint(min_point, max_point), 28 rnd.randint(min_point, max_point)) 29 tree_counter += 1 30 for _ in range(3): 31 t2 = Tree(TreeType.cherry_tree) 32 t2.render(rnd.randint(age_min, age_max), 33 rnd.randint(min_point, max_point), 34 rnd.randint(min_point, max_point)) 35 tree_counter += 1 36 for _ in range(5): 37 t3 = Tree(TreeType.peach_tree) 38 t3.render(rnd.randint(age_min, age_max), 39 rnd.randint(min_point, max_point), 40 rnd.randint(min_point, max_point)) 41 tree_counter += 1 42 43 print('trees rendered: {}'.format(tree_counter)) 44 print('trees actually created: {}'.format(len(Tree.pool))) 45 t4 = Tree(TreeType.cherry_tree) 46 t5 = Tree(TreeType.cherry_tree) 47 t6 = Tree(TreeType.apple_tree) 48 print('{} == {}? {}'.format(id(t4), id(t5), id(t4) == id(t5))) 49 print('{} == {}? {}'.format(id(t5), id(t6), id(t5) == id(t6))) 50 51 main()
proxy pattern
Problems when accessing objects directly, for example, the object to be accessed is on a remote machine. In an object-oriented system, direct access to some objects will bring a lot of trouble to users or system structure for some reasons (such as high object creation cost, or some operations need security control, or need out of process access). We can add an access layer to this object when accessing this object.
__author__ = "Burgess Zheng" #!/usr/bin/env python #-*- coding:utf-8 -*- ''' Proxy ''' # proxy pattern # Application features: it is necessary to add an intermediate control layer when some special intermediate operations are required between the communication parties. # Structural features: create an intermediate class, create an object, receive an object, and then connect the two class sender_base: def __init__(self): pass def send_something(self, something): pass class send_class(sender_base): def __init__(self, receiver): self.receiver = receiver def send_something(self, something): print("SEND " + something + ' TO ' + self.receiver.name) class agent_class(sender_base): def __init__(self, receiver): self.send_obj = send_class(receiver) def send_something(self, something): self.send_obj.send_something(something) class receive_class: def __init__(self, someone): self.name = someone if '__main__' == __name__: receiver = receive_class('Burgess') agent = agent_class(receiver) agent.send_something('agentinfo') print(receiver.__class__) print(agent.__class__)
3. Behavioral model
After the problems of object structure and object creation are solved, the problem of object behavior remains. If the behavior of objects is well designed, the behavior of objects will be clearer and the cooperation efficiency between them will be improved. Here are 11 specific behavior patterns to study, which are: