Detailed explanation of design patterns
Design patterns follow six principles
The open closed principle
Definition: Classes, modules, functions, etc., should be extensible but not modifiable. The open and close principle tells us that when software needs to change, it should be done by extending it rather than modifying existing code. The four words “should try” here show that the OCP principle does not mean that you should never modify an original class. When we smell the “stink” of old code, we should refactor as early as possible so that the code can return to the normal “evolution” process, rather than adding new implementations through integration or other means, which can lead to type bloat and redundant legacy code. Therefore, in the process of development, it is necessary to consider whether to make the software system more stable and flexible by modifying the old code or by inheriting it, so as to ensure the removal of “code corruption” and the correctness of the original modules.
Single responsibility principle
The English name of the Single Responsibility Principle is SRP. This concept was first proposed by Robert C. Martin. The author himself explained this principle as “A class should have only one reason to change”, which translated as “A class should have only one reason to cause its change”.
The single responsibility principle applies not only to classes (and interfaces) but also to methods, where a method should do only one thing if possible.
Richter’s substitution principle
This principle can be described as “derived (subclass) objects may replace their base (superclass) objects in a program.
More generally, subclasses can appear wherever a parent class can, and replacing a parent class with a subclass does not raise any errors or exceptions. You may be wondering “Why is there such a principle?” . Consider a scenario where you have a calculator class Cal in use that can compute simple addition, subtraction, multiplication and division. However, because of some business logic adjustment, you need to be able to perform some advanced calculations (such as finding the remainder). It is easy to think of a solution for this scenario: since the original Cal class has been used in many places, we should not modify it on Cal. So we should choose the inheritance approach, extend a scientific calculator class SciCal and implement business needs. You should also ensure that replacing the Cal class with the SciCal class in your code does not affect other business code. As you can see, in order to minimize modifications and security risks associated with modifications, we have adopted this solution, which is in fact an exercise in Richter’s substitution principle.
Dependency inversion principle
Definition: high-level modules should not depend on low-level modules; both should depend on abstractions. Abstraction should not depend on details, details should depend on abstractions.
In object-oriented programming, the Dependency Inversion principle (DIP) refers to a specific form of decoupling in which higher-level modules are not dependent on the implementation details of lower-level modules and dependencies are reversed. Thus, low-level modules depend on high-level modules’ requirements abstraction. The principles state that:
- High-level modules should not depend on low-level modules; both should depend on abstract interfaces
- Abstract interfaces should not depend on concrete implementations
- Concrete implementations should rely on abstract interfaces
Interface Isolation Principle
Interface-Segregation Principles (ISP) state that a Client should not rely on methods it does not use
To put it bluntly, don’t build a bloated interface. Instead, make it as detailed as possible, with as few methods as possible. It should be noted that this principle, while at first glance the same as a single responsibility, is not. A single responsibility requires a single responsibility, which is divided from the business logic. The interface isolation principle requires as few methods in an interface as possible. Although all methods in an interface revolve around one responsibility, the interface may not comply with the interface isolation principle. For example, suppose that interface A contains 10 methods to query users, five of which are ordinary query methods and the other five are advanced query methods for the management module. According to the interface isolation principle, interface A should be divided into A1 and A2, which contain common query methods and advanced query methods respectively and are used by different modules. The advantage of this is that module developers can safely use the method of interface exposure, and interface developers in the future maintenance, but also more clear of the dependencies, reduce the probability of “affecting the whole” BUG.
extension
The interface should be first business subdivided according to the single responsibility principle, and then subdivided according to the conditions such as interface user, and compressed the public methods exposed by the interface.
Demeter’s rule
The Law of Demeter (LoD) is also called the Least Knowledge Principle (LKP). Although the names are different, they describe the same rule: one object should know the least about the other. In layman’s terms, for dependent classes, no matter how complex the implementation logic is, they try to encapsulate the logic internally, providing public methods, and not giving away any information. Best practices A class should know the least about the classes it needs to couple or call, and the more closely the internal implementation of a class is related to the caller or dependencies, the more decoupled it is, and the greater the impact of changes in one class on the other. (1) In the division of classes, should try to create loosely coupled classes. The lower the coupling degree between the classes, the more conducive to ingestion. Once a class in loose coupling is modified, it does not have much impact on the associated classes. (2) In the mechanism design of classes, each class should try to reduce the access rights of its member variables and member functions. (3) In reference to other classes, the reference of a class to other objects should be minimized.
Common Design Patterns
Behavior type
How do classes and objects interact, and divide responsibilities and algorithms
Template pattern: Defines an algorithm structure while deferring some steps to subclass implementation. Interpreter pattern: Given a language, define a representation of its grammar and define an interpreter. Strategy pattern: Define a set of algorithms, encapsulate them, and make them interchangeable. State mode: Allows an object to change its behavior when its internal state changes. Observer pattern: One-to-many dependencies between objects. Memo mode: Preserves the internal state of objects without breaking encapsulation. The mediator pattern: A mediation object encapsulates a set of object interactions. Command pattern: Encapsulates a command request as an object that can be parameterized with different requests. Visitor pattern: Adds new functionality to a set of object elements without changing the data structure. Chain of responsibility pattern: Decouples the sender and receiver of a request so that multiple objects have the opportunity to process the request. Iterator pattern: A method of traversing the elements of an aggregate object without exposing the internal structure of the object.
The singleton pattern
The singleton pattern, which is defined to ensure that a class has only one instance and provides a global access point. The singleton pattern has three typical characteristics: 1. There is only one instance. 2. Self-instantiation. Provide global access points. Therefore, the singleton pattern can be used when only one instance object is needed in the system or when only one common access point is allowed in the system and the instance cannot be accessed through other access points other than this common access point. The main advantage of singleton mode is to save system resources, improve system efficiency, and strictly control customer access to it.
Factory method pattern
The factory method mode is very consistent with the “open closed principle”. When a new product needs to be added, we only need to add a specific product class and the corresponding specific factory, without modifying the original system. At the same time, in the factory method mode, users only need to know the specific factory that produces the product, and do not need to know the creation process of the product, or even the specific product class name. Although it well conforms to the “open closed principle”, because every new product needs to add two classes, this will inevitably lead to increased system complexity.
- Firstly, the interface of the product is abstracted
- Different products inherit the interface to implement their own logic, so each new product adds one more class
- Design a factory class with a product abstraction return type
- Design the corresponding product factory, implement the factory interface, concrete product class with concrete subclass to instantiate. That means every product requires a factory.
- In this way, the instantiation is postponed until the factory is ready to use it.
The Abstract factory pattern is a creative design pattern that creates a series of related objects without specifying their concrete classes. The abstract factory pattern is targeted at a product group, that is, more than one type of product, possibly many classes of products, whereas the previous factory method pattern had only one product line. For example, we may have been producing cars before, but the factory is all kinds of brands of cars; Now what we have to do is, although there are many brands, our product line may be the commercial version and the sports version, two lines.
- Abstract each product, such as vegetable interface, fruit interface, grain interface
- Specific implementation classes, to achieve the corresponding interface, such as gm vegetables and non-GM vegetables implementation car interface, gm fruit and non-GM fruit interface, GM food and non-GM food interface.
- Create a factory interface for generating products for various product lines
- Create a specific product line factory, implement the factory interface above, implement the production of products on the product line, here create examples of different product lines. For example, genetically modified factories can produce genetically modified fruits and food, etc. Non-gm plants produce non-GM fruits and grains.
- That’s when the previous product line went from 1 to 2. The products in the product line are consistent with the product line.
The prototype pattern
Implement a deep copy of the prototype, resulting in a logically identical new object.
- Create a prototype interface and declare it
cloning
Methods. If you already have a class hierarchy, simply add this method to all of its classes. - A stereotype class must define a separate constructor that takes that class object as an argument. The constructor must copy all member variable values from the parameter object into the new entity. If you need to modify a subclass, you must call the parent class constructor to have the parent class copy its private member variable values.
If the programming language does not support method overloading, you may need to define a special method to copy object data. It is easier to do this in the constructor because it is callingnew
The result object is returned immediately after the operator. - Cloning methods are usually a single line of code: use
new
Operator calls the constructor of the prototype version. Note that each class must explicitly override the clone method and call it using its own class namenew
Operator. Otherwise, the cloning method might generate objects of the parent class. - You can also create a centralized prototype registry to store commonly used prototypes. You can create a new factory class to implement the registry, or add a static method to get the prototype in the prototype base class. The method must be able to search based on criteria set by the client code. The search criteria can be a simple string or a complex set of search parameters. Once a suitable prototype is found, the registry should clone the prototype and return the object generated by the copy to the client. Finally, the direct call to the subclass constructor is replaced with a call to the prototype registry factory method
import java.util.Objects;
/** * Created by kingfou on 2020/7/23. */
public abstract class Shape {
public int x;
public int y;
public String color;
public Shape(a) {}public Shape(Shape target) {
if(target ! =null) {
this.x = target.x;
this.y = target.y;
this.color = target.color; }}public abstract Shape clonee(a);
@Override
public boolean equals(Object object2) {
if(! (object2instanceof Shape)) return false;
Shape shape2 = (Shape) object2;
returnshape2.x == x && shape2.y == y && Objects.equals(shape2.color, color); }}public class Circle extends Shape {
public int radius;
public Circle(a) {}public Circle(Circle target) {
super(target);
if(target ! =null) {
this.radius = target.radius; }}@Override
public Shape clonee(a) {
// Because it is new, so it must be consistent
return new Circle(this);
}
@Override
public boolean equals(Object object2) {
if(! (object2instanceof Circle) || !super.equals(object2)) return false;
Circle shape2 = (Circle) object2;
return shape2.radius == radius;
}
public static void main(String[] args) {
Circle circle = newCircle(); Circle circle1 = (Circle) circle.clonee(); System.out.println(circle1 == circle); System.out.println(circle1.equals(circle)); }}Copy the code
The singleton pattern
There is only one object instance of the class globally, and no separate constructor is provided, only a method to obtain the unique instance. There are four main implementation patterns
The hungry type
public class Singleton {
// Whether it is used or not, it will be created first and may have performance defects
private static Singleton instance = new Singleton();
private Singleton (a){}public static Singleton getInstance(a) {
returninstance; }}Copy the code
LanHanShi
// Thread unsafe mode
public class Singleton {
private static Singleton instance;
private Singleton (a){}public static Singleton getInstance(a) {
if (instance == null) {
// Objects may be created multiple times because of thread insecurity
instance = new Singleton();
}
returninstance; }}public class Singleton {
// Thread safe mode
private static Singleton instance;
private Singleton (a){}public static synchronized Singleton getInstance(a) {
if (instance == null) {
instance = new Singleton();
}
returninstance; }}Copy the code
Double check lock
public class Singleton {
//volatile prevents instruction reordering.
private volatile static Singleton instance;
private Singleton (a){}public static Singleton getInstance(a) {
if (instance== null) {
synchronized (Singleton.class) {
if (instance== null) {
// This assignment statement is not an atomic operation, but involves an object creation and assignment process.
// There are actually 3 steps: allocate memory address, initialize object, assign address to singleton.
// But the order is not certain, that is, may occur first assignment and then initialization process.
// At initialization time, if there is a new thread if the singleton has been assigned, then it may get
// an empty singleton. With volatile, however, the order of execution is guaranteed to be
// The assignment of the singleton must be at the end. (Code sequentiality)
instance= newSingleton(); }}}returnsingleton; }}Copy the code
Static inner class
public class Singleton {
private Singleton(a) {}
Static inner classes are not initialized unless the initialization condition is triggered
static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance(a) {
// Trigger the initialization condition. A static property of a static inner class was called.
// The inner class is static because the inner class cannot have static attributes.
// Simply static inner classes can have static attributes.
returnSingletonHolder.instance; }}Copy the code
Adapter mode
An adapter is a structural design pattern that enables incompatible objects to cooperate with each other. An adapter acts as a wrapper between two objects, receiving calls to one object and converting it to a format and interface that the other object recognizes
/** * SquarePegs are not compatible with RoundHoles (they were implemented by * previous development team). But we have to integrate them into our program. */
public class SquarePeg {
private double width;
public SquarePeg(double width) {
this.width = width;
}
public double getWidth(a) {
return width;
}
public double getSquare(a) {
double result;
result = Math.pow(this.width, 2);
returnresult; }}public class RoundPeg {
private double radius;
public RoundPeg(a) {}
public RoundPeg(double radius) {
this.radius = radius;
}
public double getRadius(a) {
returnradius; }}public class RoundHole {
private double radius;
public RoundHole(double radius) {
this.radius = radius;
}
public double getRadius(a) {
return radius;
}
public boolean fits(RoundPeg peg) {
boolean result;
result = (this.getRadius() >= peg.getRadius());
returnresult; }}// Whoever needs to be adapted is passed in. Adapters are made for new classes that are suitable for older systems.
// New is passed in, old is inherited.
public class SquarePegAdapter extends RoundPeg {
private SquarePeg peg;
public SquarePegAdapter(SquarePeg peg) {
this.peg = peg;
}
@Override
public double getRadius(a) {
double result;
// Calculate a minimum circle radius, which can fit this peg.
result = (Math.sqrt(Math.pow((peg.getWidth() / 2), 2) * 2));
returnresult; }} Because the adapter is of an old type and a new type is passed in, the adapter can be inserted into the old system and continue to be used without impact. Only the adapter modifies the way in which the newly passed type is used.Copy the code
Decorator pattern
Decorator is a structural design pattern that allows you to bind new behavior to an object by putting it into a special wrapper that contains behavior. Because the target object and decorator follow the same interface, you can use decorators to encapsulate objects an infinite number of times. The resulting object gets the superimposed behavior of all the wrappers. To put it simply, the decorator pattern is a simple nesting technique, passing the simplest object to the constructor of a complex class so that complex instance objects can be obtained.
The proxy pattern
See the section on static and dynamic proxies for details.
The flyweight pattern
implementation
- ::before
- Find all methods that use external state member variables, create a new parameter for each member variable used in the method, and use that parameter instead of the member variable.
- Optionally, you can create a factory class to manage the pool of privileges, which checks for existing privileges as they are created. If you choose to use a factory, the client can only request the privilege through the factory, and they need to pass the intrinsic state of the privilege as a parameter to the factory.
- The client must store and calculate the value of the external state (scene) because only then can the methods of the metadata object be called. For ease of use, the external state and member variables that reference the member can be moved into a separate situation class.
Observer model
The Observer pattern is a behavior design pattern that allows you to define a subscription mechanism that notifies multiple other objects that “observe” an object when an event occurs. An object that has some interesting state is often called a target, and since it notifies other objects of its state changes, it is also called a publisher. Subscribers are called all other objects that want to pay attention to changes in the publisher’s state. The Observer pattern suggests that you add a subscription mechanism to the publisher class so that each object can subscribe to or unsubscribe from the publisher event stream. Don’t be afraid! It’s not as complicated as it sounds. In fact, the mechanism consists of 1) a list member variable used to store references to subscriber objects; 2) Several public methods for adding or removing subscribers to the list.
- For publishers, within a particular method, a call is woven into a method that implements notification to all subscribers.
- The subscriber is added to a specific event queue for traversal notification.
- Publishers need to have a subscription method to add corresponding subscription events and subscribers. That is, publishers need to maintain events and their corresponding subscribers, typically using HashMap+ queues. For each event, we abstract out a notification method, which is used to store the corresponding event and the subscription queue. When the event occurs, we call the subscriber’s callback function.
- For subscribers, a callback function abstraction is provided for the publisher’s invocation.
- In simple terms, the subscriber simply implements a subscription interface that has an abstract callback function called by the publisher and does nothing else
- For publishers, the first step is to maintain individual event queues, with individual subscribers in the queues. Also, the subscriber’s callback interface is invoked when the event occurs.
/** Attribute subscriber's callback interface */
public interface EventListener {
void update(String eventType, File file);
}
The callback function is instantiated when the subscriber is registered with the publisher. * /
public class EmailNotificationListener implements EventListener {
private String email;
public EmailNotificationListener(String email) {
this.email = email;
}
@Override
public void update(String eventType, File file) {
System.out.println("Email to " + email + ": Someone has performed " + eventType + " operation with the following file: "+ file.getName()); }}/** Define a publisher's interface. Publishers need method abstractions such as 1. Event register method, maintain a queue for each event, queue to put subscribers 2. 3. Subscribe, add subscribers to the corresponding queue for a specific event */
public class EventManager {
/ / container
Map<String, List<EventListener>> listeners = new HashMap<>();
// Maintain a queue for each event
public EventManager(String... operations) {
for (String operation : operations) {
this.listeners.put(operation, newArrayList<>()); }}// Add the subscriber of the corresponding event to the queue
public void subscribe(String eventType, EventListener listener) {
List<EventListener> users = listeners.get(eventType);
users.add(listener);
}
// Unsubscribe
public void unsubscribe(String eventType, EventListener listener) {
List<EventListener> users = listeners.get(eventType);
users.remove(listener);
}
// The notification mechanism is provided when an event occurs, by calling back the publisher's method.
public void notify(String eventType, File file) {
List<EventListener> users = listeners.get(eventType);
for(EventListener listener : users) { listener.update(eventType, file); }}}/** Define what a specific publisher needs to do. 1. Call a method in the abstract class when the corresponding event occurs. 2. Write a variety of corresponding event processing logic, when the event processing is completed, call */
public class Editor {
public EventManager events;
private File file;
// Hard coded??
public Editor(a) {
this.events = new EventManager("open"."save");
}
// The specific publisher needs to process the corresponding logic before calling the function
public void openFile(String filePath) {
this.file = new File(filePath);
events.notify("open", file);
}
// The specific publisher needs to process its own logic before calling the function
public void saveFile(a) throws Exception {
if (this.file ! =null) {
events.notify("save", file);
} else {
throw new Exception("Please open a file first."); }}}Copy the code