We know that design patterns are very useful, and learning them will not only help you write more concise and elegant code, but also make the structure of the code clearer and more extensible
Of course, design patterns are not omnipotent and immutable. Design pattern is just a kind of experience summed up by predecessors, a solution to a specific problem, not a dead thing
It is not necessary to follow the design pattern book, as long as we write code in accordance with certain principles, can also be regarded as their own pattern. But our predecessors
The sum up must be very worthy of our learning. This series of 23 design patterns will use the simplest examples, will use the most clear language to explain the ideas inside,
I won’t go into too much detail, but all of the code in this series is written by myself, and examples come from books or reading
All the code is on Github. The address will be listed later for download and reference.
Before we get into design patterns, let’s talk about some basic principles of code writing
A single responsibility principle
The first step in optimizing code is when a class or method’s job is to do only one thing, for example, if a writer needs to write a book, he needs to write the book first and then hand it over to a publisher. The following code
5 Public void writeBook(String bookName) {6 system.out.println (" write a book "+ BookName + "bookName "); Public void publishBook(String bookName) {11 system.out.println (" publishBook "+ bookName +" bookName "); 12} 13 14}Copy the code
The above example is a little simple, but it can illustrate the problem. The Author class should only be responsible for writing the book, and the publishing house should be responsible for publishing the book. In this example, the Authon class is not only responsible for writing the book,
He is also in charge of publishing books, which creates a situation of multiple functions. Instead, the author writes the book, and the publishing house gets the job done. The code is as follows:
1 // 2 public class Author {3 private String bookName; 6 public String writeBook() {7 system.out.println (" write a book "+ bookName +" book "); 8 return bookName; 9} 10 11}Copy the code
1 // Publishing house
3 Public class Publisher {4 4 public void publishBook(String bookName){5 system.out.println (" publishBook "+ bookName + "Book"); 6} 7 8}Copy the code
The test class:
1 public class TestAuthor {2 public static void main(String[] args){3 // Author 4 Author = new Author(); 7. Publisher = new Publisher(); 10 String bookName = author.writebook (); 13 13 Publisher.publishbook (bookName); 14}} 15Copy the code
Through the above functional disassembly, the responsibility of the class is clearly divided, the function is single.
Two open and close principle
Make your application more stable and flexible: open for extension, closed for modification. What do you mean?
If I add a feature, I can just add the new feature without modifying the old code. If you don’t modify old code, you are immune to changes. If you add new code, you are open to extensions.
Directly add new code, do not need to modify the old code can be completed software function expansion, this will reduce the possibility of error, improve the system stability.
The factory model of the design pattern uses the on/off principle: Let’s take a look at the general factory model, for example, making mobile phones:
1 public class PhoneFactory { 2 3 public Phone produce(String type){ 4 Phone phone = null; 5 6 if("xiaomi".equals(type)){ 7 phone = new XiaoMiPhone(); 8 }else if("sanuag".equals(type)){ 9 phone = new SanuagPhone(); 10 }else if("nokia".equals(type)){ 11 phone = new NokiaPhone(); 12 } 13 14 return phone; 16 15}}Copy the code
The code above is the factory that produces mobile phones. If the factory is upgraded now and needs to produce Huawei mobile phones, what shall we do? It is clear that we can add an if condition to the original code:
1 public class PhoneFactory { 2 3 public Phone produce(String type){ 4 Phone phone = null; 5 6 if("xiaomi".equals(type)){ 7 phone = new XiaoMiPhone(); 8 }else if("sanuag".equals(type)){ 9 phone = new SanuagPhone(); 10 }else if("nokia".equals(type)){ 11 phone = new NokiaPhone(); 12 }else if("huawei".equals(type)){ 13 phone = new HuaweiPhone(); 14 } 15 16 return phone; 17} 18 19}Copy the code
By modifying the original code, we met the need to add another production line. The open closed principle is closed to modification and open to extension. We don’t advocate that, close on modifications, in this case, just don’t
To add functionality by adding an if judgment to the original code, we need to make it open to extensions
We made the factory independent
1 // Factory 2 public interface Factory {3 Phone produce(); 4} We are adding other factories that specialize in a particular type of mobile phone, 2 public class NokiaFactory implements Factory{3 @override 4 public Phone produce() {5 return new NokiaPhone(); 3 public class implements Factory{3 @override 4 public Phone produce() {5 return new SanuagPhone(); Public class XiaomiFactory implements Factory{@override public Phone produce() {return new XiaoMiPhone(); }}Copy the code
So let’s see how it works, and the test class is, okay
Private static void testYMethod (){3 Factory Factory = new NokiaFactory(); 4 Phone phone = factory.produce(); 5 phone.call(); 6}Copy the code
Directly new a factory to produce corresponding mobile phones, so that if we add another factory to produce Huawei mobile phones, we can add another factory to produce Huawei mobile phones, and then directly produce mobile phones. In this way,
We can simply add a new factory (open for extensions) without modifying the original code (closed for modifications), and we can do just that.
Three Richter substitution principle
Build systems that scale better: It’s called the Richter substitution principle because it was proposed by a lady with her last name, which basically means that all references to base classes can be subclassed.
We know that the three main characteristics of object orientation are inheritance, encapsulation and polymorphism. The Principle of Richter substitution is based on inheritance and polymorphism.
A simple example is as follows:
Window class, which displays a View. In the show(View child) method, child can be replaced by a subclass of View, which is the Richter substitution principle, which relies on object-oriented inheritance and polymorphism
Public class Window {3 public void show(View child){4 child.draw(); 6 5}}Copy the code
/ / all kinds of View
1 //View 2 public abstract class View { 3 public abstract void draw(); 5 5 public void measure(int width,int height){6 // Measure the view size 7} 8} 9Copy the code
13 Class extends View{13 13 @override 13 public void draw() {15} 16} 17 18 class TextView extends View{13 13 @override 13 public void draw() {15} 16} 17 View{20 20 @override 21 public void draw() {22}Copy the code
In the above example, Window depends on View, and View defines a View object. Measure is a method shared by subclasses. Subclasses override the Draw () method of View to achieve specific features of their own.
In this case, the function is to draw its own content, which can be set to the show() method in any subclass that inherits from the View class, known as the Richter substitution.
Four dependence inversion principle
Enabling the project to change: There are three characteristics
A high-level module should not depend on a low-level module; both should depend on its abstraction; Abstraction should not depend on details; Details should depend on abstractions. Dependency inversion principle is the programming to an interface, the following example cited blog.csdn.net/zhengzhb/ar…
I’m going to give you this example here because it illustrates the point:
The dependency inversion principle is based on the fact that abstract things are much more stable than details. An architecture based on abstraction is much more stable than one based on detail. In Java, abstraction refers to interfaces or abstract classes, and details are concrete implementation classes. The purpose of using interfaces or abstract classes is to establish specifications and contracts without involving any concrete operations, leaving the task of presenting details to their implementation classes.
The core idea of the dependency inversion principle is interface oriented programming, and we'll use an example to show how interface oriented programming is better than implementation oriented programming. The scene goes like this: A mother is telling a story to her child, just give her a book, and she can read the story to her child. The code is as follows:Copy the code
1 class Book{2 public String getContent(){3 return "Once upon a time there was an Arab story..." ; 4} 5} 6 7 class Mother{8 public void narrate(Book Book){9 system.out.println (" Mother start to tell a story "); 10 System.out.println(book.getContent()); 11 } 12 } 13 14 public class Client{ 15 public static void main(String[] args){ 16 Mother mother = new Mother(); 17 mother.narrate(new Book()); 18}} 19Copy the code
Running results:
Once upon a time there was an Arab story…
That works fine. Suppose one day, instead of a book, the demand is this: Give the mother a newspaper, and ask her to tell a story in the newspaper. The code for the newspaper is as follows:Copy the code
1 class Newspaper{2 public String getContent(){3 return "Jeremy Lin leads Knicks to beat Lakers..." ; 4} 5}Copy the code
But the Mother couldn’t do it because she couldn’t read the story in the newspaper. It was ridiculous. What if the demand is changed to magazines? What about web pages? You have to constantly modify Mother, which is obviously not good design. The reason is that the coupling between Mother and Book is so high that the coupling between them must be reduced.
We introduce an abstract interface, IReader. Readings, as long as they have words, belong to readings:
interface IReader{
public String getContent();
}
Copy the code
The Mother class relies on the IReader interface, while Book and Newspaper belong to the category of reading materials. They implement the IReader interface respectively, which conforms to the principle of dependency inversion. The code is modified as follows:
1 class Newspaper implements IReader {2 public String getContent(){3 return "Jeremy Lin 17+9 help Knicks win hawks......" ; 4} 5} 6 class Book implements IReader{7 public String getContent(){8 return "Once upon a time there was an Arab story..." ; 9} 10} 11 12 class Mother{13 public void narrate(IReader reader){14 system.out.println (" Mother began to tell a story "); 15 System.out.println(reader.getContent()); 16 } 17 } 18 19 public class Client{ 20 public static void main(String[] args){ 21 Mother mother = new Mother(); 22 mother.narrate(new Book()); 23 mother.narrate(new Newspaper()); 25 24}}Copy the code
Running results:
Once upon a time there was an Arab story… Mom starts telling stories about Jeremy Lin’s 17-9 knicks win over Hawks…
After this change, no matter how the Client class is extended later, you do not need to modify the Mother class. This is just a simple example; in practice, the Mother class representing the high-level module is responsible for doing the main business logic, and the risk of introducing errors is high if it needs to be modified. Therefore, following the dependency inversion principle can reduce the coupling between classes, improve the stability of the system, and reduce the risk caused by modifying programs. The dependency inversion principle has brought great convenience to the parallel development of multiple people. For example, when the original Mother class and Book class are directly coupled, the Mother class must wait until the Book class is coded, because the Mother class depends on the Book class. The modified program can be started at the same time without affecting each other, because the Mother and Book classes have nothing to do with each other. The more people involved in collaborative development and the larger the project, the more important it becomes to adopt the dependency lead principle. The most successful application of the inversion principle is the popular TDD development model. There are three ways to pass dependencies. The method used in the above example is interface pass, and there are two other ways to pass dependencies: constructor pass and setter pass.Copy the code
In practical programming, we generally need to do the following three points:
Low-level modules should always have abstract classes or interfaces, or both. Declare variables as abstract classes or interfaces as possible. Follow the Richter substitution principle when using inheritance. The core of dependency inversion principle is that we need to program toward interface. If we understand interface programming, we can understand dependency inversion.
Five Interface isolation principles
Make the system more flexible: making it so that a client does not have to rely on interfaces it does not need is to make the interface that the client relies on as small as possible. Let’s take an example
One of the annoying issues with JDK6 and previous versions was that after using OutputStream or other closable objects, we had to make sure they were shut down.
For example: 1 // Cache the image in memory
2 public void put(String URL,Bitmap BMP){3 FileOutputStream FileOutputStream = null; 4 try{ 5 fileOutputStream = new FileOutputStream(fileName); 6 bmp.compress(CompressFormat.PNG,100,fileOutputStream); 7 }catch (FileNotFoundException e){ 8 e.printStackTrace(); 9 }finally { 10 if(fileOutputStream ! = null){ 11 try{ 12 fileOutputStream.close(); 13 }catch (IOException e){ 14 e.printStackTrace(); 15} 16} 17} 18}Copy the code
As you can see, this code is very poorly readable, and various try.. Catch is simple code, but it can seriously affect the readability of the code, and multi-level braces can easily write code into the error level. People should hate this kind of code, but I do hate this kind of code.
We know that There is a Closeable interface in Java that identifies a Closeable object with only a close method. The FileOutputStream we’ll be talking about implements this class. So we can just create a class that closes this object. The following tools are available:
1 public class CloseUtils {3 private CloseUtils(){} 4 5 Public static void closeQuitely(Closeable closeable){ 7 if(closeable ! = null){ 8 try { 9 closeable.close(); 10 }catch (IOException e){ 11 e.printStackTrace(); 12} 13} 14} 15}Copy the code
Let’s apply this code to the put code above:
2 public void put(String URL,Bitmap BMP){3 FileOutputStream FileOutputStream = null; 4 try{ 5 fileOutputStream = new FileOutputStream(fileName); 6 bmp.compress(CompressFormat.PNG,100,fileOutputStream); 7 }catch (FileNotFoundException e){ 8 e.printStackTrace(); 9 }finally { 10 CloseUtils.closeQuitely(fileOutputStream); 12 11}}Copy the code
The code is much cleaner, and the closeQuitely methods can be applied to objects that can be closed by each class, making the code reusable. And based on the principle of minimal dependency, it only needs the object to be closed and doesn’t care about anything else, namely the interface isolation principle here.
Six Demeter principle
Make systems more scalable: one object should know the least about the other. In layman’s terms, a class should know the least about the classes it needs to couple or call
How a class is implemented internally does not matter to the caller or the dependent.
The Demeter principle is also interpreted in English as “Only talk to your immedate friends”, which translates as “Only communicate with your immediate friends”. Let me just write an example
Beipiao’s friends have rented a house. Most of the houses are rented by agents. Our requirements are that I only ask for the area and price of the room, nothing else, and the agents will provide me with the house that meets the requirements. Take a look at this example
1 // Room 2 public class Room {3 public float area; // area 4 public float price; Public Room(float area, float price) {7 this.area = area; 8 this.price = price; 9 } 10 11 @Override 12 public String toString() { 13 return "Room area=" + area + ",price=" + price; Public class Mediator {3 List<Room> roomList = new ArrayList<>(); 4 5 public Mediator(){ 6 for (int i = 0; i < 10 ; i++){ 7 roomList.add(new Room(12 + i, (12 + i) * 100 )); 8 } 9 } 10 11 public List<Room> getRoomList(){ 12 return roomList; 14 13}}Copy the code
1 // Customer 2 public class Customer {3 public float roomArea; 4 public float roomPrice; 5 6 public void rentRoom(Mediator mediator){ 7 System.out.println(mediator.rentOut(roomArea,roomPrice)); 8} 9 10 11 public void main(String[] args){12 // Customer Customer = new Customer(); Mediator = new Mediator(); 17 Customer. rentRoom(mediator); 18}} 19Copy the code
It can be seen from the above code that the Customer not only relied on Mediator, but also needed to deal with Room. The Customer only required to find a suitable house. If all these conditions were put into the Customer, then
The function of the mediation will be weakened, and the coupling between Customer and Room will be high, because the Customer must know many details of the Room, and the Room changes accordingly. At this point, we need to know who our real friends are, in our case, the intermediaries, and we just need to deal with the intermediaries. So we can do the following refactoring
Public class Mediator {3 List<Room> roomList = new ArrayList<>(); 4 5 public Mediator(){ 6 for (int i = 0; i < 10 ; i++){ 7 roomList.add(new Room(12 + i, (12 + i) * 100 )); 8 } 9 } 10 11 public Room rentOut(float area,float price){ 12 for(Room room : roomList){ 13 if(isSuitable(room,area,price)){ 14 return room; 15 } 16 } 17 18 return null; 19 } 20 21 private boolean isSuitable(Room room,float area,float price){ 22 return room.area == area && room.price == price; 23}} 24Copy the code
1 // Customer 2 public class Customer {3 public float roomArea; 4 public float roomPrice; 5 6 public void rentRoom(Mediator mediator){ 7 System.out.println(mediator.rentOut(roomArea,roomPrice)); 9 8}}Copy the code
Through the above reconstruction, the Customer only dealt with Mediator, which should have been the responsibility of Mediator. It was enough to select a suitable house according to the conditions set by the Customer and deliver the result to the Customer.
This unlocks the more complex coupling of the code. This makes code less coupled and more stable.