Java Design Patterns from Novice to Master

Keywords: Java jvm Programming

Study Java Students pay attention!!!  
If you encounter any problems or want to acquire learning resources, you are welcome to join us. Java Learning Exchange Group, Group Number: 456544752. Let's learn Java together!


Design pattern is a set of repeated use, most people know, classification and cataloguing, code design experience summary. Design patterns are used to reuse code, make it easier for others to understand, and ensure code reliability. Undoubtedly, design patterns are win-win for others and systems. Design patterns make code compilation truly engineering. Design patterns are the cornerstone of software engineering, just like building blocks. Reasonable application of design patterns in projects can solve many problems perfectly. Each pattern has corresponding principles to correspond to it. Each pattern describes a problem that occurs repeatedly around us and the core solution of the problem. This is also the reason why it can be widely used. I will study this chapter in a way that combines theory with practice. I hope that the vast number of program enthusiasts will learn design patterns well and become an excellent software engineer! _________

Classification of Design Patterns

Overall, design patterns fall into three categories:

There are five types of creative mode: factory method mode, abstract factory mode, singleton mode, builder mode and prototype mode.

There are seven structural modes: adapter mode, decorator mode, agent mode, appearance mode, bridge mode, combination mode and hedonic mode.

There are eleven behavioral modes: strategy mode, template method mode, observer mode, iteration sub-mode, responsibility chain mode, command mode, memo mode, status mode, visitor mode, mediator mode and interpreter mode.

There are actually two types: concurrent mode and thread pool mode. Use a picture to describe it as a whole:

Six Principles of Design Patterns

1. Open Close Principle

The Open-Close Principle is that it is open to expansion and closed to modification. When the program needs to be expanded, we can not modify the original code to achieve a hot-swap effect. So in a word: in order to make the program scalable, easy to maintain and upgrade. To achieve this effect, we need to use interfaces and abstract classes, which we will mention later in the specific design.

2. Liskov Substitution Principle

One of the basic principles of object-oriented design is Liskov Substitution Principle LSP. According to Richter's substitution principle, subclasses can appear wherever base classes can appear. LSP is the cornerstone of inheritance and reuse. Only when derivative classes can replace base classes and the functions of software units are not affected, can base classes be reused, and derivative classes can add new behaviors on the basis of base classes. Richter's substitution principle is a supplement to the "open-close" principle. The key step to realize the "open-close" principle is abstraction. The inheritance relationship between base class and subclass is the concrete realization of abstraction, so Richter's substitution principle is the specification of concrete steps to realize abstraction. —— From Baidu Encyclopedias

3. Dependence Inversion Principle

This is the basis of the Open-Close Principle. Specific content: True programming of interfaces depends on abstraction rather than on concrete.

4. Interface Segregation Principle

This principle means that it is better to use multiple isolated interfaces than to use a single interface. It also means reducing the coupling between classes. From here we can see that the design pattern is actually the design idea of a software, from large software. Framework In order to upgrade and maintain conveniently. So there are many times above: reducing dependency and coupling.

5. Demeter Principle

Why is the least known principle? That is to say, an entity should interact with other entities as little as possible, so that the system function modules are relatively independent.

6. Composite Reuse Principle

The principle is to try to use synthesis/aggregation rather than inheritance.

Design Patterns in Java 23

Starting from this section, we introduce the concepts and application scenarios of 23 design patterns in Java in detail, and analyze their characteristics and principles of design patterns.

1. Factory Method

The factory method model is divided into three types:

11. The common factory pattern is to build a factory class, and create instances of some classes that implement the same interface. First, look at the diagram:

Examples are as follows: (Let's give an example of sending e-mails and short messages)

First, create a common interface between the two:

  1. public interface Sender {  
  2.     public void Send();  
  3. }  

Second, create implementation classes:

  1. public class MailSender implements Sender {  
  2.     @Override  
  3.     public void Send() {  
  4.         System.out.println("this is mailsender!");  
  5.     }  
  6. }  
  1. public class SmsSender implements Sender {  
  2.   
  3.     @Override  
  4.     public void Send() {  
  5.         System.out.println("this is sms sender!");  
  6.     }  
  7. }  

Finally, plant construction class:

  1. public class SendFactory {  
  2.   
  3.     public Sender produce(String type) {  
  4.         if ("mail".equals(type)) {  
  5.             return new MailSender();  
  6.         } else if ("sms".equals(type)) {  
  7.             return new SmsSender();  
  8.         } else {  
  9.             System.out.println("Please enter the correct type!");  
  10.             return null;  
  11.         }  
  12.     }  
  13. }  

Let's come. test Next:

  1. public class FactoryTest {  
  2.   
  3.     public static void main(String[] args) {  
  4.         SendFactory factory = new SendFactory();  
  5.         Sender sender = factory.produce("sms");  
  6.         sender.Send();  
  7.     }  
  8. }  

Output: this is sms sender!

22. The multiple factory method pattern is an improvement on the common factory method pattern. In the common factory method pattern, if the string passed is wrong, the object can not be created correctly. The multiple factory method pattern provides multiple factory methods to create objects separately. Diagram:

Modify the above code and change the SendFactory class as follows:

  1. public class SendFactory {  
  2.       
  3.     public Sender produceMail(){  
  4.         return new MailSender();  
  5.     }  
  6.       
  7.     public Sender produceSms(){  
  8.         return new SmsSender();  
  9.     }  
  10. }  

The test classes are as follows:

  1. public class FactoryTest {  
  2.   
  3.     public static void main(String[] args) {  
  4.         SendFactory factory = new SendFactory();  
  5.         Sender sender = factory.produceMail();  
  6.         sender.Send();  
  7.     }  
  8. }  

Output: this is mail ender!

33. Static factory method mode. Set the methods in the above multiple factory method modes as static, and call them directly without creating instances.

  1. public class SendFactory {  
  2.       
  3.     public static Sender produceMail(){  
  4.         return new MailSender();  
  5.     }  
  6.       
  7.     public static Sender produceSms(){  
  8.         return new SmsSender();  
  9.     }  
  10. }  
  1. public class FactoryTest {  
  2.   
  3.     public static void main(String[] args) {      
  4.         Sender sender = SendFactory.produceMail();  
  5.         sender.Send();  
  6.     }  
  7. }  

Output: this is mail ender!

Generally speaking, factory mode is suitable: when a large number of products need to be created and have a common interface, they can be created through factory method mode. Among the three modes mentioned above, the first one is that if the incoming string is incorrect, the object can not be created correctly. The third one does not need to instantiate the chemical plant class as compared with the second one. Therefore, in most cases, we will choose the third mode, the static factory method mode.

2. Abstract Factory

The factory method pattern has a problem that class creation depends on factory class, that is to say, if you want to expand the program, you must modify the factory class, which violates the closure principle. So, from the design point of view, there are some problems, how to solve them? The abstract factory pattern is used to create multiple factory classes, so that once new functions are needed, new factory classes can be added directly without modifying the previous code. Because Abstract factories are not easy to understand, let's look at the diagram first, and then with the code, it's easier to understand.

See examples:

  1. public interface Sender {  
  2.     public void Send();  
  3. }  

Two implementation classes:

  1. public class MailSender implements Sender {  
  2.     @Override  
  3.     public void Send() {  
  4.         System.out.println("this is mailsender!");  
  5.     }  
  6. }  
  1. public class SmsSender implements Sender {  
  2.   
  3.     @Override  
  4.     public void Send() {  
  5.         System.out.println("this is sms sender!");  
  6.     }  
  7. }  

Two factory classes:

  1. public class SendMailFactory implements Provider {  
  2.       
  3.     @Override  
  4.     public Sender produce(){  
  5.         return new MailSender();  
  6.     }  
  7. }  
  1. public class SendSmsFactory implements Provider{  
  2.   
  3.     @Override  
  4.     public Sender produce() {  
  5.         return new SmsSender();  
  6.     }  
  7. }  

Providing an interface:

  1. public interface Provider {  
  2.     public Sender produce();  
  3. }  

Test class:

  1. public class Test {  
  2.   
  3.     public static void main(String[] args) {  
  4.         Provider provider = new SendMailFactory();  
  5.         Sender sender = provider.produce();  
  6.         sender.Send();  
  7.     }  
  8. }  

In fact, the advantage of this mode is that if you want to add a function: send timely information, you just need to do an implementation class, implement Sender interface, and a factory class, implement Provider interface, OK, no need to change the existing code. In this way, the expansion is better!

3. Singleton

Singleton is a common design pattern. In Java applications, a singleton object ensures that only one instance of the object exists in a JVM. Such a model has several advantages:

1. Some classes are created frequently. For some large objects, this is a huge system overhead.

2. The new operator is omitted, which reduces the frequency of system memory usage and the pressure of GC.

3. Some classes, such as the core trading engine of an exchange, control the trading process. If this class can create more than one, the system is completely chaotic. (For example, when multiple commanders are in command at the same time in an army, they are bound to be in a mess), so only using the singleton mode can the core transaction server control the whole process independently.

First, let's write a simple singleton class:

  1. public class Singleton {  
  2.   
  3.     /* Hold private static instances to prevent references, where null is assigned to achieve delayed loading*/  
  4.     private static Singleton instance = null;  
  5.   
  6.     /* Private construction methods to prevent instantiation*/  
  7.     private Singleton() {  
  8.     }  
  9.   
  10.     /* Static Engineering Method, Create Examples*/  
  11.     public static Singleton getInstance() {  
  12.         if (instance == null) {  
  13.             instance = new Singleton();  
  14.         }  
  15.         return instance;  
  16.     }  
  17.   
  18.     /* If the object is used for serialization, the consistency of the object before and after serialization can be ensured.*/  
  19.     public Object readResolve() {  
  20.         return instance;  
  21.     }  
  22. }  


This class can satisfy the basic requirements, but if we put it into a multi-threaded environment without thread security protection, there will be problems. How to solve them? We first think of adding synchronized keywords to the getInstance method, as follows:

  1. public static synchronized Singleton getInstance() {  
  2.         if (instance == null) {  
  3.             instance = new Singleton();  
  4.         }  
  5.         return instance;  
  6.     }  

However, the synchronized keyword locks the object. This usage will degrade in performance because every call to getInstance() locks the object. In fact, it is only necessary to lock the object when it is first created, and then it is not needed. Therefore, this place needs to be improved. Let's change it to the following one:

  1. public static Singleton getInstance() {  
  2.         if (instance == null) {  
  3.             synchronized (instance) {  
  4.                 if (instance == null) {  
  5.                     instance = new Singleton();  
  6.                 }  
  7.             }  
  8.         }  
  9.         return instance;  
  10.     }  

It seems to solve the problem mentioned earlier, adding synchronized keywords to the inside, that is to say, there is no need to lock when calling, only when instance is null, and when creating objects, it needs to lock, and the performance has been improved. However, this situation may be problematic, as follows: creating objects and assigning operations in Java instructions are done separately, that is, instance = new Singleton(); statements are executed in two steps. However, JVM does not guarantee the order of these two operations, that is to say, it is possible for JVM to allocate space for the new Singleton instance, then assign it directly to the instance member, and then initialize the Singleton instance. This may lead to errors. Let's take threads A and B as examples.

Threads a > A and B enter the first if judgment at the same time

B > A first enters the synchronized block, because instance is null, it executes instance = new Singleton();

C > Because of the optimization mechanism within the JVM, the JVM first draws some blank memory allocated to the Singleton instance and assigns it to the instance member (note that the JVM does not start initializing the instance at this time), then A leaves the synchronized block.

D > B enters the synchronized block. Because instance is not null at this time, it immediately leaves the synchronized block and returns the result to the program calling the method.

E > At this point, the B thread intends to use the Singleton instance, but finds that it has not been initialized, and the error occurs.

So it is possible for the program to make mistakes. In fact, the running process of the program is very complicated. From this point, we can see that, especially in the multi-threaded environment, the program is more difficult and challenging. We further optimize the program:

  1. private static class SingletonFactory{           
  2.         private static Singleton instance = new Singleton();           
  3.     }           
  4.     public static Singleton getInstance(){           
  5.         return SingletonFactory.instance;           
  6.     }   

In fact, the singleton mode uses internal classes to maintain the implementation of singletons, and the internal mechanism of JVM can ensure that when a class is loaded, the loading process of this class is threaded mutually exclusive. So when we first call getInstance, JVM can help us ensure that instances are created only once, and that the memory assigned to instances is initialized, so that we don't have to worry about the above problems. At the same time, this method only uses the mutex mechanism in the first call, which solves the problem of low performance. So let's summarize a perfect singleton model for the time being:

  1. public class Singleton {  
  2.   
  3.     /* Private construction methods to prevent instantiation*/  
  4.     private Singleton() {  
  5.     }  
  6.   
  7.     /* An internal class is used here to maintain the singleton.*/  
  8.     private static class SingletonFactory {  
  9.         private static Singleton instance = new Singleton();  
  10.     }  
  11.   
  12.     /* Get examples*/  
  13.     public static Singleton getInstance() {  
  14.         return SingletonFactory.instance;  
  15.     }  
  16.   
  17.     /* If the object is used for serialization, the consistency of the object before and after serialization can be ensured.*/  
  18.     public Object readResolve() {  
  19.         return getInstance();  
  20.     }  
  21. }  

In fact, it is perfect, and not necessarily, if an exception is thrown in the constructor, the instance will never be created and error will occur. So, there is no perfect thing, we can only choose the most suitable implementation method for our application scenario according to the actual situation. Others do this: because we only need to synchronize when creating classes, it is also possible to separate the creation from getInstance() and add synchronized keywords to the creation alone:

  1. public class SingletonTest {  
  2.   
  3.     private static SingletonTest instance = null;  
  4.   
  5.     private SingletonTest() {  
  6.     }  
  7.   
  8.     private static synchronized void syncInit() {  
  9.         if (instance == null) {  
  10.             instance = new SingletonTest();  
  11.         }  
  12.     }  
  13.   
  14.     public static SingletonTest getInstance() {  
  15.         if (instance == null) {  
  16.             syncInit();  
  17.         }  
  18.         return instance;  
  19.     }  
  20. }  

Considering performance, the whole program only needs to create an instance once, so performance will not have any impact.

Supplementary: Synchronized updating of attributes for individual objects by "shadow instances"

  1. public class SingletonTest {  
  2.   
  3.     private static SingletonTest instance = null;  
  4.     private Vector properties = null;  
  5.   
  6.     public Vector getProperties() {  
  7.         return properties;  
  8.     }  
  9.   
  10.     private SingletonTest() {  
  11.     }  
  12.   
  13.     private static synchronized void syncInit() {  
  14.         if (instance == null) {  
  15.             instance = new SingletonTest();  
  16.         }  
  17.     }  
  18.   
  19.     public static SingletonTest getInstance() {  
  20.         if (instance == null) {  
  21.             syncInit();  
  22.         }  
  23.         return instance;  
  24.     }  
  25.   
  26.     public void updateProperties() {  
  27.         SingletonTest shadow = new SingletonTest();  
  28.         properties = shadow.getProperties();  
  29.     }  
  30. }  

Through the study of the singleton model, we can learn that:

1. The singleton model is easy to understand, but it is still difficult to implement.

2. The synchronized keyword locks the object. When it is used, it must be used in the right place. (Note the objects and processes that need to be locked, sometimes not the whole object and the whole process need to be locked.)

At this point, the singleton mode has been basically finished. At the end, the author suddenly thought of another problem, that is, using the static method of classes to achieve the effect of singleton mode is also feasible. What is the difference between the two?

First, static classes cannot implement interfaces. (From a class point of view, yes, but that destroys the static state. Because static modification is not allowed in the interface, it is non-static even if implemented.

Secondly, singletons can be delayed initialization, and static classes are usually initialized at the first load. The reason for delayed loading is that some classes are relatively large, so delayed loading helps improve performance.

Third, a singleton class can be inherited and its methods can be overridden. However, the internal methods of static classes are static and cannot be overridden.

Finally, the singleton class is more flexible. After all, it is only a common Java class. As long as it meets the basic needs of the singleton, you can implement some other functions as you like, but the static class is not. From the above generalizations, we can basically see the difference between the two, but on the other hand, the singleton pattern that we finally implemented above is implemented by a static class internally. Therefore, the two are closely related, but the level we consider is different. The combination of the two ideas can create a perfect solution, just like HashMap using array + linked list to achieve, in fact, many things in life are like this, using different methods to deal with problems, there are always advantages and disadvantages, the most perfect way is to combine the advantages of each method, in order to solve the best problem!

4. Builder Model

The factory class pattern provides the pattern of creating a single class, while the builder pattern manages all kinds of products together to create composite objects. The so-called composite objects refer to a class with different attributes. In fact, the builder pattern is the combination of the former abstract factory pattern and the last Test. Let's look at the code:

As before, a Sender interface, two implementation classes MailSender and SmithSender. Finally, the categories of builders are as follows:

  1. public class Builder {  
  2.       
  3.     private List<Sender> list = new ArrayList<Sender>();  
  4.       
  5.     public void produceMailSender(int count){  
  6.         for(int i=0; i<count; i++){  
  7.             list.add(new MailSender());  
  8.         }  
  9.     }  
  10.       
  11.     public void produceSmsSender(int count){  
  12.         for(int i=0; i<count; i++){  
  13.             list.add(new SmsSender());  
  14.         }  
  15.     }  
  16. }  

Test class:

  1. public class Test {  
  2.   
  3.     public static void main(String[] args) {  
  4.         Builder builder = new Builder();  
  5.         builder.produceMailSender(10);  
  6.     }  
  7. }  

From this point of view, the builder pattern integrates many functions into a class that can create more complex things. So the difference from the engineering model is that the factory model focuses on creating a single product, while the builder model focuses on creating conforming objects and parts. Therefore, the choice of factory mode or builder mode depends on the actual situation.

5. Prototype

Although the prototype pattern is a creation pattern, it has nothing to do with the engineering pattern. From the name, we can see that the idea of this pattern is to copy and clone an object as a prototype, and produce a new object similar to the original object. This summary will explain through the replication of the object. In Java, clone() is used to replicate objects. First, a prototype class is created.

  1. public class Prototype implements Cloneable {  
  2.   
  3.     public Object clone() throws CloneNotSupportedException {  
  4.         Prototype proto = (Prototype) super.clone();  
  5.         return proto;  
  6.     }  
  7. }  

Simply, a prototype class only needs to implement Cloneable interface and override clone method, where clone method can be changed to any name, because Cloneable interface is an empty interface, you can arbitrarily define the method name of implementation class, such as cloneA or cloneB, because the emphasis here is super.clone(), which calls clone() method of Object, and clone() method of Object. In class, clone() is native. How to implement it? I'll go into another article about interpreting calls to native methods in Java, but I won't go into it here. Here, I will talk about shallow and deep replication of objects. First of all, we need to understand the concept of deep and shallow replication of objects.

Shallow replication: After an object is replicated, the variables of the basic data type are recreated, and the reference type refers to the original object.

Deep copy: After copying an object, both the basic data type and the reference type are recreated. Simply put, deep copy is completely and thoroughly duplicated, while shallow copy is not thoroughly duplicated.

Here, write an example of deep and shallow replication:

  1. public class Prototype implements Cloneable, Serializable {  
  2.   
  3.     private static final long serialVersionUID = 1L;  
  4.     private String string;  
  5.   
  6.     private SerializableObject obj;  
  7.   
  8.     /* Shallow copy*/  
  9.     public Object clone() throws CloneNotSupportedException {  
  10.         Prototype proto = (Prototype) super.clone();  
  11.         return proto;  
  12.     }  
  13.   
  14.     /* Deep replication*/  
  15.     public Object deepClone() throws IOException, ClassNotFoundException {  
  16.   
  17.         /* Write a binary stream to the current object*/  
  18.         ByteArrayOutputStream bos = new ByteArrayOutputStream();  
  19.         ObjectOutputStream oos = new ObjectOutputStream(bos);  
  20.         oos.writeObject(this);  
  21.   
  22.         /* Read out new objects generated by binary streams*/  
  23.         ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());  
  24.         ObjectInputStream ois = new ObjectInputStream(bis);  
  25.         return ois.readObject();  
  26.     }  
  27.   
  28.     public String getString() {  
  29.         return string;  
  30.     }  
  31.   
  32.     public void setString(String string) {  
  33.         this.string = string;  
  34.     }  
  35.   
  36.     public SerializableObject getObj() {  
  37.         return obj;  
  38.     }  
  39.   
  40.     public void setObj(SerializableObject obj) {  
  41.         this.obj = obj;  
  42.     }  
  43.   
  44. }  
  45.   
  46. class SerializableObject implements Serializable {  
  47.     private static final long serialVersionUID = 1L;  
  48. }  

In order to achieve deep replication, it is necessary to read the binary input of the current object in the form of stream, and then write out the corresponding object of binary data.

Study Java Students pay attention!!!  
If you encounter any problems or want to acquire learning resources, you are welcome to join us. Java Learning Exchange Group, Group Number: 456544752. Let's learn Java together!

Posted by SkyRanger on Mon, 01 Apr 2019 09:00:31 -0700