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 Principles | Explanation |
---|---|
Principle of opening and closing | Open for extension, closed for modification |
Dependence inversion principle | Through abstraction, each class or module does not affect each other and realizes loose coupling |
Single responsibility principle | A class, interface, and method only do one thing |
Interface isolation principle | Try to ensure the purity of the interface, the client should not rely on unneeded interfaces |
Dimit's Law | Also known as the least-know principle, the less a class knows about the classes it depends on, the better |
Richter's Substitution Principle | Subclasses can extend the functions of the parent class but cannot change the original functions of the parent class |
Synthetic reuse principle | Try 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
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)
- High-level modules (calling layers) should not rely on low-level modules, both should rely on their abstraction
- Abstraction should not depend on details, details should depend on abstraction
- The central idea of dependency inversion is interface-oriented programming
- Through dependency inversion, the coupling between classes can be reduced, and the stability, readability and maintainability of the code can be improved
- 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
public class JavaCourse implements ICourse {
@Override
public void study () {
System.out.println( "Jack is learning Java" );
}
}
Copy code
Create a course class
public class PythonCourse implements ICourse {
@Override
public void study () {
System.out.println( "Jack is learning Python" );
}
}
Copy code
modify
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:
- Every class has an interface or abstract class as much as possible
- The surface type of the variable should be an interface or abstract class as much as possible
- No class should be derived from a concrete class
- Try not to override the methods of the base class
- 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)
- The client should not rely on interfaces it does not need
- 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
- Subclasses can implement the abstract methods of the parent class, but cannot override the non-abstract methods of the parent class
- You can add your own unique methods in subclasses
- 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
- 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
- Constraint inheritance overflows, a manifestation of the principle of opening and closing
- 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