Seven design principles of design patterns

7.design principles of design patterns

Design patterns are solutions to common problems in software design. As a qualified programmer, design patterns must also be mastered. Before learning design patterns, you must learn the basis of design patterns, that is, design principles

7.design principles

First of all, let s take a look at the seven design principles, as follows:

Design PrinciplesExplanation
Principle of opening and closingOpen for extension, closed for modification
Dependence inversion principleThrough abstraction, each class or module does not affect each other and realizes loose coupling
Single responsibility principleA class, interface, and method only do one thing
Interface isolation principleTry to ensure the purity of the interface, the client should not rely on unneeded interfaces
Dimit's LawAlso known as the least-know principle, the less a class knows about the classes it depends on, the better
Richter's Substitution PrincipleSubclasses can extend the functions of the parent class but cannot change the original functions of the parent class
Synthetic reuse principleTry to use object composition and aggregation instead of inheritance to achieve the purpose of code reuse

Open-closed Principle (OCP)

The opening and closing principle is the most basic design principle for object-oriented design, which guides us how to build a stable and flexible system.

The principle of opening and closing means that an entity such as a class, module, and function is open for extension and closed for modification . The so-called opening and closing is exactly a principle for the two behaviors of expansion and modification. Emphasizes the use of abstraction to build the framework, and implementation to extend the details to improve the reusability and maintainability of the software

For example, the company implements flexible working hours, stipulating 8 hours a day. This means that the rule for working 8 hours a day is closed, but when you come and when you leave are open, come early and leave early, and come late and leave late.

The core idea of implementing the principle of opening and closing is to oriented to abstract programming . Next, look at the following piece of code:

An online education system with many courses, such as Java, Big Data, Python, etc. Now create a course interface

public interface ICourse { Integer getId () ; String getName () ; Double getPrice () ; } Copy code

Create a Java course

public class JavaCourse implements ICourse { private Integer id; private String name; private Double price; public JavaCourse (Integer id, String name, Double price) { this .id = id; this .name = name; this .price = price; } @Override public Integer getId () { return this .id; } @Override public String getName () { return this .name; } @Override public Double getPrice () { return this .price; } } Copy code

Now we are going to do activities for Java courses and get discounts. If we go to modify

JavaCourse
middle
getPrice()
Method, there will be a certain risk, which will affect the results of other calls. How to realize the price discount function without modifying the original code? Then we need to create another class that handles the preferential logic

public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse (Integer id, String name, Double price) { super (id, name, price); } public Double getOriginPrice () { return super .getPrice(); } public Double getPrice () { return super .getPrice() * 0.6 ; } } Copy code
public static void main (String[] args) { ICourse javaCourse = new JavaCourse( 1 , "java" , 666.66 ); javaCourse = new JavaDiscountCourse( 1 , "Java" , 666.66 ); System.out.println(javaCourse.getPrice()); } Copy code

Finally, let's take a look at the class diagram of the code

Dependence Inversion Principle (DIP)

  1. High-level modules (calling layers) should not rely on low-level modules, both should rely on their abstraction
  2. Abstraction should not depend on details, details should depend on abstraction
  3. The central idea of dependency inversion is interface-oriented programming
  4. Through dependency inversion, the coupling between classes can be reduced, and the stability, readability and maintainability of the code can be improved
  5. The purpose of using interfaces or abstract classes is to specify a good specification, without involving any specific operations, and to delegate the task of showing details to their implementation classes to complete

Look at a case, or take the course as an example, first create a user

public clss Jack { public void studyJavaCourse () { System.out.println( "Jack is learning Java" ); } public void studyPythonCourse () { System.out.println( "Jack is learning Python" ); } } Copy code
public static void main (String[] args) { Jack jack = new Jack(); jack.studyJavaCourse(); jack.studyPythonCourse(); } Copy code

If you want to learn a new course in the future, you must modify the code from the bottom to the call level. This is very troublesome, and it may bring other risks while modifying the code, so you need to optimize the code and create a course Abstract interface

public interface ICourse { void study () ; } Copy code

Create a course class

JavaCourse
class

public class JavaCourse implements ICourse { @Override public void study () { System.out.println( "Jack is learning Java" ); } } Copy code

Create a course class

PythonCourse
class

public class PythonCourse implements ICourse { @Override public void study () { System.out.println( "Jack is learning Python" ); } } Copy code

modify

Jack
class

public clss Jack { public void study (ICourse course) { course.study(); } } Copy code

Call again

public static void main (String[] args) { Jack jack = new Jack(); jack.study( new JavaCourse); jack.study( new PythonCourse); } Copy code

The optimized code, for the follow-up need to learn a new course, only need to create a class, and then tell Jack by passing parameters, without the need to modify the underlying code. In fact, this is dependency injection . There are constructor and setter methods for injection.

Constructor method

public clss Jack { private ICourse course; public Jack (ICourse course) { this .course = course; } public void study () { course.study(); } } Copy code

According to the constructor method, it is not very convenient to create an instance every time when you call it, so you can only choose the Setter method for injection

public class Jack { private ICourse course; public void setCourse (ICourse course) { this .course = course; } public void study () { course.study(); } } Copy code
public static void main (String[] args) { Jack jack = new Jack(); jack.setCourse( new JavaCourse()); jack.study(); jack.setCourse( new PythonCourse()); jack.study(); } Copy code

The essence of the dependency inversion principle is to make the realization of each class or module independent of each other through abstraction (interface or abstract class), without affecting each other, and to achieve loose coupling between modules. The following rules should be followed when using this principle in the development of the project in the future:

  1. Every class has an interface or abstract class as much as possible
  2. The surface type of the variable should be an interface or abstract class as much as possible
  3. No class should be derived from a concrete class
  4. Try not to override the methods of the base class
  5. If the base class is an abstract class, and this method has been implemented, subclasses should try not to overwrite it. The inter-class dependency is abstraction, which overwrites the abstract method, which will have a certain impact on the stability of the dependency

Finally, we look at the class diagram:

Single Responsibility Principle (Simple Responsibility Principle, SRP)

The principle of single responsibility means that the object should not take on too many functions. Just as one mind cannot use multiple functions, only concentration can ensure the high cohesion of the object; only the only one can ensure the fine-grained object. For example, there are two types of A and B. When A needs to send a change and need to be modified, it cannot cause a problem with the B type. How to solve this problem? We have to give the two classes a responsibility respectively, decoupling, and later requirements change and maintenance will not affect each other

Take the course as an example. Courses can be divided into live courses and recorded courses. Live broadcast courses cannot be fast-forwarded and rewinded. Recorded courses can be watched back and forth at will. The functional responsibilities are different. 1. create a course class.

public class Course { public void study (String courseName) { if ( "Live Class" .equals(courseName)) { System.out.println( "Cannot fast forward" ); } else { System.out.println( "Play back and forth at will" ); } } } Copy code
public static void main (String[] args) { Course course = new Course(); course.study( "Live Lesson" ); course.study( "recorded and broadcast class" ); } Copy code

From the above code point of view, the Course class undertakes two processing logics. If you want to perform an operation on these two courses later, and the operation logic of the two courses is different, then you must modify the code, which increases the complexity of the code and reduces the maintainability. Therefore, we need to decouple its responsibilities and create live and recorded classes respectively

public class LiveCourse { public void study (String courseName) { System.out.println(courseName + "Cannot fast forward" ); } } Copy code
public class ReplayCourse { public void study (String courseName) { System.out.println(courseName + "Can play back and forth at will" ); } } Copy code

Code call

public static void main (String[] args) { LiveCourse liveCourse = new LiveCourse(); liveCourse.study( "Live Lesson" ); ReplayCourse replayCourse = new ReplayCourse(); replayCourse.study( "Recording and Broadcasting Course" ); } Copy code

After such modification, later maintenance is easier. However, in our actual development, due to project dependencies, combination and other relationships, many classes do not actually conform to a single responsibility. We also want to keep the interfaces and methods as single responsibility as possible, which is of great help to the maintenance of the project later.

Finally, let's take a look at the class diagram:

Interface Segregation Principle (ISP)

  1. The client should not rely on interfaces it does not need
  2. The dependencies between classes should be established on the smallest interface

In layman's terms, don't put too many methods in an interface, it will be very bloated. The interface should be as detailed as possible, one interface corresponds to one functional module, and the methods in the interface should be as few as possible, making the interface more flexible and lighter

Let's look at an example

Create an abstraction of animal behavior

public interface IAnimal { void eat () ; void fly () ; void swim () ; } Copy code

Bird class implementation

public class Bird implements IAnimal { @Override public void eat () {} @Override public void fly () {} @Override public void swim () {} } Copy code

Dog class implementation

public class Dog implements IAnimal { @Override public void eat () {} @Override public void fly () {} @Override public void swim () {} } Copy code

As can be seen from the above code, Bird's swim() method may be empty, and Dog's fly() method is obviously impossible. Therefore, we have to design different interfaces for different animal behaviors, see the following code

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

Bird class implements IEatAnimal, IFlyAnimal

public class Bird implements IEatAnimal , IFlyAnimal { @Override public void eat () {} @Override public void fly () {} } Copy code

Dog class implements IEatAnimal, ISwimAnimal

public class Dog implements IEatAnimal , ISwimAnimal { @Override public void eat () {} @Override public void swim () {} } Copy code

Through the following class diagram, it can be very clear

Law of Demeter LOD

Dimit s rule refers to that an object should have the least knowledge of other objects. It is also called the Least Knowledge Principle (LKP) . It minimizes the coupling between classes and emphasizes that only talking with your direct friends. Talk to strangers.

The friend in Dimit s rule refers to: the current object itself, member objects of the current object, objects created by the current object, method parameters, etc. These objects have association, aggregation, or combination relationships, and you can directly access these objects

Now let's take an example. To design a permission system, TeamLeader needs to check the number of courses currently posted online. At this time, TeamLeader needs to find the employee Employee to enter the statistics, and then Employee tells TeamLeader the statistics results, and then look at the code

Course class

public class Course { } Copy code

Employee class

public class Employee { public void checkNumberOfCourse (List<Course> courseList) { System.out.println( "Released Course" + courseList.size()); } } Copy code

TeamLeader class

public class TeamLeader { public void commandCheckNumber (Employee employee) { List<Course> courseList = new ArrayList<>(); for ( int i = 0 ; i < 20 ; i++) { courseList.add( new Course()); } employee.checkNumberOfCourse(courseList); } } Copy code

Test code

public static void main (String[] args) { TeamLeader leader = new TeamLeader(); Employee employee = new Employee(); leader.commandCheckNumber(employee); } Copy code

The code is written here, in fact, the function has been completed, but according to Dimit's principle, TeamLeader only needs to know the result, and does not need to have direct communication with Course, and Employee statistics need to reference the Course object. TeamLeader and Course are not friends, as you can see from the class diagram below

Let's modify the code below

Employee class

public class Employee { public void checkNumberOfCourse () { List<Course> courseList = new ArrayList<>(); for ( int i = 0 ; i < 20 ; i++) { courseList.add( new Course()); } System.out.println( "Released Course" + courseList.size()); } } Copy code

TeamLader class

public class TeamLeader { public void commandCheckNumber (Employee employee) { employee.checkNumberOfCourse(); } } Copy code

Look at the following class diagram again, Course and TeamLeader are no longer related

Liskov SubStitution Principle

The Richter substitution principle means that for every object o1 of type T1, there is an object o2 of type T2, so that when all objects o1 are replaced with o2 in all programs P defined by T1, the behavior of program p does not change. Then type T2 is a subtype of type T1.

The definition looks more abstract. We can simply understand that if a software entity applies to a parent class, it must be applicable to its subclasses. The subclass objects can replace the parent class objects, while the program logic remains unchanged. Based on this understanding, let's summarize:

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. You can add your own unique methods in subclasses
  3. When the method of the subclass overloads the method of the parent class, the pre-addition of the method (that is, the input/parameter of the method) is more relaxed than the input parameter of the parent method
  4. When a method of a subclass overloads a method of a parent class (override/overload or implement an abstract method), the post-condition of the method (that is, the output/return value of the method) is stricter or equal to that of the parent class

Using the Richter substitution principle has the following advantages

  1. Constraint inheritance overflows, a manifestation of the principle of opening and closing
  2. Strengthen the robustness of the program, and at the same time, it can also achieve very good compatibility when changing, and improve the maintainability and scalability of the program.

Let s use a case to illustrate the principle of Richter substitution. We all know that a square is a special rectangle. Let s create a rectangular parent class below.

Rectangle class

public class Rectangle { private Long height; private Long width; public Long getHeight () { return height; } public void setHeight (Long height) { this .height = height; } public Long getWidth () { return width; } public void setWidth (Long width) { this .width = width; } } Copy code

Square class

public class Square extends Rectangle { private Long length; public Long getLength () { return length; } public void setLength (Long length) { this .length = length; } @Override public void setHeight (Long height) { setLength(height); } @Override public Long getHeight () { return getLength(); } @Override public void setWidth (Long width) { setLength(width); } @Override public Long getWidth () { return getLength(); } } Copy code

Create a resize() method in the Main class, if the width is greater than or equal to the height, let the height increase continuously

public class Main { public static void main (String[] args) { Rectangle rectangle = new Rectangle(); rectangle.setHeight( 5L ); rectangle.setWidth( 10L ); resize(rectangle); } public static void resize (Rectangle rectangle) { while (rectangle.getWidth() >= rectangle.getHeight()) { rectangle.setHeight(rectangle.getHeight() + 1 ); System.out.println( "Width:" + rectangle.getWidth() + ", Height:" + rectangle.getHeight()); } System.out.println( "Resize End, Width:" + rectangle.getWidth() + ", Height:" + rectangle.getHeight()); } } Copy code

The print result is as follows

Width: 10 , Height: 6 Width: 10 , Height: 7 Width: 10 , Height: 8 Width: 10 , Height: 9 Width: 10 , Height: 10 Width: 10 , Height: 11 Resize End, Width: 10 , Height : 11 copy the code

Now replace Rectangle with Square

Square square = new Square(); square.setLength( 10L ); resize(square); Copy code

At this time, we run the code in an infinite loop, which violates the Liskov substitution principle. After replacing the parent class with a subclass, the results of the program running did not meet expectations. Therefore, there is a certain risk in our code design. The Richter substitution principle only exists between the parent class and the child class, and constraint inheritance is flooded.

Composite/Aggregate Reuse Principe (CARP)

The principle of composite reuse is to use object composition/aggregation as much as possible instead of inheritance to achieve the purpose of software reuse, which can make the system more flexible, reduce the coupling between classes, and the impact of changes in one class on other classes Relatively small

Inheritance is equivalent to exposing all implementation details to subclasses, and composition/aggregation means that the implementation details cannot be obtained for objects other than the class

Explain through an example of a database

DBConnection class

public class DBConnection { public String getConnection () { return "MSQL database connection" ; } } Copy code

ProductDao class

public class ProductDao { private DBConnection connection; public void setConnection (DBConnection connection) { this .connection = connection; } public void addProduct () { String conn = this .connection.getConnection(); System.out.println( "Use" + conn + "Add product" ); } } Copy code

From the above code, DBConnection is not an abstraction, and it is not easy to extend. Only MySQL database is currently supported. If the business changes in the future, Oracle database must be supported. We can add a method to connect to the Oracle database in DBConnection, but this violates the principle of opening and closing . In fact, we only need to modify the DBConnection to abstract.

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

Extract the logic of MySQL

public class MySQLConnection extends DBConnection { @Override public String getConnection () { return "MySQL database connection" ; } } Copy code

Create Oracle support

public class OracleConnection extends DBConnection { @Override public String getConnection () { return "Oracle database connection" ; } } Copy code

Dao does not need to change at all, how to choose to directly hand it over to the application layer for processing

public static void main (String[] args) { DBConnection connection = new MySQLConnection(); ProductDao dao = new ProductDao(); dao.setConnection(connection); dao.addProduct(); } Copy code

Finally look at the class diagram

summary

Learn the principles of design and the basis of design patterns. In the actual development process, it is not necessarily required that all codes 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