Java Advanced Topics: Principles of Software Architecture Design

1. Introduction

​ Today is the first lesson of our topic, and it is also the first day I started advanced learning. Let's start with classic design ideas and see how the big bull market writes code, enhances the aesthetics of technology, and enhances core competitiveness. This chapter refers to the first Spring Inner Strength Method in the reference book "Spring 5 Core Principles" (you need an electronic file plus Miss Sister V: java9610 to get it for free).

2. The principle of opening and closing

The Open-Closed Principle (OCP) means that a software entity (such as classes, modules, and functions) should be developed for extensions and closed for modifications. The so-called opening and closing is also a principle for the two behaviors of expansion and modification. It emphasizes the use of abstract construction frameworks and implementation of extension details, which can improve the reusability and maintainability of the software system. The principle of opening and closing is the most basic design principle in object-oriented design. It knows how we build a stable and flexible system. For example, for version updates, we do not modify the source code as much as possible, but we can add new features. It is common in the use of interfaces and implementation classes that we often write. First, the interface is defined, and different implementation classes are written for different needs. If there are changes later, if you do not modify the original code, you can rewrite an implementation class.

3. Dependence on the principle of inversion

Dependence Inversion Principle (DIP) means that when designing code structure, high-level modules should not rely on low-level modules, and both should rely on their abstraction. Abstraction should not depend on details, and details should depend on abstraction. Through dependency inversion, the coupling between classes can be reduced, the stability of the system can be improved, the readability and maintainability of the code can be improved, and the risk of modifying the program can be reduced. This is a relatively important design principle. In our daily development, there are often scenarios where this idea is used, which can avoid many business changes and only need to change a small amount of code to complete the requirements. Let's use an example to understand this idea in depth.

Take the learning course as an example:

//Tom正在学习两个课程 public class Tom { public void studyJva() { System.out.println("正在学习Java"); } public void studyPython() { System.out.println("正在学习Python"); } }
//这里调用Tom的两个学习方法 public static void main(String[] args) { Tom tom = new Tom(); tom.studyJva();; tom.studyPython(); }

It seems that there is nothing wrong with the methods and calls of the above two classes, but with the expansion of the business, Tom wants to continue to learn the Go language. At this time, we need to modify the code from the low-level to the high-level (calling layer). The studyGo() method is being started in Tom, and then called in main. As a result, after the system was released, it was actually very unstable. Now the code is less clearly known. However, in actual projects, the amount of code and the business involved are very large. Modifying the code of multiple classes may cause unexpected risks to the code.

​ Next, we optimize the code based on the idea of ​​dependency inversion, and first create an ICourse interface.

public interface ICourse { /** * 抽象出来一个专门用来学习的方法,抽象不依赖细节,不去关心去学习什么课程 */ void study(); } 
//创建一个Java课程学习 public class JavaCourse implements ICourse { /** *由实现类去觉得具体学习什么课程(细节应该依赖抽象) */ public void study() { System.out.println("Tom在学习Java"); } } 
//创建一个Python课程学习 public class PythonCourse implements ICourse { public void study() { System.out.println("Tom在学习Python"); } } 
//修改Tom public class Tom { public void study(ICourse course){ //应证了高层模块不应该依赖底层模块,应该依赖其抽象; } }
//调用方代码 public static void main(String[] args) { Tom tom = new Tom(); JavaCourse()); PythonCourse()); }

Through the above code transformation, we can see the core idea of ​​the dependency inversion principle. The interface learned through abstract courses reduces the coupling and maintainability between classes. When a new course is added, we can directly add another implementation class and inform Tom by passing parameters without modifying the underlying code (which also reflects the principle of opening and closing).

​ Everyone must remember: the architecture built on the basis of abstraction is much more stable than that built on the basis of details. Therefore, after obtaining the requirements, it is necessary to program the interface for the interface, and design the code structure at the top level and then the details.

4. Single responsibility principle

​ Single responsibility (Simple Responsibility Pinciple, SRP) means that there should not be more than one reason for class changes. Suppose we have a class responsible for two responsibilities. Once the requirements change, modifying the code of one of the responsibilities may cause the function of the other responsibilities to malfunction. In this way, there are two reasons for this class to change. How to solve this problem? The two responsibilities are implemented in two classes for decoupling. Later requirements change and maintenance do not affect each other. Such a design can reduce the complexity of the class, improve the readability of the class, improve the maintainability of the system, and reduce the risk caused by changes. Generally speaking, a class, interface, or method is only responsible for one responsibility.

​ Next, let’s take a look at the code examples and use the courses as examples. Our courses include live and recorded courses. Live broadcast lessons cannot be fast forwarded or rewinded. Recorded broadcast courses can be watched repeatedly. The functions and responsibilities are different. I still want to create a Course class:

public class Course { public void study(String courseName){ if ("直播课".equals(courseName)){ System.out.println(courseName + "不能快进"); }else { System.out.println(courseName + "可以反复回看"); } } } 
//看下调用代码 public static void main(String[] args) { Course course = new Course();"直播课");"录播课"); }

​ From the code above, the Course class assumes two processing logic. If you want to encrypt the course now, the encryption logic of the live course is different from that of the recorded course, and the code must be modified. The logic of modifying the code is bound to affect each other, and it is easy to bring uncontrollable risks. We decouple responsibilities, look at the code, and create two classes respectively

//直播 public class LiveCourse { public void study(String courseName){ System.out.println(courseName + "不能快进看"); } }
//录播 public class ReplayCourse { public void study(String courseName){ System.out.println(courseName + "可以反复回看"); } }
//调用代码 public static void main(String[] args) { LiveCourse liveCourse = new LiveCourse();"直播课!"); ReplayCourse replayCourse = new ReplayCourse();"录播课!"); }

​ At this time, the business continues to develop and the courses need to be authorized. Students who have not paid can get the basic information of the course, and those who have paid can get the video stream, that is, the learning authority. Design a top-level interface and create an ICourse interface:

public interface ICourse { //获取课程基本信息 String getCourseName(); //获取视频流 byte[] getCourseVideo(); //学习课程 void studyCourse(); //退款 void refundCourse(); }

​ In fact, there are at least two responsibilities at the level of control courses. We can separate the presentation responsibilities and management responsibilities, and realize the same abstract dependency. We can split this interface into two interfaces: ICourseInfo and ICourseManager.

public interface ICourseInfo { //获取课程基本信息 String getCourseName(); //获取视频流 byte[] getCourseVideo(); } public interface ICourseManager { //学习课程 void studyCourse(); //退款 void refundCourse(); }

Let's look at the class diagram

The above is the single responsibility of the class/interface. Let's look at the single responsibility of the method level. Sometimes we will be lazy and write a method as follows:

/** * 修改用户信息 */ private void modityUserInfo(String userName,String address){ userName="LaoWang"; address = "BeiJin"; } private void modityUserInfo(String userName,String... fileds){ userName="LaoWang"; for (String filed : fileds) { //.... } } private void modityUserInfo(String userName,String address,boolean bool){ if (bool){ //... }else { //... } userName = "LaoWang"; address = "BeiJin"; }

Obviously, the modityUserInfo() method above assumes multiple responsibilities, both userName and address can be modified, and even more, which obviously does not conform to a single responsibility. We make the following modifications, which can be split into two methods

private void modifyUserName(String userName){ userName="LaoWang"; } private void modifyAddress(String address){ address = "BeiJin"; }

​ After modification, it is easy to develop and maintain. In the actual development, we will have the relationships of project dependency, combination, and aggregation, as well as the scale, cycle, level of technical personnel, and progress control of the project. Many categories do not conform to a single responsibility. However, in the process of writing code, we try to maintain a single responsibility for interfaces and methods as much as possible, which is of great help to the maintenance of the project later.

Section 2:

1. The principle of interface isolation

​ The interface segregation principle (Interface Segregation Principke, ISP) refers to the use of multiple specialized interfaces instead of a single general interface, and the client should not rely on interfaces it does not need. This principle knows that we should pay attention to the following points when designing interfaces:

(1) The dependence of one class on another should be based on the minimal interface.

(2) Establish a single interface, don't create a huge and bloated interface.

(3) Refine the interface as much as possible, with as few methods as possible in the interface (not as few as possible, it must be moderate).

​ The principle of interface isolation conforms to the design philosophy of high cohesion and low coupling that we often say, which can make the class have good readability, scalability and maintainability. When we are designing the interface, we need to spend more time thinking, we must consider the business model, including making some predictions about possible changes in the future. Therefore, it is very important to understand abstraction and business model.

​ Let's look at a piece of code to describe an animal behavior abstractly.

//描述动物行为的接口 public interface IAnimal { void eat(); void fly(); void swim(); } 
//鸟类 public class Bird implements IAnimal { public void eat() { } public void fly() { } public void swim() { } }
//狗 public class Dog implements IAnimal { public void eat() { } public void fly() { } public void swim() { } }

It can be seen that Brid's swim() method can only be empty, and Dog's fly() method is obviously impossible. At this time, we design different interfaces for different animal behaviors, respectively design IEatAnimal, IFlyAnimal and ISwimAnimal interfaces, look at the code:

public interface IEatAnimal { void eat(); } 
public interface IFlyAnimal { void fly(); }
public interface ISwimAnimal { void swim(); }

At this time, Dog only needs to implement the IEatAnimal and ISwimAnimal interfaces, so it's clear.

public class Dog implements IEatAnimal,ISwimAnimal { public void eat() { } public void swim() { } }

2. Dimit's principle

​ The Law of Demeter LoD means that an object should have the least knowledge of other objects, also known as the Least Knowledge Principle (LKP), to minimize the coupling between classes. The Dimit principle mainly emphasizes: only communicate with friends, not with strangers. Classes that appear in member variables, input and output parameters of methods can be called member friend classes, while classes that appear inside the method body do not belong to friend classes.

​ Now to design a permission system, the boss needs to check the number of courses currently posted online. At this time, the Boss needs to find TeamLeader for statistics, and TeamLeader tells the Boss the statistical results. Next, let's take a look at the code:

//课程类 public class Course { }
//TeamLeader类 public class TeamLeader { public void checkNumberOfCourses(List<Course> courses){ System.out.println("目前已经发布的课程数量:"+courses.size()); } }
//Boss类 public class Boss { public void commandCheckNumber(TeamLeader teamLeader){ //模拟BOSS一页一页往下翻页,TeamLeader实时统计 List<Course> courseList = new ArrayList<Course>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); } teamLeader.checkNumberOfCourses(courseList); } } 
//调用方代码 public static void main(String[] args) { Boss boss = new Boss(); TeamLeader teamLeader = new TeamLeader(); boss.commandCheckNumber(teamLeader); }

​ At this point, the function has been implemented, and the code seems to have no problem, but according to Dimit's principle, Boss only wants results and does not want to communicate directly with Course. The TeamLeader statistics need to reference the Course object. Boss and Course are not friends, as can be seen from the following class diagram:​ The code is modified below:

//TeamLeader做与course的交流 public class TeamLeader { public void checkNumberOfCourses(){ List<Course> courses = new ArrayList<Course>(); for (int i = 0; i < 20; i++) { courses.add(new Course()); } System.out.println("目前已经发布的课程数量:"+courses.size()); } }
//Boss直接与TeamLeader交流,不再直接与Course交流 public class Boss { public void commandCheckNumber(TeamLeader teamLeader){ //模拟BOSS一页一页往下翻页,TeamLeader实时统计 teamLeader.checkNumberOfCourses(); } }

Look at the class diagram again, Boss and Course are no longer connected​. Remember here, learning software design rules must not form obsessive-compulsive disorder. When encountering complex business scenarios, we need to adapt to changes.

3. The principle of Richter substitution

​ The Liskov Substitution Priciple (LSP) means that if for every object O1 of type T1, there is an object O2 of type T2, so that all programs P defined by T1 are replaced with O2 in all objects O1 When the behavior of program P does not change, then type T2 is a subtype of type T1.

​ This definition seems rather abstract, we need to re-understand it. It can be understood that if a software entity is applicable to a parent class, then its subclass must be applicable. Therefore, the reference to the parent class must be able to transparently use the object of its subclass. The subclass object can replace the parent class object, and the program logic is not According to this understanding, the extended meaning is: subclasses can extend the functions of the parent class, but cannot change the original functions of the parent class.

​ (1) Subclasses can implement the abstract methods of the parent class, but cannot override the non-abstract methods of the parent class.

​ (2) Subclasses can add their own unique methods.

​ (3) When the method of the subclass overloads the method of the parent class, the preconditions of the method (that is, the input and parameters of the method) are more relaxed than the method input parameters of the parent class.

​ (4) When the method of the subclass implements the method of the parent class (overriding, overloading or implementing abstract methods), the post-conditions of the method (that is, the output and return value of the method) are stricter than the parent class or the parent class Class is the same.

​ Using the Richter substitution principle has the following advantages:

​ (1) The proliferation of constraint inheritance is a manifestation of the principle of opening and closing.

​ (2) Strengthen the robustness of the program. When colleagues change, they can also achieve very good compatibility, improve the maintainability and scalability of the program, and reduce the risk introduced when the demand changes.

​ Now to describe a classic business scenario, use the relationship between square, rectangle and quadrilateral to illustrate the principle of Richter substitution. We all know that square is a special rectangle, so we can create a parent class Rectangle:

//矩形类public class Rectangle {    private long hight;    private long width;     public long getHight() {        return hight;    }     public void setHight(long hight) {        this.hight = hight;    }     public long getWidth() {        return width;    }     public void setWidth(long width) {        this.width = width;    }}
//正方形类public class Square extends Rectangle {    private long length;     public long getLength() {        return length;    }     public void setLength(long length) {        this.length = length;    }     @Override    public long getHight() {        return super.getHight();    }     @Override    public void setHight(long hight) {        super.setHight(hight);    }     @Override    public long getWidth() {        return super.getWidth();    }     @Override    public void setWidth(long width) {        super.setWidth(width);    }}
public class DemoTest {    //在测试类中创建resize方法,长方形的宽应该大于等于高,我们让高一直增加,直至高等于宽,变成正方形。    public static void resize(Rectangle rectangle) {        while (rectangle.getWidth() >= rectangle.getHight()){            rectangle.setHight(rectangle.getHight()+1);            System.out.println("宽度:"+rectangle.getWidth()+"高度:"+rectangle.getHight());        }        System.out.println("resize方法结束!");    } 	//测试代码如下    public static void main(String[] args) {        Rectangle rectangle = new Rectangle();        rectangle.setHight(10);        rectangle.setWidth(20);        resize(rectangle);    }

​ Looking at the console output, it is found that the height is finally greater than the width. This situation is normal in a square. Now if we replace the Rectangle class with its subclass, it is not logical and violates the Richter substitution principle. After replacing the parent class with a subclass, the results of the program did not meet expectations. Therefore, there are certain risks in our code design. The Richter substitution principle only exists between the parent class and the child class, and constraint inheritance is flooded. Let's create an abstract interface based on the common square and rectangle Quadrangle:

public interface Quadrangle {     long getWidth();     long getHeight(); }

Modify the rectangular Rectangle class:

public class Rectangle implements Quadrangle {    private long height;    private long width;     public long getHeight() {        return height;    }     public long getWidth() {        return width;    }     public void setHeight(long height) {        this.height = height;    }     public void setWidth(long width) {        this.width = width;    }}

Modify the Square class:

public class Square implements Quadrangle {    private long length;     public long getLength() {        return length;    }     public void setLength(long length) {        this.length = length;    }     public long getWidth() {        return 0;    }     public long getHeight() {        return 0;    }}

​ At this point, if we replace the parameters of the resize() method with the Quadrangle class, an error will be reported inside the method. Because Square has no setWidth() and setHeight() methods. Therefore, in order to restrain the proliferation of inheritance, the method parameter of resize() can only use Rectangle rectangle. Of course, we will continue to explain in depth in the following design pattern courses.

4. The principle of composite reuse

​ The Composite/Aggregate Reuse Principle (CARP) refers to the use of object combination (has-a)/aggregation (contanis-a) instead of inheritance to achieve the purpose of software reuse. It can make the system more flexible and reduce the degree of coupling between classes. Changes in one class have relatively little impact on other classes. Inheritance is called white box reuse, which is equivalent to exposing all implementation details to subclasses. Composition/aggregation is also called black box reuse, and implementation details cannot be obtained for objects other than classes. To do code design according to specific business scenarios, in fact, all need to follow the OOP model. Still taking database operations as an example, let's create the DBConnection class first:

public class DBConnection {     public String getConnection(){         return "MySQL 数据库连接";     } }

Create the ProductDao class:

public class ProductDao{     private DBConnection dbConnection;     public void setDbConnection(DBConnection dbConnection) {         this.dbConnection = dbConnection;     }    public void addProduct(){         String conn = dbConnection.getConnection();         System.out.println("使用"+conn+"增加产品");     } }

​ This is a very typical application scenario of the principle of composite reuse. However, in terms of current design, DBConnection is not an abstraction, and it is not convenient for system expansion. The current system supports MySQL database connection. Assuming business changes, the database operation layer must support Oracle database. Of course, we can add methods to support the Oracle database in DBConnection. But it violates the principle of opening and closing. In fact, we can change the DBConnection to abstract without modifying Dao's code, and look at the code:

public abstract class DBConnection {     public abstract String getConnection(); }

Then, extract the logic of MySQL:

public class MySQLConnection extends DBConnection {     @Override     public String getConnection() {         return "MySQL 数据库连接";     } }

Then create the logic supported by Oracle:

public class OracleConnection extends DBConnection {     @Override     public String getConnection() {         return "Oracle 数据库连接";     } }

The specific choice is handed over to the application layer, take a look at the class diagram:

5. Summary of design principles

​ Learn the principles of design and the basis of design patterns. In the actual development process, all codes are not necessarily required to follow the design principles. We have to consider manpower, time, cost, and quality, instead of deliberately pursuing perfection. We must follow the design principles in appropriate scenarios, which reflects a balanced trade-off. Help us design a more elegant code structure.


Finally, I hope you can support the one-click triple connection + comment and present my own "Dachang Zhenti + Microservice + MySQL + Distributed + SSM framework + Java + Redis + data structure and algorithm + network + Linux + Spring family bucket + JVM+ High concurrency + major learning mind map + interview collection"