preface

I haven’t written my blog for a long time. I have been making excuses for myself that I am too busy. I will write in a few days when I have time. The bottom line is that you’re just too lazy not to make up your mind. I decided to push myself and start learning the design patterns series today and write a blog about it. If I couldn’t do it, I would forfeit myself from playing games for a month.

The six principles

Without further discussion, this is the first article in my series of learning design patterns. This article mainly talks about the six principles that object-oriented design should follow. Mastering these principles can help us better understand the concept of object-oriented and also better understand the design pattern. The six principles are:

  • Single responsibility principle — SRP
  • Open and close principle — OCP
  • The li substitution principle is LSP
  • Dependency inversion principle — DIP
  • Interface isolation principle – ISP
  • Demeter principle — LOD

Single responsibility principle

Single Responsibility Principle, SRP. The definition is that there should be and only one class that causes a class to change, which means that a class has only one responsibility.

For example, in a start-up company, due to the lack of standard labor cost control and process, one person often needs to take on N responsibilities. An engineer may not only issue requirements, but also write codes, or even interview customers. There are several kinds of problems to be solved, which can be expressed simply in code.

public class Engineer {
    public void makeDemand(){}
    public void writeCode(){}
    public void meetClient(){}
}
Copy the code

The code might seem fine, because that’s how we normally write it, but if you look at it more closely, it’s clear that it doesn’t follow the single-responsibility principle, because there are at least three methods that can change a class, like one day for business reasons, Out demand method need to add a function (such as the demand of cost analysis), or see the customer also need the parameters such as, in that case the change of the class will have a variety of possibility and other references of the class class also needs corresponding change, if the number of the reference classes a lot, one can imagine the code maintenance cost will be high. So we need to break these methods up into separate responsibilities, so that one class is responsible for only one method, and each class can focus on its own methods.

Advantages of the single responsibility principle:

  • Class complexity is reduced, and what responsibilities are implemented are clearly defined;
  • The logic is simpler, the class is more readable, and because the logic is simpler, the code is more maintainable;
  • The risk of change is reduced because only changes are made in a single class.

The open closed principle

The Open Closed Principle is the most fundamental design Principle in the Java world. It is defined as:

A software entity such as a class, module, or function should be open for extension and closed for modification

That is, a software entity should make changes through extensions, not through modifications to existing code. This is a principle that constrains current development design for future events of software entities.

In the process of our coding, demand changes are constantly happening, when we need to modify the code, we should try to do not move the original code is not moving, through the way of expansion to meet the demand.

Following the open closed principle is the best means to abstract, such as single responsibility principle for engineers in front of the class, we are talking about the way out into a separate class, each class is responsible for the duties of a single, but from the perspective of the open closed principle, better way is to put the responsibility designed interface, such as writing the code method pull away into the form of the interface, the duty of at the same time, At the beginning of design, we need to take into account all the factors that may change in the future, such as the functions that may be divided into background and front end due to business needs in the future. At this time, we can design two interfaces at the beginning of design.

public interface BackCode{
	void writeCode();
}
Copy the code
public interface FrontCode{
	void writeCode();
}
Copy the code

If the business of the front-end code changes in the future, we only need to expand the functions of the front-end interface or modify the implementation class of the front-end interface, and the background interface and implementation class will not be affected, which is the benefit of abstraction.

Richter’s substitution principle

The Richter Substitution Principle, Liskov Substitution Principle, is defined as

If for every object o1 of type T1 there is an object O2 of type T2 such that the behavior of all programs P defined by T1 does not change when all objects o1 is replaced by O2, then type T2 is a subtype of type T1.

It’s a little tricky, but there’s a simple definition:

All references to base classes must be able to transparently use objects from their subclasses.

In layman’s terms, subclasses can appear wherever their parent can, and replacing them with subclasses does not raise any exceptions. But the reverse is not true, because a subclass can extend functionality that its parent doesn’t have, and it can’t change functionality that its parent does.

As we all know, the three characteristics of object orientation are encapsulation, inheritance and polymorphism, but none of them is “harmonious”. Because inheritance has many disadvantages, when a subclass inherits the parent class, although it can reuse the code of the parent class, but the attributes and methods of the parent class are transparent to the subclass, and the subclass can modify the member of the parent class at will. If the requirements change, when the subclass makes some copies of the methods of the parent class, the other subclasses may need to change, which violates the principle of encapsulation to some extent. The solution is to introduce the Richter substitution principle.

The Richter substitution principle defines a specification for good inheritance, which has four meanings:

A subclass can implement abstract methods of its parent class, but cannot override non-abstract methods of its parent class.

2, subclasses can have their own personality, can have their own attributes and methods.

3. Input parameters can be amplified when a subclass overrides or overrides a parent class’s method.

For example, the parent class has a method that takes a HashMap

public class Father {
    public void test(HashMap map){
        System.out.println("Parent class executed..."); }}Copy the code

The type of input parameter for the method of the same name in the subclass can be expanded, for example, if we input the parameter Map,

public class Son extends Father{
    public void test(Map map){
        System.out.println("Subclasses are executed..."); }}Copy the code

Let’s write a scenario class to test the method execution of the parent class,

public class Client { public static void main(String[] args) { Father father = new Father(); HashMap map = new HashMap(); father.test(map); }}Copy the code

Result output: the parent class is executed…

Because of the Richter’s substitution principle, subclasses can appear wherever their parent can, and replacing them with subclasses does not raise any exceptions. Let’s change the code, call the method of the subclass,

public class Client { public static void main(String[] args) { Son son = new Son(); HashMap map = new HashMap(); father.test(map); }}Copy the code

The result is the same, because the subclass is passed to the caller instead of the parent class, and the subclass’s method is never executed. This result is actually correct. If you want the subclass to execute, you can override the method body.

On the other hand, if the subclass has a smaller range of input parameter types than the parent class, such as Map and HashMap, then the result of executing the code above will be the method body of the subclass. Isn’t that right? Subclasses display their own content. In fact this is wrong, because there is no copy a subclass of the parent class method, with the same method was carried out, this will cause the confusion of logic, if the parent class is an abstract class, subclass’s implementation class, you pass such an implementation class has violated the intent of the parent, easy to cause logical confusion, so subclasses override or overloading of the parent class method input parameters must be the same or enlarged.

4. When a subclass overrides or overrides a method of the parent class, the output can be reduced, that is, the return value must be less than or equal to the method return value of the parent class.

Ensuring that programs follow the Richter substitution principle requires our programs to build abstractions, to build specifications through abstractions, and to extend details with implementations, so that it is often interdependent with the open and closed principle.

Dependency inversion principle

Dependence Inversion Principle, abbreviated as DIP, is defined as:

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;

What are high-level modules and low-level modules? The indivisible atomic logic is the bottom module, and the reassembly of atomic logic is the top module.

In the Java language, abstraction means either an interface or an abstract class, neither of which can be instantiated; The details are the classes that implement interfaces or inherit abstract classes, that is, the implementation classes that can be instantiated. The dependency inversion principle means that the dependency between modules occurs through abstraction, and the dependency between implementation classes is realized through interface, which is commonly known as interface oriented programming.

Let’s take singer singing as an example. For example, if a singer sings a Mandarin song, the code is:

public class ChineseSong {
    public String language() {
        return "Mandarin Song"; }} public class Singer {public void sing(ChineseSong song) {system.out.println ("Singer"+ song.language()); } } public class Client { public static void main(String[] args) { Singer singer = new Singer(); ChineseSong song = new ChineseSong(); singer.sing(song); }}Copy the code

Run the main method and the result is: singers sing Mandarin songs

Now, we need to add some difficulty to the singer, such as rap English songs, in this class, we found it is difficult to do. Because our Singer class relies on a specific implementation class ChineseSong, some people may say that we can add another method, but then we have modified the Singer class. If more songs need to be added in the future, then the Singer class has to be modified all the time. In other words, the dependent classes are already unstable, which is obviously not what we want.

So we need to use the idea of interface oriented programming to optimize our solution, change to the following code:

public interface Song {
    public String language();
}
public class ChineseSong implements Song{
    public String language() {
        return "Sing mandarin songs";
    }
}
public class EnglishSong implements Song {
    public String language() {
        return "Sing English songs"; }} public class Singer {// sing sing public void sing(Song Song) {system.out.println ("Singer"+ song.language()); } } public class Client { public static void main(String[] args) { Singer singer = new Singer(); EnglishSong englishSong = new EnglishSong(); // Sing English songs. Sing English songs. }}Copy the code

We separate songs into a single interface Song, and each Song type implements this interface and rewrite method. In this way, the code of the singer does not need to change. If we need to add a Song type, we just need to write another implementation class to inherit Song.

Through this interface oriented programming, our code has better scalability, while reducing coupling and improving system stability.

Interface Isolation Principle

Interface Segregation Principle, or ISP for short, is defined as:

A client should not rely on interfaces it does not need

This means that the client provides whatever interface it needs and removes unnecessary interfaces, which requires refinement of interfaces to ensure the purity of interfaces. To put it another way, the dependencies between classes should be built on the smallest interface, that is, a single interface.

You might wonder, isn’t creating a single interface a single responsibility principle? In fact is not, the requirements of the single responsibility principle is single class and interface functions, notice is a duty, a duty interface is there can be multiple method, and the requirements of the interface segregation principle is the method of the interface as little as possible, single module as far as possible, if you need to provide the client a lot of module, then the definition of the corresponding multiple interfaces, Do not define all module functions in one interface, as this will become bloated.

For example, nowadays smart phones are very developed and almost everyone has one. In our young people’s mind, a good smart phone should be cheap, good-looking and functional. Therefore, we can define an abstract interface of smart phones, ISmartPhone, with the code as follows:

public interface ISmartPhone {
    public void cheapPrice();
    public void goodLooking();
    public void richFunction();
}
Copy the code

Next, we define an implementation class for the mobile phone interface that implements these three abstract methods,

public class SmartPhone implements ISmartPhone{
    public void cheapPrice() {
        System.out.println("This phone is cheap ~~~~~");
    }

    public void goodLooking() {
        System.out.println("This phone looks good ~~~~~");
    }

    public void richFunction() {
        System.out.println("This phone has so many functions ~~~~~"); }}Copy the code

We then define an entity class User for the User and a constructor that takes ISmartPhone as an argument. We also define a method usePhone to call the interface’s methods.

public class User {

    private ISmartPhone phone;
    public User(ISmartPhone phone){
        this.phone = phone;
    }
    public void usePhone(){ phone.cheapPrice(); phone.goodLooking(); phone.richFunction(); }}Copy the code

It can be seen that when we instantiate the User class and call its method usePhone, the console will display the method body information of the three methods of the mobile phone interface. This design seems to have no major problems, but we can think carefully, ISmartPhone interface design has reached the optimal? Unfortunately, the answer is no, the interface can be optimized.

Because in addition to young people, middle-aged business people are also using smart phones, in their concept, smart phones do not need rich functions, even need not consider whether cheap (rich is capricious ~~~~), because successful people are busy, the requirements of smart phones are mostly atmospheric appearance, simple functions can be, That’s what they think smart phones should be, and then the ISmartPhone interface that we define is not applicable, because our interface defines that smart phones have to meet three features, and if we implement this interface, we have to implement all three methods, and for business people, The methods we define are cosmetic and reusable only. You might say, well, I could rewrite one implementation class, just implement the external methods, and leave the other two methods empty and write nothing, wouldn’t that be enough? But that doesn’t work either, because the User is referring to the ISmartPhone interface. It calls three methods, and you only implement two of them, so there are two less printed messages.

According to the interface isolation principle, we can split the interfaces of smart phones according to different features. In this way, the functions of each interface will become single, ensuring the purity of the interface. It also further improves the flexibility and stability of the code.

Demeter principle

The Law of Demeter principle, LoD for short, also known as the least Knowledge Principle, describes the rules as follows:

One object should know the least about other objects

In other words, a class should know the least about the classes it needs to couple or call. The closer the relationship between classes is, the greater the degree of coupling is, and the greater the influence of class changes on its coupled classes will be. This is also our core design-oriented principle: low coupling, high cohesion.

There’s another explanation for Demeter’s rule: only write to direct friends.

What is an immediate friend? Each object is bound to have a coupling relationship with other objects, and the coupling of two objects becomes a friend relationship. There are many types of this relationship, such as composition, aggregation, dependency and so on. Classes that appear in member variables, method parameters, and method return values are direct friends, while classes that appear in local variables are not direct friends. That is, unfamiliar classes are best left inside the class as local variables.

For example, before the P.E. class, the teacher asked the class leader to go to the physical education office to get 20 basketballs for later class. According to this scene, we can design three classes of Teacher, Monitor and BasketBall, as well as the method of issuing commands and the method of taking BasketBall takeBall.

Public class Teacher {// Command class Teacher to get the ball public voidcommand(Monitor monitor) { List<BasketBall> ballList = new ArrayList<BasketBall>(); // Initialize the number of basketballsfor(int i = 0; i<20; i++){ ballList.add(new BasketBall()); } // Tell the monitor to start to get the ball. Public void takeBall(List<BasketBall> balls) {public void takeBall(List<BasketBall> balls) { System.out.println("Number of basketballs:+ balls.size()); }}Copy the code

Then, we write a scenario class to test:

public class Client { public static void main(String[] args) { Teacher teacher = new Teacher(); teacher.command(new Monitor()); }}Copy the code

The result is as follows:

Number of basketballs: 20Copy the code

Although the result is correct, there are still problems in our program, because from the scene, the Teacher only needs to order the Monitor to take the BasketBall, the Teacher only needs a friend —-Monitor, but in the program, the Teacher’s method body relies on the BasketBall class, that is to say, The Teacher class communicates with an unfamiliar class, so the robustness of the Teacher class is broken, because once the BasketBall class changes, the Teacher needs to change too, which clearly violates Demeter’s law.

The Teacher class and the Monitor class are both dependent on the Teacher class and the Monitor class. The Teacher class and the Monitor class are dependent on the Teacher class.

Public class Teacher {// Command class Teacher to get the ball public voidcommand(Monitor Monitor) {// Tell the Monitor to start to get the ball. }} public class Monitor {// get the ball public voidtakeBall() { List<BasketBall> ballList = new ArrayList<BasketBall>(); // Initialize the number of basketballsfor(int i = 0; i<20; i++){ ballList.add(new BasketBall()); } System.out.println("Number of basketballs:+ ballList.size()); }}Copy the code

In this way, the Teacher class will not be dependent on the BasketBall class, even if the business needs to modify the BasketBall class will not affect the Teacher class.

conclusion

Ok, so that’s the six principles of object orientation. In fact, it is not difficult to find that although the six principles are principles, they are not mandatory, but more suggestions. In accordance with these principles not only can help us to better regulate our system design and code habit, but not all of the scenarios, such as the interface segregation principle, in the real system development, it is difficult for us to fully comply with a module interface design, otherwise the business will appear more excessive code design, make the whole system is too big, Increase the complexity of the system, and even affect their own project schedule, not worth the loss ah.

So again, choose the right technology for the right scenario!

Reference: Zen of Design Patterns