I have always wanted to write an article introducing design patterns so that readers can read it quickly, understand it as soon as they read it, and use it when they understand it. At the same time, they will not confuse various patterns. I think this article is well written 😂😂😂, Spent a lot of effort to write this article and make pictures, and strive to make readers really look at simplicity and gain something at the same time.
Design pattern is a high-level abstract summary of various codes written in your actual work. Among them, the most famous is the classification of Gang of Four (GoF). They classify design patterns into 23 classic patterns. According to the purpose, we can divide them into three categories, namely creation pattern, structural pattern and behavior pattern.
There are some important design principles to share with you at the beginning. These principles will run through the full text:
- Interface oriented programming, not implementation oriented. This is very important and the first step in elegant and extensible code, so I don't need to say more.
- The principle of single responsibility. Each class should have a single function, and that function should be completely encapsulated by this class.
- It is closed for modification and open for extension. To modify and close means that the functions to be implemented and the bug s to be repaired have been completed for the code we have worked hard to write, and others can't change it; It's easier to understand the opening of extensions, that is, it's easy to implement extensions on the basis of our written code.
The creation mode is relatively simple, but it will be more boring. The structure type and behavior type are more interesting.
1. Create mode
The function of the creation mode is to create an object. When it comes to creating an object, the most familiar thing is to new an object and then set the related attributes. However, in many scenarios, we need to provide the client with a more friendly way to create objects, especially when we define classes but need to provide them to other developers.
1.1. Simple factory mode
As simple as the name, it's very simple. Go directly to the code:
public class FoodFactory { public static Food makeFood(String name) { if (name.equals("noodle")) { Food noodle = new LanZhouNoodle(); noodle.addSpicy("more"); return noodle; } else if (name.equals("chicken")) { Food chicken = new HuangMenChicken(); chicken.addCondiment("potato"); return chicken; } else { return null; } } }
Among them, Lanzhou noodle and HuangMenChicken inherit from Food.
In short, the simple factory pattern is usually like this. A factory class XxxFactory has a static method that returns different instance objects derived from the same parent class (or implementing the same interface) according to our different parameters.
We emphasize the principle of single responsibility. A class only provides one function. The function of FoodFactory is to produce all kinds of Food.
1.2. Factory mode
The simple factory model is very simple. If it can meet our needs, I don't think we should bother. The reason why we need to introduce the factory model is that we often need to use two or more factories.
public interface FoodFactory { Food makeFood(String name); } public class ChineseFoodFactory implements FoodFactory { @Override public Food makeFood(String name) { if (name.equals("A")) { return new ChineseFoodA(); } else if (name.equals("B")) { return new ChineseFoodB(); } else { return null; } } } public class AmericanFoodFactory implements FoodFactory { @Override public Food makeFood(String name) { if (name.equals("A")) { return new AmericanFoodA(); } else if (name.equals("B")) { return new AmericanFoodB(); } else { return null; } } }
Among them, Chinese Food, Chinese Food B, American Food and American Food B are all derived from Food.
Client call:
public class APP { public static void main(String[] args) { // Select a specific factory first FoodFactory factory = new ChineseFoodFactory(); // The factory in the first step generates specific objects, and different factories create different objects Food food = factory.makeFood("A"); } }
Although makeFood("A") is used to produce class A food, it is completely different from that produced by different factories.
In the first step, we need to select a suitable factory, and then the second step is basically the same as a simple factory.
The core is that we need to choose the factory we need in the first step. For example, we have a LogFactory interface, and the implementation classes are FileLogFactory and KafkaLogFactory, which correspond to writing logs to files and Kafka respectively. Obviously, in the first step, our client needs to decide whether to instantiate FileLogFactory or KafkaLogFactory, which will determine all subsequent operations.
Although it is simple, I also draw all the components on one diagram, so that the reader can see it clearly:
picture
1.3. Abstract factory pattern
When it comes to product families, it is necessary to introduce the abstract factory pattern.
A classic example is building a computer. Let's not introduce the abstract factory pattern and see how to implement it.
Because the computer is composed of many components, we abstract the CPU and the motherboard, and then the CPU is produced by CPUFactory and the motherboard is produced by MainBoardFactory. Then, we combine the CPU and the motherboard together, as shown in the following figure:
picture
The client call at this time is as follows:
// Get Intel CPU CPUFactory cpuFactory = new IntelCPUFactory(); CPU cpu = intelCPUFactory.makeCPU(); // Get AMD motherboard MainBoardFactory mainBoardFactory = new AmdMainBoardFactory(); MainBoard mainBoard = mainBoardFactory.make(); // Assembling CPU and motherboard Computer computer = new Computer(cpu, mainBoard);
Look at the CPU factory and motherboard factory separately. They are the factory mode we mentioned earlier. This method is also easy to expand, because if you want to add a hard disk to the computer, you only need to add a HardDiskFactory and the corresponding implementation, without modifying the existing factory.
However, there is a problem with this method, that is, if the CPU made by Intel and the motherboard made by AMD are not compatible, the code is prone to error, because the client does not know that they are incompatible, and there will be arbitrary combination by mistake.
The following is the concept of product family, which represents a set of accessories that make up a product:
picture
When it comes to the problem of this product family, it needs to be supported by the abstract factory pattern. We no longer define CPU factories, mainboard factories, hard disk factories, display screen factories, etc. we directly define computer factories. Each computer factory is responsible for producing all equipment, which can ensure that there is no compatibility problem.
picture
At this time, for the client, it is no longer necessary to select CPU manufacturers, motherboard manufacturers, hard disk manufacturers, etc. directly select a brand factory. The brand factory will be responsible for producing all things and ensure that they are compatible and available.
public static void main(String[] args) { // The first step is to select a "big factory" ComputerFactory cf = new AmdFactory(); // Make CPU from this big factory CPU cpu = cf.makeCPU(); // Make motherboards from this big factory MainBoard board = cf.makeMainBoard(); // Make hard drives from this big factory HardDisk hardDisk = cf.makeHardDisk(); // Assemble the CPU, motherboard and hard disk from the same factory Computer result = new Computer(cpu, board, hardDisk); }
Of course, the problem of abstract factories is also obvious. For example, if we want to add a display, we need to modify all factories and add the method of manufacturing displays to all factories. This is a bit against the design principle of closing to modifications and opening to extensions.
1.4. Singleton mode
Singleton mode is used the most and made the most mistakes.
The hungry man mode is the simplest:
public class Singleton { // First, block new Singleton() private Singleton() {}; // Creating a private static instance means that this class will be created when it is first used private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } // Write a static method. What we want to say here is that if we just want to call Singleton.getDate(...), // Originally, I didn't want to generate a Singleton instance, but I can't help it. It has been generated public static Date getDate(String mode) }
Many people can tell the disadvantages of the hungry man mode, but I think this is rarely encountered in the production process: you define a singleton class without its instance, but you plug one or several static methods you will use into this class.
The most error prone mode:
public class Singleton { // First, block the road of new Singleton() private Singleton() {} // Compared with the hungry man mode, there is no need to instantiate it first. Note the volatile here, which is necessary private static volatile Singleton instance = null; public static Singleton getInstance() { if (instance == null) { // Lock synchronized (Singleton.class) { // This judgment is also necessary, otherwise there will be concurrency problems if (instance == null) { instance = new Singleton(); } } } return instance; } }
Double check refers to checking whether instance is null twice. volatile is needed here. I hope it can attract the attention of readers. Many people don't know how to write. They just add synchronized to the getInstance() method signature. That's enough. The performance is too poor.
Nested classes are the most classic. Let's use them in the future:
public class Singleton3 { private Singleton3() {} // It mainly uses nested classes to access the static properties and static methods of external classes private static class Holder { private static Singleton3 instance = new Singleton3(); } public static Singleton3 getInstance() { return Holder.instance; } }
Note that many people will describe this nested class as a static inner class. Strictly speaking, inner classes and nested classes are different, and they can access different permissions of external classes.
Finally, let's talk about enumeration. Enumeration is very special. It initializes all instances when the class is loaded, and the JVM ensures that they will not be instantiated again, so it is inherently singleton.
Although we seldom see enumerations to implement singletons, enumerations are used in many places in the source code of RxJava.
1.5. Builder mode
The classes of xbuilder that we often meet are usually the product of the builder pattern. In fact, there are many variants of the builder mode, but for the client, we usually use the same mode:
Food food = new FoodBuilder().a().b().c().build(); Food food = Food.builder().a().b().c().build();
The routine is to create a new Builder first, then call a bunch of methods in a chain, and finally call the build() method again, and we have the objects we need.
A regular builder model:
class User { // Here are the properties of "a pile" private String name; private String password; private String nickName; private int age; // The constructor is privatized, or the client will call the constructor directly private User(String name, String password, String nickName, int age) { this.name = name; this.password = password; this.nickName = nickName; this.age = age; } // Static methods are used to generate a Builder. This is not necessary, but it is a good habit to write this method, // Some code requires others to write new User.UserBuilder().a()...build() doesn't look so good public static UserBuilder builder() { return new UserBuilder(); } public static class UserBuilder { // As like as two peas, User is a bunch of attributes. private String name; private String password; private String nickName; private int age; private UserBuilder() { } // The chain call sets the value of each attribute and returns this, that is, UserBuilder public UserBuilder name(String name) { this.name = name; return this; } public UserBuilder password(String password) { this.password = password; return this; } public UserBuilder nickName(String nickName) { this.nickName = nickName; return this; } public UserBuilder age(int age) { this.age = age; return this; } // The build() method is responsible for "copying" the properties set in UserBuilder to User. // Of course, you can do some checking before "copying" public User build() { if (name == null || password == null) { throw new RuntimeException("User name and password are required"); } if (age <= 0 || age >= 150) { throw new RuntimeException("Illegal age"); } // You can also assign the function of "default value" if (nickName == null) { nickName = name; } return new User(name, password, nickName, age); } } }
The core is: first set all properties to Builder, and then copy these properties to the actual object when building () method.
Look at the call of the client:
public class APP { public static void main(String[] args) { User d = User.builder() .name("foo") .password("pAss12345") .age(25) .build(); } }
To tell you the truth, the chain writing of the builder mode is very attractive, but it has written a lot of "useless" However, when there are many properties, some are required and some are optional, this mode will make the code much clearer. We can force the caller to provide required fields in the builder construction method, and in build() The code for checking various parameters in the method is more elegant than that in the User's construction method.
As an aside, readers are strongly recommended to use lombok. After using lombok, a lot of codes above will become as follows:
@Builder class User { private String name; private String password; private String nickName; private int age; }
How about saving time to do something else.
Of course, if you only want chained writing and do not want the builder mode, there is a very simple way. The getter method of the User remains unchanged. All setter methods can return this, and then you can call it as follows:
User user = new User().setName("").setPassword("").setAge(20);
Many people use it like this, but the author thinks that this writing method is not elegant and is not recommended.
1.6. Prototype mode
This is the last design pattern I want to talk about creating patterns.
The prototype pattern is very simple: there is a prototype instance, and a new instance is generated based on this prototype instance, that is, "clone".
There is a clone() method in the Object class, which is used to generate a new Object. Of course, if we want to call this method, java requires our class to implement the clonable interface first. This interface does not define any methods, but if we do not do so, CloneNotSupportedException will be thrown during clone().
protected native Object clone() throws CloneNotSupportedException;
java cloning is a shallow cloning. When an object reference is encountered, the cloned object and the reference in the original object will point to the same object. Usually, the method of deep cloning is to serialize the object and then deserialize it.
I think it's enough to understand the prototype pattern here. It's meaningless to say that this code or that code is a prototype pattern.
1.7. Summary of creation mode
Creating patterns is generally simple. Their function is to generate instance objects. It is the first step of all kinds of work. Because we write object-oriented code, of course, our first step is to create an object.
The simple factory mode is the simplest. The factory mode adds the dimension of selecting factories on the basis of the simple factory mode. The first step is to select appropriate factories. The abstract factory mode has the concept of product family. If each product has compatibility problems, the abstract factory mode should be used. The single example mode is not mentioned. In order to ensure that the same Object is used globally, on the one hand It is for security reasons. On the one hand, it is to save resources. The builder pattern is designed to deal with the type with many attributes in order to make the code more beautiful. The prototype pattern is used at least. You can understand the knowledge related to the clone() method in the Object class.
2. Structural mode
The previous creation pattern introduces some design patterns for creating objects. The structural pattern introduced in this section aims to achieve decoupling by changing the code structure, making our code easy to maintain and expand.
2.1. Agent mode
The first proxy pattern to be introduced is one of the most commonly used patterns. A proxy is used to hide the implementation details of specific implementation classes. It is usually used to add some logic before and after the real implementation.
Since it is an agent, it is necessary to hide the real implementation from the client, and the agent is responsible for all requests of the client. Of course, the agent is just an agent. It will not complete the actual business logic, but a layer of skin. However, for the client, it must be the real implementation required by the client.
Understanding the word agent, this model is actually simple.
public interface FoodService { Food makeChicken(); Food makeNoodle(); } public class FoodServiceImpl implements FoodService { public Food makeChicken() { Food f = new Chicken() f.setChicken("1kg"); f.setSpicy("1g"); f.setSalt("3g"); return f; } public Food makeNoodle() { Food f = new Noodle(); f.setNoodle("500g"); f.setSalt("5g"); return f; } } // The agent needs to be "like" a real implementation class, so it needs to implement FoodService public class FoodServiceProxy implements FoodService { // There must be a real implementation class inside. Of course, it can also be injected through construction methods private FoodService foodService = new FoodServiceImpl(); public Food makeChicken() { System.out.println("We're about to start making chicken"); // If we define this sentence as the core code, then the core code is made by the real implementation class, // Agents just do "insignificant" things before and after the core code Food food = foodService.makeChicken(); System.out.println("The chicken is done. Add some pepper"); // enhance food.addCondiment("pepper"); return food; } public Food makeNoodle() { System.out.println("Prepare Ramen~"); Food food = foodService.makeNoodle(); System.out.println("The production is finished") return food; } }
For client calls, note that we need to instantiate the interface with a proxy:
// Proxy classes are used here to instantiate FoodService foodService = new FoodServiceProxy(); foodService.makeChicken();
picture
We found that no, the agent model is to do "method packaging" or "method enhancement". In aspect oriented programming, it is actually the process of dynamic agent. For example, in Spring, we do not define proxy classes ourselves, but Spring will help us define the proxy dynamically, and then dynamically add the code logic we defined in @ Before, @ After, @ Around to the proxy.
When it comes to dynamic proxy, we can also expand that there are two ways to implement dynamic proxy in spring. One is that if our class defines interfaces, such as UserService interface and UserServiceImpl implementation, we use JDK dynamic proxy. Interested readers can see the source code of java.lang.reflect.Proxy class; The other is that we do not define an interface ourselves. Spring will use CGLIB for dynamic proxy. It is a jar package with good performance.
2.2. Adapter mode
After talking about the proxy mode and the adapter mode, it is because they are very similar. Here we can make a comparison.
What the adapter mode does is that there is an interface to be implemented, but our existing objects are not satisfied. We need to add a layer of adapter for adaptation.
Generally speaking, there are three adapter modes: default adapter mode, object adapter mode and class adapter mode. Don't rush to distinguish these. Let's look at the examples first.
2.2.1. Default adapter modeFirst, let's take a look at the simplest adapter mode * * Default Adapter) *.
We use the FileAlterationListener in the Apache commons IO package as an example. This interface defines many methods for monitoring files or folders. Once the corresponding operation occurs, the corresponding method will be triggered.
public interface FileAlterationListener { void onStart(final FileAlterationObserver observer); void onDirectoryCreate(final File directory); void onDirectoryChange(final File directory); void onDirectoryDelete(final File directory); void onFileCreate(final File file); void onFileChange(final File file); void onFileDelete(final File file); void onStop(final FileAlterationObserver observer); }
A big problem with this interface is that there are too many abstract methods. If we want to use this interface, it means that we need to implement each abstract method. If we just want to monitor file creation and file deletion events in folders, we still have to implement all methods. Obviously, this is not what we want.
Therefore, we need the following adapter to implement the above interface, but all methods are empty methods. In this way, we can instead define our own class to inherit the following class.
public class FileAlterationListenerAdaptor implements FileAlterationListener { public void onStart(final FileAlterationObserver observer) { } public void onDirectoryCreate(final File directory) { } public void onDirectoryChange(final File directory) { } public void onDirectoryDelete(final File directory) { } public void onFileCreate(final File file) { } public void onFileChange(final File file) { } public void onFileDelete(final File file) { } public void onStop(final FileAlterationObserver observer) { } }
For example, we can define the following classes. We only need to implement the methods we want to implement:
public class FileMonitor extends FileAlterationListenerAdaptor { public void onFileCreate(final File file) { // File creation doSomething(); } public void onFileDelete(final File file) { // File deletion doSomething(); } }
Of course, the above is only one of the adapter modes, and it is also the simplest one. There is no need to say more. Next, let's introduce the "orthodox" adapter mode.
2.2.2. Object adapter modeLet's take a look at an example in the Head First design pattern. I modified it a little to see how to adapt the chicken to the duck, so that the chicken can also be used as a duck. Because we don't have an appropriate implementation class for this interface, we need an adapter.
public interface Duck { public void quack(); // The quack of a duck public void fly(); // fly } public interface Cock { public void gobble(); // The cooing of a chicken public void fly(); // fly } public class WildCock implements Cock { public void gobble() { System.out.println("Coo"); } public void fly() { System.out.println("Chickens can fly, too"); } }
The duck interface has two methods, fly() and square (). If chicken Cock wants to impersonate duck, the fly() method is ready-made, but chicken can't quack like duck. There is no square () method. Adaptation is needed at this time:
// There is no doubt that first of all, the adapter must need implements Duck to be used as a duck public class CockAdapter implements Duck { Cock cock; // A chicken instance is required in the construction method. This class is used to adapt the chicken to a duck public CockAdapter(Cock cock) { this.cock = cock; } // Method for realizing duck quack @Override public void quack() { // Inside is actually the cooing of a chicken cock.gobble(); } @Override public void fly() { cock.fly(); } }
The client call is simple:
public static void main(String[] args) { // There is a pheasant Cock wildCock = new WildCock(); // Successfully adapted pheasant to duck Duck duck = new CockAdapter(wildCock); ... }
Here, you will know what the adapter mode is. It's just that we need a duck, but we only have a chicken. At this time, we need to define an adapter to act as a duck, but the methods in the adapter are still implemented by the chicken.
Let's use a diagram to briefly illustrate the following:
picture
The above figure should still be easy to understand, so I won't explain more. Next, let's look at how the class adaptation pattern works.
2.2.3. Class adapter modeCut the crap and go straight to the figure above:
picture
It should be easy for you to understand this figure. Through the inherited method, the adapter automatically obtains most of the required methods. At this time, it is easier to use the client directly. Target t = new SomeAdapter(); That's it.
2.2.4. Adapter mode summary- Similarities and differences between class adaptation and object adaptation One adopts inheritance and the other adopts combination; Class adaptation belongs to static implementation, object adaptation belongs to dynamic implementation of composition, and object adaptation needs to instantiate one more object. Generally speaking, object adaptation is used more.
- Similarities and differences between adapter mode and agent mode Comparing these two modes is actually comparing the object adapter mode and the proxy mode. In terms of code structure, they are very similar, and both need a specific implementation class instance. However, their purposes are different. What the agent mode does is to enhance the vitality of the original method; The adapter does the adaptation work to provide "packaging the chicken into a duck and then use it as a duck", and there is no inheritance relationship between the chicken and the duck.
picture
2.3. Bridge mode
Understanding bridge patterns is actually understanding code abstraction and decoupling.
We first need a bridge, which is an interface that defines the interface methods provided.
public interface DrawAPI { public void draw(int radius, int x, int y); }
Then there are a series of implementation classes:
public class RedPen implements DrawAPI { @Override public void draw(int radius, int x, int y) { System.out.println("Draw with red strokes, radius:" + radius + ", x:" + x + ", y:" + y); } } public class GreenPen implements DrawAPI { @Override public void draw(int radius, int x, int y) { System.out.println("Draw with green strokes, radius:" + radius + ", x:" + x + ", y:" + y); } } public class BluePen implements DrawAPI { @Override public void draw(int radius, int x, int y) { System.out.println("Draw with blue strokes, radius:" + radius + ", x:" + x + ", y:" + y); } }
Define an abstract class whose implementation classes need to use the DrawAPI:
public abstract class Shape { protected DrawAPI drawAPI; protected Shape(DrawAPI drawAPI) { this.drawAPI = drawAPI; } public abstract void draw(); }
Define subclasses of abstract classes:
// circular public class Circle extends Shape { private int radius; public Circle(int radius, DrawAPI drawAPI) { super(drawAPI); this.radius = radius; } public void draw() { drawAPI.draw(radius, 0, 0); } } // rectangle public class Rectangle extends Shape { private int x; private int y; public Rectangle(int x, int y, DrawAPI drawAPI) { super(drawAPI); this.x = x; this.y = y; } public void draw() { drawAPI.draw(0, x, y); } }
Finally, let's look at the client demonstration:
public static void main(String[] args) { Shape greenCircle = new Circle(10, new GreenPen()); Shape redRectangle = new Rectangle(4, 8, new RedPen()); greenCircle.draw(); redRectangle.draw(); }
You may not see the above step by step clearly. I integrate all things into one picture:
picture
This time you should know where the abstraction is and how to decouple it. The advantage of bridge mode is also obvious, that is, it is very easy to expand.
This section references https://www.tutorialspoint.com/design_pattern/bridge_pattern.htm and modified it.
2.4. Decoration mode
It is not easy to make the decoration pattern clear. Maybe readers know that several classes in Java IO are typical decorative pattern applications, but readers may not know the relationship between them. Maybe they forget after reading this section. I hope readers can have a deeper understanding of it after reading this section.
First, let's look at a simple diagram. When looking at this diagram, we can understand the following hierarchy:
picture
Let's talk about the starting point of the decoration mode. As can be seen from the figure, the interface Component actually has two implementation classes: ConcreteComponentA and ConcreteComponentB. However, if we want to enhance these two implementation classes, we can adopt the decoration mode and decorate the implementation classes with specific decorators to achieve the purpose of enhancement.
Briefly explain the decorator from the name. Since it is decoration, it is often to add small functions. Moreover, we should meet the requirement that multiple small functions can be added. In the simplest way, the agent mode can enhance functions, but it is not easy for the agent to enhance multiple functions. Of course, you can say that the multi-layer packaging method of using the agent to wrap the agent, but in that case, the code will be complex.
First, understand some simple concepts. From the figure, we can see that all concrete decorators * can be used as components because they all implement all interfaces in components. The difference between them and the Component implementation class ConcreteComponent * is that they are just decorators and play a decorative role, that is, even if they look awesome, they are only decorated with layers in the specific implementation.
Pay attention to the Component and Decorator mixed in various nouns in this paragraph. Don't get confused.
Let's take a look at an example. First clarify the decoration mode, and then introduce the application of decoration mode in java io.
Recently, "happy lemon" has become popular in the street. We divide the drinks of happy lemon into three categories: black tea, green tea and coffee. On the basis of these three categories, many flavors have been added, such as kumquat lemon black tea, kumquat lemon pearl green tea, mango black tea, mango pearl black tea, roasted pearl black tea, roasted pearl mango green tea, coconut germ coffee Caramel, cocoa, coffee, etc. each store has a long menu, but look carefully. In fact, there are few raw materials, but they can match many combinations. If customers need, they can also make many drinks that do not appear in the menu.
In this example, black tea, green tea and coffee are the most basic drinks, while others such as kumquat, lemon, mango, pearl, coconut and caramel are used for decoration. Of course, in the development, we can develop these categories like stores: lemon blacktea, lemon greentea, mango blacktea, mango lemon greentea... However, we soon found that this is definitely not possible, which will lead to the need to combine all the possibilities. What if the guest needs to add double lemon to the black tea? What about three lemons?
Stop talking nonsense and code.
First, define the beverage abstract base class:
public abstract class Beverage { // Return description public abstract String getDescription(); // Return price public abstract double cost(); }
Then there are three basic beverage implementation categories, black tea, green tea and coffee:
public class BlackTea extends Beverage { public String getDescription() { return "black tea"; } public double cost() { return 10; } } public class GreenTea extends Beverage { public String getDescription() { return "Green Tea"; } public double cost() { return 11; } } ...// Coffee omission
Define seasoning, that is, the base class of decorator. This class must inherit from Beverage:
// Seasoning public abstract class Condiment extends Beverage { }
Then let's define lemon, mango and other specific condiments. They belong to decorators. There is no doubt that these condiments must inherit the Condiment class:
public class Lemon extends Condiment { private Beverage bevarage; // It is very important to introduce specific drinks, such as black tea or green tea without decoration, // Of course, it can also be introduced into the decorated mango green tea, which can be made into mango lemon green tea public Lemon(Beverage bevarage) { this.bevarage = bevarage; } public String getDescription() { // decorate return bevarage.getDescription() + ", Add lemon"; } public double cost() { // decorate return beverage.cost() + 2; // It costs 2 yuan with lemon } } public class Mango extends Condiment { private Beverage bevarage; public Mango(Beverage bevarage) { this.bevarage = bevarage; } public String getDescription() { return bevarage.getDescription() + ", Add mango"; } public double cost() { return beverage.cost() + 3; // It costs 3 yuan to add mango } } ...// Add a class to each seasoning
Look at the client call:
public static void main(String[] args) { // First, we need a basic drink, black tea, green tea or coffee Beverage beverage = new GreenTea(); // Start decorating beverage = new Lemon(beverage); // Add a lemon first beverage = new Mongo(beverage); // Add another mango System.out.println(beverage.getDescription() + " Price:¥" + beverage.cost()); //"Green tea, lemon and mango price: ¥ 16" }
If we need mango pearl double lemon black tea:
Beverage beverage = new Mongo(new Pearl(new Lemon(new Lemon(new BlackTea()))));
Isn't it abnormal?
It may be clearer to look at the following figure:
picture
By now, everyone should have known the decoration mode.
Next, let's talk about the decoration mode in java IO. See the following figure for some classes derived from InputStream:
picture
We know that InputStream represents the input stream. The specific input sources can be file InputStream, PipedInputStream, ByteArrayInputStream, etc. These are like black tea and green tea in the previous example of milk tea, which belong to the basic input stream.
FilterInputStream undertakes the key nodes of the decoration mode. Its implementation class is a series of decorators. For example, BufferedInputStream represents decoration with buffer, which makes the input stream have the function of buffer. LineNumberInputStream represents decoration with line number, which can be obtained during operation. The decoration of DataInputStream, Enables us to convert from the input stream to basic type values in java.
Of course, in java IO, if we use decorators, it is not suitable for interface oriented programming, such as:
InputStream inputStream = new LineNumberInputStream(new BufferedInputStream(new FileInputStream("")));
As a result, InputStream still does not have the ability to read line numbers, because the method to read line numbers is defined in the LineNumberInputStream class.
We should use it like this:
DataInputStream is = new DataInputStream( new BufferedInputStream( new FileInputStream("")));
Therefore, it is difficult to find pure code that strictly conforms to the design pattern.
2.5. Facade mode
Facade Pattern (also known as Facade Pattern) is used in many source codes. For example slf4j, it can be understood as the application of Facade Pattern. This is a simple design pattern. Let's talk about it directly in the code.
First, we define an interface:
public interface Shape { void draw(); }
Define several implementation classes:
public class Circle implements Shape { @Override public void draw() { System.out.println("Circle::draw()"); } } public class Rectangle implements Shape { @Override public void draw() { System.out.println("Rectangle::draw()"); } }
Client call:
public static void main(String[] args) { // Draw a circle Shape circle = new Circle(); circle.draw(); // draw a rectangle Shape rectangle = new Rectangle(); rectangle.draw(); }
The above is the code we often write. If we need to draw a circle, we need to instantiate a circle first, and if we need to draw a rectangle, we need to instantiate a rectangle first, and then call the corresponding draw() method.
Next, let's see how to use facade mode to make client calls more friendly.
Let's define a facade first:
public class ShapeMaker { private Shape circle; private Shape rectangle; private Shape square; public ShapeMaker() { circle = new Circle(); rectangle = new Rectangle(); square = new Square(); } /** * The following defines a bunch of methods, and the specific methods that should be called are determined by this facade */ public void drawCircle(){ circle.draw(); } public void drawRectangle(){ rectangle.draw(); } public void drawSquare(){ square.draw(); } }
Let's see how the client calls:
public static void main(String[] args) { ShapeMaker shapeMaker = new ShapeMaker(); // Client calls are now clearer shapeMaker.drawCircle(); shapeMaker.drawRectangle(); shapeMaker.drawSquare(); }
The advantages of facade mode are obvious. The client no longer needs to pay attention to which implementation class should be used during instantiation. It can directly call the methods provided by the facade, because the method name of the methods provided by the facade class is already very friendly to the client.
2.6. Combination mode
Composite pattern is used to represent data with hierarchical structure, which makes our access to single object and composite object consistent.
Let's take a direct look at an example. Each employee has attributes such as name, department and salary, as well as a set of subordinate employees (although the set may be empty), and subordinate employees have the same structure as their own. They also have attributes such as name and department, as well as their set of subordinate employees.
public class Employee { private String name; private String dept; private int salary; private List<Employee> subordinates; // subordinate public Employee(String name,String dept, int sal) { this.name = name; this.dept = dept; this.salary = sal; subordinates = new ArrayList<Employee>(); } public void add(Employee e) { subordinates.add(e); } public void remove(Employee e) { subordinates.remove(e); } public List<Employee> getSubordinates(){ return subordinates; } public String toString(){ return ("Employee :[ Name : " + name + ", dept : " + dept + ", salary :" + salary+" ]"); } }
Usually, this class needs to define methods such as add(node), remove(node) and getChildren().
This is actually a combination model. I won't introduce this simple model too much. I believe readers don't like watching me write nonsense.
2.7. Yuan sharing mode
English is Flyweight Pattern. I don't know who translated the word first. It's really hard to understand. Let's try to associate it forcibly. Flyweight means lightweight. Separately, sharing elements means sharing components, that is, reusing generated objects. Of course, this approach is lightweight.
The simplest way to reuse objects is to use a HashMap to store each newly generated object. Every time you need an object, first go to the HashMap to see if there is one. If not, regenerate it into a new object, and then put the object into the HashMap.
I won't demonstrate this simple code.
2.8. Summary of structural mode
Earlier, we talked about agent mode, adapter mode, bridge mode, decoration mode, facade mode, combination mode and sharing mode. Can readers explain these models clearly? When talking about these patterns, do you have a clear picture or processing flow in mind?
The proxy mode is used for method enhancement. The adapter mode is used to adapt the interface by packaging the chicken into a duck. The bridge mode is well decoupled. The decoration mode can be seen from the name and is suitable for the scenes of decoration classes or enhanced classes. The advantage of the facade mode is that the client does not need to care about the instantiation process, but only needs to call the required methods, Composite mode is used to describe data with hierarchical structure. Meta mode is used to cache created objects in a specific scene to improve performance.
3. Behavioral model
Behavioral patterns focus on the interaction between various classes, and divide responsibilities clearly, making our code clearer.
3.1. Strategy mode
The policy pattern is too common, so it is introduced at the beginning. It's relatively simple. I don't talk nonsense. Just say things in code.
The following design scenario is that we need to draw a figure. The optional strategy is to draw with a red pen, a green pen, or a blue pen.
First, define a policy interface:
public interface Strategy { public void draw(int radius, int x, int y); }
Then we define several specific strategies:
public class RedPen implements Strategy { @Override public void draw(int radius, int x, int y) { System.out.println("Draw with red strokes, radius:" + radius + ", x:" + x + ", y:" + y); } } public class GreenPen implements Strategy { @Override public void draw(int radius, int x, int y) { System.out.println("Draw with green strokes, radius:" + radius + ", x:" + x + ", y:" + y); } } public class BluePen implements Strategy { @Override public void draw(int radius, int x, int y) { System.out.println("Draw with blue strokes, radius:" + radius + ", x:" + x + ", y:" + y); } }
Classes using policies:
public class Context { private Strategy strategy; public Context(Strategy strategy){ this.strategy = strategy; } public int executeDraw(int radius, int x, int y){ return strategy.draw(radius, x, y); } }
Client presentation:
public static void main(String[] args) { Context context = new Context(new BluePen()); // Use a green pen to draw context.executeDraw(10, 0, 0); }
Put it on a picture so that everyone can see it clearly:
picture
At this time, do you think of the bridge modes in the structural mode? They are actually very similar. I'll take the diagram of the bridge mode for comparison:
picture
If I say, they are very similar. The bridge pattern adds a layer of abstraction to the left. The coupling of bridge modes is lower and the structure is more complex.
3.2. Observer mode
The observer model can't be simpler for us. There are no more than two operations. Observers subscribe to topics they care about and notify observers of data changes.
First, you need to define topics. Each topic needs to hold a reference to the observer list to notify each observer when the data changes:
public class Subject { private List<Observer> observers = new ArrayList<Observer>(); private int state; public int getState() { return state; } public void setState(int state) { this.state = state; // The data has changed. Notify the observers notifyAllObservers(); } // Registered observer public void attach(Observer observer) { observers.add(observer); } // Inform the observers public void notifyAllObservers() { for (Observer observer : observers) { observer.update(); } } }
Define the observer interface:
public abstract class Observer { protected Subject subject; public abstract void update(); }
In fact, if there is only one observer class, the interface does not need to be defined. However, in normal scenarios, since the observer mode is used, we just hope that when an event comes out, there will be multiple different classes that need to process corresponding information. For example, in the event of successful order modification, we want the class sending SMS to be notified, the class sending email to be notified, and the class processing logistics information to be notified.
Let's define several specific observer classes:
public class BinaryObserver extends Observer { // Subscribe to topics in construction methods public BinaryObserver(Subject subject) { this.subject = subject; // In general, you must be careful when publishing this in the construction method this.subject.attach(this); } // This method is called by the topic class when the data changes @Override public void update() { String result = Integer.toBinaryString(subject.getState()); System.out.println("The subscribed data changes, and the new data is processed into binary values:" + result); } } public class HexaObserver extends Observer { public HexaObserver(Subject subject) { this.subject = subject; this.subject.attach(this); } @Override public void update() { String result = Integer.toHexString(subject.getState()).toUpperCase(); System.out.println("The subscription data changes, and the new data processing is hexadecimal. The value is:" + result); } }
The client is also very simple to use:
public static void main(String[] args) { // Define a topic first Subject subject1 = new Subject(); // Define observer new BinaryObserver(subject1); new HexaObserver(subject1); // Simulate data changes, at which time the observers' update method will be called subject.setState(11); }
output:
The subscribed data changes, and the new data is processed into binary value: 1011 The subscription data changes, and the new data processing is hexadecimal. The value is: B
Of course, jdk also provides similar support. For details, you can refer to java.util.Observable and java.util.Observer.
In the actual production process, the observer mode is often implemented by message middleware. If you want to implement the stand-alone observer mode, the author suggests readers to use EventBus in Guava, which has both synchronous and asynchronous implementations. This paper mainly introduces the design mode, so I won't talk about it.
In addition, even the above code will have many variants. Just remember the core part, that is, there must be a place to store all observers, and then traverse the observers and call their callback functions when the event occurs.
3.3. Responsibility chain model
The chain of responsibility usually needs to establish a one-way list first, then the caller needs to call the header node, and the latter will automatically flow down. For example, process approval is a good example. As long as the end user submits an application, a responsibility chain is automatically established according to the content information of the application, and then the flow can begin.
In such a scenario, users can receive prizes by participating in an activity, but the activity requires a lot of rule verification before it can be released. For example, it is necessary to verify whether the user is a new user, whether there is a limit on the number of participants today, whether there is a limit on the number of participants in the whole audience, and so on. After the set rules are passed, the user can take away the prize.
If the product gives you this requirement, I think what most people must think at the beginning is to use a List to store all the rules, and then foreach will execute each rule. However, don't worry. What's the difference between the responsibility chain model and what we said?
First, we need to define the base class of nodes on the process:
public abstract class RuleHandler { // Successor node protected RuleHandler successor; public abstract void apply(Context context); public void setSuccessor(RuleHandler successor) { this.successor = successor; } public RuleHandler getSuccessor() { return successor; } }
Next, we need to define each specific node.
Verify whether the user is a new user:
public class NewUserRuleHandler extends RuleHandler { public void apply(Context context) { if (context.isNewUser()) { // If there is a successor node, pass it on if (this.getSuccessor() != null) { this.getSuccessor().apply(context); } } else { throw new RuntimeException("This activity is limited to new users"); } } }
Verify whether the user's region can participate:
public class LocationRuleHandler extends RuleHandler { public void apply(Context context) { boolean allowed = activityService.isSupportedLocation(context.getLocation); if (allowed) { if (this.getSuccessor() != null) { this.getSuccessor().apply(context); } } else { throw new RuntimeException("Sorry, your region can't participate in this event"); } } }
Check whether the prize has been received:
public class LimitRuleHandler extends RuleHandler { public void apply(Context context) { int remainedTimes = activityService.queryRemainedTimes(context); // Query remaining prizes if (remainedTimes > 0) { if (this.getSuccessor() != null) { this.getSuccessor().apply(userInfo); } } else { throw new RuntimeException("You're too late. The prizes have been collected"); } } }
client:
public static void main(String[] args) { RuleHandler newUserHandler = new NewUserRuleHandler(); RuleHandler locationHandler = new LocationRuleHandler(); RuleHandler limitHandler = new LimitRuleHandler(); // It is assumed that this activity only verifies the region and the number of prizes, and does not verify new and old users locationHandler.setSuccessor(limitHandler); locationHandler.apply(context); }
The code is actually very simple, that is, first define a linked list, and then pass it on after passing through any node. If this node has a successor node, then pass it on.
As for the similarities and differences between it and the previous practice of using a List to store the rules to be executed, leave it to the readers to ponder for themselves.
3.4. Template method mode
The template method pattern is very common in code with inheritance structures.
There is usually an abstract class:
public abstract class AbstractTemplate { // This is the template method public void templateMethod() { init(); apply(); // This is the point end(); // Can be used as a hook method } protected void init() { System.out.println("init The abstraction layer has been implemented, and subclasses can also choose to override"); } // Left to subclass implementation protected abstract void apply(); protected void end() { } }
The template method calls 3 methods, where apply() is an abstract method, and the subclass must implement it. In fact, several abstract methods in the template method are completely free. We can also set the three methods as abstract methods, so that the subclasses can be implemented. In other words, the template method is only responsible for defining what should be done in the first step, what should be done in the second step and what should be done in the third step. As for how to do it, it is implemented by subclasses.
Let's write an implementation class:
public class ConcreteTemplate extends AbstractTemplate { public void apply() { System.out.println("Subclass implementation abstract method apply"); } public void end() { System.out.println("We can put method3 It can be used as a hook method and can be overwritten when necessary"); } }
Client call Demo:
public static void main(String[] args) { AbstractTemplate t = new ConcreteTemplate(); // Call template method t.templateMethod(); }
The code is actually very simple. You can basically understand it when you see it. The key is to learn to use it in your own code.
3.5. Status mode
I won't talk nonsense. Let's talk about a simple example. The most basic demand of commodity inventory center is to reduce inventory and replenish inventory. Let's see how to write it in status mode.
The core is that our focus is no longer on the operation of the Context, but on the operation of the Context.
Define status interface:
public interface State { public void doAction(Context context); }
Define inventory reduction status:
public class DeductState implements State { public void doAction(Context context) { System.out.println("Goods sold, ready to reduce inventory"); context.setState(this); //... perform specific operations for inventory reduction } public String toString() { return "Deduct State"; } }
Define replenishment status:
public class RevertState implements State { public void doAction(Context context) { System.out.println("Replenish this item"); context.setState(this); //... perform the specific operation of adding inventory } public String toString() { return "Revert State"; } }
context.setState(this) was used earlier. Let's see how to define the context class:
public class Context { private State state; private String name; public Context(String name) { this.name = name; } public void setState(State state) { this.state = state; } public void getState() { return this.state; } }
Let's take a look at the client call, and you'll know it clearly:
public static void main(String[] args) { // What we need to operate is iPhone X Context context = new Context("iPhone X"); // See how to replenish inventory State revertState = new RevertState(); revertState.doAction(context); // Similarly, the inventory reduction operation is also very simple State deductState = new DeductState(); deductState.doAction(context); // If necessary, we can get the current status // context.getState().toString(); }
Readers may find that in the above example, if we don't care what state the current context is in, the context can not maintain the state attribute, so the code will be much simpler.
However, the example of commodity inventory is only an example after all. We still have many examples that need to know the status of the current context.
3.6. Behavioral model summary
The behavioral mode part introduces the strategy mode, observer mode, responsibility chain mode, template method mode and state mode. In fact, the classic behavioral modes also include memo mode, command mode, etc., but their use scenarios are relatively limited, and the length of this paper is very large, so I won't introduce them.
4. Summary
The purpose of learning design patterns is to make our code more elegant, easy to maintain and easy to expand. After finishing this article this time, I have re examined various design patterns. For myself, I have gained a lot. I think the biggest beneficiaries of articles are generally the author himself. In order to write an article, you need to consolidate your knowledge and look for all kinds of materials. Moreover, what you have written is the easiest to remember. It can be regarded as my advice to readers.
Well, that's all for today.
Reference: avadoop.com/post/design-pattern