When we write code, we rarely write it to conform to software design principles. In fact, sometimes you use one or more of these design principles without realizing it. It’s also possible that some people don’t know what design principles are.
No matter, in order to understand the six principles that are both abstract and fantastical, I have summarized the main ideas embodied in each design principle in one sentence.
Richter’s substitution principle means that inheritance should not destroy the original function of the parent class; The dependency inversion principle refers to programming for interfaces; The open-closed principle refers to open for extension and closed for modification; The single responsibility principle means that the responsibility of the implementation class should be single; The principle of interface isolation means that the interface should be designed as simple and specific as possible. Demeter’s rule is to reduce coupling between classes.
Here are six categories of design principles, and after reading them, you’ll have a better understanding.
I. Richter’s substitution principle
Richter’s substitution principle, at first glance, is a very confusing name. Actually, it was proposed by a lady named Li, so it was named after her. The Richter’s substitution principle, generally speaking, means that when a subclass inherits from a parent class, it can extend the functionality of the parent class, but not modify the original functionality of the parent class. What does that mean? Let me give you an example.
Public class Calculate {public int CAL (int a,int b){return a + b; }} // Subclass public class Calculate2 extends Calculate {public int CAL (int a,int b){return a-b; Public class TestCal {public static void main(String[] args) {Calculate2 cal2 = new Calculate2(); int res = cal2.cal(1, 1); System.out.println("1+1="+res); // 1+1 = 0}}Copy the code
When a subclass inherits from the parent class, it wants to implement a new function, but instead of extending the new method, it overrides the CAL method of the parent class, thus resulting in 1+1=0. This violates the Richter’s substitution principle.
Subclass Calculate2 to add a new method cal2 for subtraction
public class Calculate2 extends Calculate { public int cal2(int a, int b){ return a - b; } } public class TestCal { public static void main(String[] args) { Calculate2 cal2 = new Calculate2(); int res = cal2.cal2(1, 1); System.out.println("1-1="+res); / / 1-1 = 0}}Copy the code
The Richter’s substitution principle states that subclasses cannot override methods of their parent class. This conflicts with “polymorphism”, one of the three characteristics of object orientation. An important premise of polymorphism implementation is that a subclass inherits and overwrites its parent class’s methods.
In fact, I also had such doubts when I started to learn The Richter’s substitution principle. After a lot of research, I realized that subclasses should not override methods already implemented by their parent (non-abstract methods), but should implement abstract methods of their parent. That is, try to base inheritance on abstract classes and interfaces rather than on instantiable parent classes. Explanation about this, we can see this article, I feel very good summary: www.jianshu.com/p/e6a7bbde8…
Second, the principle of single responsibility
To put it simply, it is necessary to control the granularity of classes and reduce their complexity, so that each class has only one responsibility.
For example, when developing a new product feature. The project manager is required to take requirements, assess workload, and then distribute tasks to programmers. Programmers write code according to requirements and test themselves. Only by performing their duties can we ensure the steady progress of the project. The class diagram is shown below
In addition, the single responsibility principle also applies to methods, where a method only does one thing.
3. Reliance inversion principle
The dependency inversion principle is defined as: high-level modules should not depend on low-level modules, but both should depend on their abstractions. Abstractions should not depend on details, details should depend on abstractions. In fact, we’re talking about abstract-oriented, interface-oriented programming.
For example, if a student goes to study history, all he needs to do is give him the history book
Public class History {public String getKnowledge(){return "History knowledge "; }} public class Student {public void study(History History){system.out.println (" learn "+ history.getKnowledge()); } } public class Test { public static void main(String[] args) { Student stu = new Student(); stu.study(new History()); // Learn about history}}Copy the code
However, if he needs to learn Geography, we need to change History to Geography and change the parameter type of the study method to Geography
Public class Geography {public String getKnowledge(){return "Geography "; }} public class Student {public void study(Geography){system.out.println (" learn "+ Geography.getKnowledge()); }} // Learn geographyCopy the code
Although, this implementation is also possible, but the universality is too poor, the degree of coupling between the classes is too high. Imagine, if the student has to learn math knowledge, Chinese, English, is it necessary to modify the study method every time. Such a design does not conform to the principle of dependency inversion. Knowledge of each discipline should be abstracted and an interface, IKnowledge, should be defined for each discipline to implement this interface, while the parameters of study method should be passed a fixed type of IKnowledge.
public interface IKnowledge { String getKnowledge(); } public class implements IKnowledge{public String getKnowledge(){return "implements "; }} public class implements IKnowledge{public String getKnowledge(){return "Geography "; }} public class Student {public void study(IKnowledge IKnowledge){system.out.println (" learn "+ iKnowledge.getKnowledge()); } } public class Test { public static void main(String[] args) { Student stu = new Student(); stu.study(new History()); Stu. study(new Geography()); // Learn geography}}Copy the code
In this case, if you need to learn English again, you just need to define an English class to implement the IKnowledge interface. This is interface-oriented programming that relies on the inversion principle.
The class diagram relationship between them is shown below
4. Interface isolation principle
The interface isolation principle is defined: a client should not rely on interfaces it does not need; The dependency of one class on another should be based on the smallest interface.
When you design an interface, you don’t define a bunch of abstract methods that need to be implemented into the same interface, you should break them up into different interfaces based on different functions. We know that when implementing a class to implement an interface, we need to implement all the abstract methods. It doesn’t make sense if you have some method in your interface that you don’t need and need to implement, but the method body is empty.
For example, I define an interface for Animal and use lions to implement the interface
public interface Animal { void eat(); void fly(); void run(); } public implements Animal {@override public void eat() {system.out.println (" implements Animal "); } @override public void run() {} @override public void run() {system.out.println (); }}Copy the code
Obviously, the lion can’t fly. The fly method is empty. This design does not comply with the principle of interface isolation. Therefore, we split the interface into Animal, IFly, IRun three interfaces, let the lion selective implementation.
public interface Animal { void eat(); } public interface IFly { void fly(); } public interface IRun { void run(); } public implements Animal,IRun {@override public void eat() {system.out.println (" implements Animal "); } @override public void run() {system.out.println (" run "); }} // Lion only needs to implement the eating method and the running method, do not need to implement the IFly interface.Copy the code
You can see that the interface isolation principle and the single responsibility principle are very similar, but they are different. The single responsibility principle is mainly to constrain the class, is specific to the implementation, emphasizes the single responsibility of the class. The interface isolation principle mainly restricts interfaces, focusing on high-level abstraction and isolation of interface dependencies.
In addition, it should be noted that too fine interface design is not good, will increase the complexity of the system. Imagine a scenario where you need to implement a dozen interfaces in order to implement something. Therefore, you need to split interfaces properly.
5. Demeter’s Rule
Demeter’s law defines that an object should know the least about other objects. It means to minimize the degree of coupling between classes and improve the independence of classes, so that when one class changes, the impact on other classes will be minimized.
In layman’s terms, the less a class knows about the classes it depends on, the better. For dependent classes, no matter how complex the internal implementation is, it simply exposes a common method that other classes can call.
Let’s take a simple example. When the boss needs to hand out an assignment, he doesn’t just call every employee together and assign them specific tasks. Instead, they call in department managers and give them assignments, and then the department managers give assignments to the people below them. The boss only needs to monitor the department manager and does not need to be concerned with the specific tasks that the department manager assigns to each employee.
You can write it in code like this
Public class Employee {public void doTask(){system.out.println (" Employee performs the task "); }} public class DeptManager {public void task(){system.out.println (" DeptManager "); Employee employee = new Employee(); employee.doTask(); } } public class Boss { private DeptManager deptMgr; public void setDeptMgr(DeptManager mgr){ this.deptMgr = mgr; } public void task(){system.out.println (" boss issue task "); deptMgr.task(); } } public class TestD { public static void main(String[] args) { Boss boss = new Boss(); boss.setDeptMgr(new DeptManager()); boss.task(); }} // The boss releases the task // the department leader releases the task // the employee executes the taskCopy the code
In this way, the boss does not have any direct contact with the specific individual employee, reducing the coupling.
It can be seen that in fact, the department manager acts as an intermediary to establish a connection between the boss and employees. It is important to use mediations appropriately. If there are too many mediations, the system complexity will be too high and communication efficiency will be reduced. Just like a company, the more departments and levels there are, the more difficult it is to manage, the more communication costs increase, and the efficiency of task execution decreases. Therefore, it is necessary to design the mediation class reasonably.
Six, open and close principle
The open closed principle defines: open for extension, closed for modification.
In fact, this sentence embodies the idea of encapsulation, inheritance and polymorphism. An entity class that already does what it does should not be modified, but should be extended if necessary. That sounds a lot like Richter’s substitution. In fact, the open close principle is more like a summary of several other principles, the ultimate goal is to use abstraction to build high-level modules, with implementation to extend the concrete details.
Richter’s substitution principle and dependency inversion principle tell you to abstract classes and methods. Single responsibility and interface isolation tell you how to make abstraction sound, and Demeter’s law tells you how to make concrete implementations that are cohesive and coupling less.
In fact, the six design principles have some rules that tell you that it’s better to do it this way, but if you don’t follow the rules, it’s ok, the code will still run, but it just increases the probability that the code will fail, it will not be robust, it will not be maintainable. It is just like many rules in our life, such as crossing the road, need to look at the traffic lights. However, if you don’t look at it and run a red light, no one can hurt you, but you increase the probability of being hit. So, by following the rules, we can minimize our losses.