Today, I updated an article about the six principles of design patterns, mainly learning the path class. I will look for opportunities to write more interview articles in the future. So, click attention, don’t get lost, you can see my update first!

| super dry

One, single responsibility principle

1, define,

First, let’s look at the definition of a single responsibility.

Single Responsibility Principle, SRP for short.

A class should have only one reason to change. There should be only one reason for a class to change

There should be only one reason for a class to change. There should be only one duty.

If a class has more than one responsibility, those responsibilities are coupled. A change in one responsibility may impair or inhibit the class’s ability to perform other responsibilities. This leads to fragile designs.

When one responsibility changes, it may affect other responsibilities. In addition, multiple responsibilities are coupled together, which affects reusability. To avoid this, adhere to the single responsibility principle as much as possible.

The weather is getting hotter and hotter. Don’t forget to study in your spare time! Maximum efficiency can be achieved by combining work with rest. I sorted out a lot of detailed Java learning materials, a number of Internet companies and a number of big men’s interview materials, as well as more interview skills. Friends in need can click into access. Code word: nuggets.

2. Why the single responsibility principle

Usually, we need to know why we do things before we do them. So why do we use the single responsibility principle?

(1) Improve the maintainability and readability of classes

When a class has fewer responsibilities and less complexity, it has less code, better readability, and higher maintainability.

(2) Improve the maintainability of the system

The system is composed of classes, each class of high maintainability, relatively speaking, the whole system of high maintainability. Of course, the premise is that the architecture of the system is ok.

(3) Reduce the risk of change

The more responsibilities a class has, the greater the likelihood of change and the greater the risk of change

If there are multiple things that can change in a class, this design is risky, and we try to make sure that only one of them can change and the others change in the other classes, which has the benefit of improving cohesion and reducing coupling.

3. Scope of application of single responsibility principle

The single responsibility principle applies to interfaces, methods, and classes. By all accounts, interfaces and methods must guarantee a single responsibility, and classes need not, as long as they conform to the business.

3.1 Application of single responsibility principle (method level)

You now have a scenario where you need to change a user’s username and password. We can have multiple implementations of this functionality.

The first:

/** * Operation type */
public enum OperateEnum {
    UPDATE_USERNAME,
    UPDATE_PASSWORD;
}

public interface UserOperate {
    void updateUserInfo(OperateEnum type, UserInfo userInfo);
}

public class UserOperateImpl implements UserOperate{
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="3">@Override
    public void updateUserInfo(OperateEnum type, UserInfo userInfo) {
        if (type == OperateEnum.UPDATE_PASSWORD) {
            // Change the password
        } else if(type == OperateEnum.UPDATE_USERNAME) {
            // Change the user name
        }
    }
}</a>
Copy the code

The second:

public interface UserOperate {
    void updateUserName(UserInfo userInfo);

    void updateUserPassword(UserInfo userInfo);
}

public class UserOperateImpl implements UserOperate {
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="4">@Override
    public void updateUserName(UserInfo userInfo) {
        // Modify the username logic
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="5">@Override
    public void updateUserPassword(UserInfo userInfo) {
        // Change the password logic
    }
}</a>
Copy the code

Let’s look at the differences between the two implementations:

The first implementation is based on the type of operation, which performs different logic. Changing the username and password are coupled together. An error occurs if the client passes the wrong type while operating.

The second implementation is the one we recommend. Changing the user name and password are logically separated. Each carrying out his own duties without interfering with each other. The function is clear and clear.

Thus, the second design is consistent with the single responsibility principle. This is implementing the single responsibility principle at the method level.

3.2 Application of the Single Responsibility Principle (interface Level)

Let’s assume a scenario, we do housework together, Zhang SAN sweeping the floor, Li Si shopping. Li Si had to cook when he came back from shopping. How does this logic work?

A:

/** * do the housework */
public interface HouseWork {
    / / sweep the floor
    void sweepFloor(a);

    / / shopping
    void shopping(a);
}

public class Zhangsan implements HouseWork{
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="6">@Override
    public void sweepFloor(a) {
        / / sweep the floor
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="Seven">@Override
    public void shopping(a) {}}public class Lisi implements HouseWork{
    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="8">@Override
    public void sweepFloor(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="9">@Override
    public void shopping(a) {
        / / shopping
    }
}
</a>
Copy the code

First, it defines an interface to do housework, and defines two methods to sweep the floor and buy vegetables. Three sweep the floor, to achieve the sweeping interface. When Li Si buys food, he realizes the interface of buying food. Add a method to the interface class called cooking. Both John and John rewrite this method, but only John has a concrete implementation.

The design itself is unreasonable.

First of all, Zhang SAN only sweep the floor, but he needs to rewrite the way to buy food, Li Si does not need to sweep the floor, but Li Si also need to rewrite the sweeping method.

Second, this is also inconsistent with the principle of open and close, add a type of cooking, to modify 3 classes. This can lead to unexpected errors when the logic is complex.

The above design does not conform to the principle of single responsibility, and changes in one place affect other areas that do not need to be changed.

Method 2:

/** * do the housework */
public interface Hoursework {}public interface Shopping extends Hoursework{
    / / shopping
    void shopping(a);
}

public interface SweepFloor extends Hoursework{
    / / sweep the floor
    void sweepFlooring(a);
}

public class Zhangsan implements SweepFloor{

    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="10">@Override
    public void sweepFlooring(a) {
        // Zhang SAN sweeps the floor}}public class Lisi implements Shopping{
    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="11">@Override
    public void shopping(a) {
        // Li Si goes shopping
    }
}</a>
Copy the code

Doing housework above is not defined as an interface, but it separates sweeping and doing housework.

Zhang SAN sweeping the floor, then Zhang SAN will realize the interface sweeping the floor; Li Si shopping, Li Si on the realization of shopping interface; After Li Si wants to add a function to cook.

Then add a cooking interface, this time only need li Si to achieve cooking interface can be.

public interface Cooking extends Hoursework{ 
    void cooking(a);
}

public class Lisi implements Shopping.Cooking{
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="12">@Override
    public void shopping(a) {
        // Li Si goes shopping
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="13">@Override
    public void cooking(a) {
        // Li Si cooks
    }
}</a>
Copy the code

As shown above, we see that John doesn’t implement the redundant interface, and neither does Tom. And when new features are added, they only affect Li Si, not Zhang SAN.

This is consistent with the single responsibility principle. A class does only one thing, and its modifications do not change anything else.

3.3 Application of single responsibility principle (class level)

At the class level, there is no way to split it completely into a single responsibility. In other words, the responsibilities of a class can be large or small, and unlike interfaces, they can be explicitly broken down according to a single responsibility principle, as long as it makes logical sense.

For example, we can register, log in, wechat log in, register log in and other operations on the home page of the website. Our usual practices are as follows:

public interface UserOperate {

    void login(UserInfo userInfo);

    void register(UserInfo userInfo);

    void logout(UserInfo userInfo);
}

public class UserOperateImpl implements UserOperate{
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="14">@Override
    public void login(UserInfo userInfo) {
        // User login
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="15">@Override
    public void register(UserInfo userInfo) {
        // User registration
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="16">@Override
    public void logout(UserInfo userInfo) {
        // The user logs out
    }
}</a>
Copy the code

If divided according to the single responsibility principle, it can also be broken down into the following forms:

public interface Register {
    void register(a);
}

public interface Login {
    void login(a);
}

public interface Logout {
    void logout(a);
}

public class RegisterImpl implements Register{

    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="17">@Override
    public void register(a) {}}public class LoginImpl implements Login{
    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="18">@Override
    public void login(a) {
        // User login}}public class LogoutImpl implements Logout{

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="19">@Override
    public void logout(a) {

    }
}
</a>
Copy the code

Is it ok to write like this? In fact, it can be, but there are many classes. If there is a lot of code for logging in, registering, and logging out, you can write this.

4. How to follow the single responsibility principle

4.1 Reasonable breakdown of responsibilities

The same responsibilities are put together, and different responsibilities are decomposed into different interfaces and implementations. This is the easiest and most difficult principle to apply. The key is to identify the same type of responsibilities from the perspective of business and requirements.

Example: The analysis of human behavior includes the analysis of living and working behaviors. Living behaviors include eating, running and sleeping, while working behaviors include commuting and meeting, as shown in the figure below:

Human behavior is divided into two interfaces: the living behavior interface, the work behavior interface, and two implementation classes. Sharing the responsibilities of both interfaces with a single implementation class would result in bloated, unmaintainable code, and the risk of change if additional behaviors, such as learning behavior interfaces, were added later (the composite pattern is also used here).

4.2 Look at the implementation of simple code

Step 1: Define a behavior interface

There are two kinds of human behavior: living behavior and working behavior
public interface IBehavior {}Copy the code

It defines an empty interface, the behavior interface. What are the interfaces under this behavioral interface? There are two aspects of life and work.

Step 2: Define life and work interfaces, and they are subclasses of behavior interfaces

Living behavior Interface:

public interface LivingBehavior extends IBehavior{
    / * * * / for dinner
    void eat(a);

    Run / * * * /
    void running(a);

    / * * * / sleep
    void sleeping(a);
}
Copy the code

Work behavior Interface:

public interface WorkingBehavior extends IBehavior{

    / * * * / work
    void goToWork(a);

    / * * * / work
    void goOffWork(a);

    / * * * / the meeting
    void meeting(a);
}
Copy the code

Step 3: Define the implementation classes for the working and living behavior interfaces

Living behavior interface implementation class:

public class LivingBehaviorImpl implements LivingBehavior{
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="20">@Override
    public void eat(a) {
        System.out.println("Eat");
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="21">@Override
    public void running(a) {
        System.out.println("Running");
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="22">@Override
    public void sleeping(a) {
        System.out.println("Sleep");
    }
}</a>
Copy the code

Working behavior interface implementation class:

public class WorkingBehaviorImpl implements WorkingBehavior{

    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="23">@Override
    public void goToWork(a) {
        System.out.println("Work");
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="24">@Override
    public void goOffWork(a) {
        System.out.println("Work");
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="25">@Override
    public void meeting(a) {
        System.out.println("The meeting");
    }
}</a>
Copy the code

Step 4: Behavior composite invocation

Now that the behavior interface is defined, a collection of behaviors is defined. Different users have different behaviors, some users only use life behavior, some users have both life behavior and work behavior

We don’t know exactly what the user will do, so we usually use a collection to receive the user’s behavior and add the user’s behavior to it.

(1) Behavior composition interface BehaviorComposer

public interface BehaviorComposer {
    void add(IBehavior behavior);
}
Copy the code

(2) Behavior combination interface implementation class IBehaviorComposerImpl

public class IBehaviorComposerImpl implements BehaviorComposer {

    private List<IBehavior> behaviors = new ArrayList<>();
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="26">@Override
    public void add(IBehavior behavior) {
        System.out.println("Add behavior");
        behaviors.add(behavior);
    }

    public void doSomeThing(a) {
        behaviors.forEach(b->{
            if(b instanceof LivingBehavior) {
                LivingBehavior li = (LivingBehavior)b;
                // Deal with life behaviors
            } else if(b instanceof WorkingBehavior) {
                WorkingBehavior wb = (WorkingBehavior) b;
                // Handle work behavior}}); } }</a>Copy the code

Step 5: Client call

When the user calls, it can be called according to the actual situation, such as the following code: Zhang SAN is a full-time mother, only life behavior, Li Si is a working mother, both life behavior and work behavior.

public static void main(String[] args) {
        // Zhang SAN -- stay-at-home mom
        LivingBehavior zslivingBehavior = new LivingBehaviorImpl();
        BehaviorComposer zsBehaviorComposer = new IBehaviorComposerImpl();
        zsBehaviorComposer.add(zslivingBehavior);

        // Li Si -- working mother
        LivingBehavior lsLivingBehavior = new LivingBehaviorImpl();
        WorkingBehavior lsWorkingBehavior = new WorkingBehaviorImpl();

        BehaviorComposer lsBehaviorComposer = new IBehaviorComposerImpl();
        lsBehaviorComposer.add(lsLivingBehavior);
        lsBehaviorComposer.add(lsWorkingBehavior);
    }
Copy the code

You can see the benefits of a single responsibility.

Advantages and disadvantages of the single responsibility principle

  • Class complexity reduction: When a class has a clear definition of what it does, complexity is reduced

  • Improved readability: As complexity decreases, readability increases

  • Increased maintainability: Improved readability makes code easier to maintain

  • Risk reduction caused by change: Change is essential, if the single responsibility of the interface is done well, an interface modification will only affect the corresponding implementation class, no impact on other interfaces and classes, which will greatly help the system’s scalability and maintenance

2. Richter’s substitution principle

How many of you don’t know about the li substitution rule? We’ve been writing code for years, using inheritance, subclassing every day. But you don’t know the Li substitution principle? Check it out.

1. What is Richter’s substitution principle

1.1 Definition of Richter’s substitution principle

The li substitution principle is used to help us design parent-child classes in inheritance relationships.

The Liskov Substitution principle is specifically defined for subtypes. Why is it called the Li substitution principle? This principle was first proposed in 1988 by a woman named Barbara Liskov at the Massachusetts Institute of Technology.

Richter’s substitution principle mainly describes some principles about inheritance, that is, when inheritance should be used, when inheritance should not be used, and the underlying principles. Richter substitution is the basis of inheritance reuse, which reflects the relationship between base class and subclass, is a supplement to the open and closed principle, and is the specification of concrete steps to achieve abstraction.

Richter’s substitution principle has two definitions:

Definition 1:

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program. If S is a subclass of T, an object of T can be replaced with an object of S without breaking the program.

Definition 2:

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing The it. All references to methods of its parent class can be transparently replaced with subclass objects.

In fact, both of these definitions mean the same thing, that is, anywhere a parent object appears in an application, we can replace it with an object of its subclass, and ensure the logical behavior and correctness of the original program.

1.2 There are at least two definitions of Richter substitution

(1) Richter’s substitution principle is for inheritance. If inheritance is to achieve code reuse, that is, to share methods, then shared parent methods should remain unchanged and cannot be redefined by subclasses. Subclass only through new add methods to extend the function of parent and child classes can be instantiated, and a subclass inherits methods and the parent class is the same, the parent class method is called, the subclass can also call the same inherited, logic, and the parent class consistent method, then use a subclass object replace superclass object, of course, logically consistent, smoothly.

(2) if the inheritance is for the purpose of polymorphism, and polymorphism is the premise of subclasses override and redefine the superclass method, in order to conform to the LSP, we should be the parent class is defined as an abstract class, and defines the abstract method, let subclasses redefine the method, when the parent class is an abstract class is the parent class cannot be instantiated, so there is no can instantiate the superclass object in the program. There is no possibility of logical inconsistencies when subclasses replace superclass instances (there are no superclass instances at all).

Do not conform to the LSP is the most common case, parent and child classes can be instantiated of abstract classes, and methods of the parent be subclasses redefine, such implementation inheritance will cause the strong coupling between parent and child classes, which is actually not related properties and methods of far-fetched together, is not conducive to expand and maintain.

2. The purpose of using Richter’s substitution principle

The Richter substitution principle is used to reduce the disadvantages of inheritance, enhance the robustness of the program, and maintain good compatibility during version upgrades. The original subclass can continue to run even if subclasses are added.

3. The relationship between Richter’s substitution principle and inherited polymorphism

The Li substitution principle is related to inherited polymorphism, but they are not the same thing. Let’s look at the following case

public class Cache {
    public void set(String key, String value) {}}public class Redis extends Cache {
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="3">@Override
    public void set(String key, String value) {}}public class Memcache extends Cache {
    </a><a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="4">@Override
    public void set(String key, String value) {}}public class CacheTest {
    public static void main(String[] args) {
        // Superclass objects can accept subclass objects
        Cache cache = new Cache();
        cache.set("key123"."key123");

        cache = new Redis();
        cache.set("key123"."key123");

        cache = new Memcache();
        cache.set("key123"."key123");
    }
}</a>
Copy the code

From the above example, you can see that Cache is the parent class, Redis and Memcache are subclasses, they inherit from Cache, this is the idea of inheritance and polymorphism. And both subclasses have so far complied with the Li substitution principle, which allows them to replace the parent class anywhere it appears, without breaking the logic of the original code.

Looking at the last CacheTest class, our cache using a superclass can receive any type of cache object, both superclass and subclass.

But if we do a length check on the set method in Redis

public class Redis extends Cache{
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="5">@Override
    public void set(String key, String value) {
        if (key == null || key.length() < 10 || key.length() > 100) {
            System.out.println("The length of the key does not meet requirements");
            throw newIllegalArgumentException(the length of key is not required); }}}public class CacheTest {
    public static void main(String[] args) {
        // Superclass objects can accept subclass objects
        Cache cache = new Cache();
        cache.set("key123"."key123");

        cache = new Redis();
        cache.set("key123"."key123");
    }
}</a>
Copy the code

In the case above, if we use a superclass object instead of a subclass object, an exception is thrown. The logical behavior of the program has changed, although the code after transformation can still be replaced by a subclass to the parent class, but from the point of view of design ideas, the design of Redis subclass is not in line with the principle of substitution.

Inheritance and polymorphism is an object-oriented language provides a syntax, is a code implementation of train of thought, and type in the replacement is a kind of thought, a kind of design principle, inheritance is used to guide the child how to design, the design of the subclass to guarantee in the replacement of the parent class, without changing the original program logic, and does not destroy the correctness of the original program.

4. Rules for substitution in

The core of the Li substitution principle is the “convention”, the convention of the parent and child classes. Richter’s substitution principle requires subclasses to comply with some behavior conventions of their parents when designing. Behavior conventions include: functions to be implemented by functions, conventions for input, output, exceptions, and even special instructions in comments.

4.1 A subclass method must not violate the convention of the superclass method on input and output exceptions

(1) Preconditions cannot be strengthened

The pre-condition is that the input parameter cannot be reinforced. As in the Cache example above, the Redis subclass has reinforced the requirement for the input parameter Key. In this case, replacing the superclass object with the subclass object at the call may raise an exception.

That is, if a subclass checks the input data more strictly than its parent class, the subclass’s design violates the li substitution principle.

(2) The postcondition cannot be weakened

The post-condition is the output. Assume that the output parameter of our superclass method convention is greater than 0, and the program calling the superclass method validates the output parameter greater than 0 according to the convention. A subclass implements a value less than or equal to zero. In this case, subclasses violate the Richter substitution rule.

(3) Can not violate the convention on exceptions

In a parent class, if a convention throws only ArgumentNullException, the subclass is designed to throw only ArgumentNullException. Throwing any other exception will cause the subclass to violate the li substitution principle.

4.2 A subclass method cannot violate the functionality defined by a superclass method

public class Product {
    private BigDecimal amount;
    private Calendar createTime;

    public BigDecimal getAmount(a) {
        return amount;
    }
    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }

    public Calendar getCreateTime(a) {
        return createTime;
    }
    public void setCreateTime(Calendar createTime) {
        this.createTime = createTime; }}public class ProductSort extends Sort<Product> {

    public void sortByAmount(List<Product> list) {
        // Sort by timelist.sort((h1, h2)->h1.getCreateTime().compareTo(h2.getCreateTime())); }}Copy the code

The sortByAmount() function provided in the parent class sorts by the smallest amount to the largest amount, while the subclass overrides the sortByAmount() function to sort by the date it was created. The design of that subclass violates the substitution rule.

In fact, there is a trick to proving that the design of a subclass complies with Richter’s substitution principle. You can use the single test of the parent class to run the code of the subclass. If it doesn’t work, then you need to consider whether your design is reasonable.

4.3 Subclasses must fully implement the parent class’s abstract methods

If you design a subclass that does not fully implement the abstract methods of its parent class then your design does not satisfy the li substitution principle.

// Define an abstract class gun
public abstract class AbstractGun{
    / / shooting
    public abstract void shoot(a);

    / / kill
    public abstract void kill(a);
}
Copy the code

For example, we define an abstract class of guns that can shoot and kill. Both rifles and guns can shoot and kill, and we can define subclasses to inherit from the parent class.

// Define ***, rifle, machine gun
public class Handgun extends AbstractGun{   
    public void shoot(a){  
         / / * * *
    }

    public void kill(a){    
        / / * * * kill}}public class Rifle extends AbstractGun{
    public void shoot(a){
         // Rifle shooting
    }

    public void kill(a){    
         // Rifles kill people}}Copy the code

But if we add a toy gun to the inheritance system, there will be a problem, because toy guns can only shoot, not kill. But that’s what a lot of people do when they write code.

public class ToyGun extends AbstractGun{
    public void shoot(a){
        // Toy gun shooting
    }

    public void kill(a){ 
        // Since the toy gun can't kill, return empty, or throw an exception
        throw new Exception("I'm a toy gun. Is it surprise, surprise, sting?"); }}Copy the code

At this point, it would obviously be a problem to replace the parent object with a subclass object (a soldier goes to war and finds himself carrying a toy).

This situation not only does not meet the Richter substitution principle, but also does not meet the interface isolation principle. In this scenario, interface isolation + delegation can be used to solve the problem.

5. The role of Richter’s substitution principle

(1) Richter’s substitution principle is one of the important ways to realize the open-close principle.

(2) It overcomes the disadvantage of reusability deterioration caused by overwriting parent class in inheritance.

(3) It is the guarantee of the correctness of the action. That is, class extensions do not introduce new errors into existing systems, reducing the likelihood of code errors.

(4) To strengthen the robustness of the program, at the same time, it can achieve very good compatibility when changing, improve the maintenance and scalability of the program, and reduce the risk introduced when the demand changes.

Instead of inheriting from an instantiable parent class, use inheritance based on abstract classes and interfaces.

6. Implementation method of Richter’s substitution principle

In plain English, the Richter substitution principle says that a subclass can extend the functionality of its parent class, but cannot change the functionality of its parent class. In other words, when a subclass inherits from a parent class, try not to override the methods of the parent class, except to add new methods to accomplish new functions.

Based on the above understanding, the definition of Richter’s substitution principle can be summarized as follows:

(1) A subclass can implement abstract methods of the parent class, but cannot override non-abstract methods of the parent class

(2) Subclasses can add their own unique methods

(3) When a method of a subclass overrides a method of the parent class, the preconditions (that is, the method’s input parameters) of the method are looser than the method of the parent class

(4) When a method of a subclass implements a method of the parent class (overriding/overloading or implementing an abstract method), the postcondition of the method (that is, the output/return value of the method) is stricter or equal to that of the method of the parent class

Although it is simple to write the new function by rewriting the parent class method, the reusability of the whole inheritance system will be poor, especially when the application of polymorphism is more frequent, the probability of program running error will be very large.

If a program violates the Richter substitution principle, an object that inherits from a class will get a runtime error where the base class appears. At this time, the correction method is to cancel the original inheritance relationship and redesign the relationship between them.

The most famous example of Richter’s substitution is “a square is not a rectangle”. Of course, there are many similar examples in life, for example, penguins, ostriches and kiwis are biologically divided, they belong to birds; However, from the perspective of class inheritance, they cannot be defined as a subclass of “bird” because they cannot inherit the function of “bird” to fly. Similarly, “balloon fish” cannot swim, so it cannot be subclassed as “fish”; “Toy gun” does not blow up enemies, so it cannot be defined as a subclass of “gun”.

7. Case analysis

Case 1: Subtract two numbers

When using inheritance, follow the Richter substitution principle. When class B inherits from class A, do not overwrite the methods of the parent class A or override the methods of the parent class A, except adding new methods to complete the new function P2.

Inheritance contains a layer of meaning: those who have achieved good method in the parent class (relative to the abstract method), are actually set a series of specifications and contract, although it is not compulsory for all the subclasses must conform to the contract, but if the subclasses for any modification, abstract methods of these products will damage to the entire inheritance system. Richter’s substitution principle is the expression of this meaning.

Inheritance, as one of the three characteristics of object orientation, brings great convenience to program design as well as disadvantages. Using inheritance will bring program invasive, for example, application of portability is reduced, increase the coupling between the objects, if a class is inherited by other classes, is when this class needs to be modified, must consider all the subclasses, and modified the parent class, all involves the function of the subclass may lead to failure.Copy the code
class A{
    public int func1(int a, int b){
        returna-b; }}public class Client{
    public static void main(String[] args){
        A a = new A();
        System.out.println("100-50 ="+a.func1(100.50));
        System.out.println("100-80 ="+a.func1(100.80)); }}Copy the code

Running results:

100-50 = 50

100-80 = 20

Later, we need to add a new feature: add the two numbers and then add them to 100, handled by class B. Class B needs to do two things:

1. Subtract two numbers.

2. Add the two numbers and add 100.

Since class A already implements the first function, class B only needs to complete the second function after inheriting from class A. The code is as follows:

class B extends A{
    public int func1(int a, int b){
        return a+b;
    }

    public int func2(int a, int b){
        return func1(a,b)+100; }}public class Client{
    public static void main(String[] args){
        B b = new B();
        System.out.println("100-50 ="+b.func1(100.50));
        System.out.println("100-80 ="+b.func1(100.80));
        System.out.println("100 + 20 + 100 ="+b.func2(100.20)); }}Copy the code

After class B is completed, the running results are as follows:

100-50 = 150

100-80 = 180

100 + 20 + 100 = 220

We found an error in the subtraction function that worked normally. The reason is that class B inadvertently overwrites the method of the parent class when naming the method, causing all the codes running the subtraction function to call the method overwritten by class B, resulting in the error of the function that used to run normally.

In this case, an exception occurs after subclass B replaces the function done by referring to base class A. In actual programming, we often rewrite the method of the parent class to complete the new function, although it is simple to write, but the reusability of the whole inheritance system will be poor, especially when the use of polymorphism is more frequent, the probability of program running error is very large.

If you have to rewrite the method of the parent class, the more common approach is: the original parent class and child class inherit a more popular base class, the original inheritance relationship removed, use dependency, aggregation, composition, etc.

Case 2: “Kiwis are not Birds”

Requirement analysis: Birds are usually able to fly, such as swallows at 120 km/h, but the Kiwi in New Zealand cannot fly due to the degeneration of its wings. Suppose you wanted to design an example of how long it would take these two birds to fly 300 kilometers. Obviously, testing the code with swallow gave the correct result for calculating the time required; However, when the kiwi is tested, the result will be “division by zero anomaly” or “infinity”, which obviously does not meet the expectation. Its class diagram is shown in Figure 1.

The source code is as follows:

/ * * * * / bird
public class Bird {
    // The speed of flight
    private double flySpeed;

    public void setFlySpeed(double flySpeed) {
        this.flySpeed = flySpeed;
    }

    public double getFlyTime(double distance) {
        returndistance/flySpeed; }}/** ** */
public class Swallow extends Bird{}/** * Kiwi */
public class Kiwi extends Bird {
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="6">@Override
    public void setFlySpeed(double flySpeed) {
        flySpeed = 0; }}/** * Test flights take time */
public class BirdTest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Bird bird2 = new Kiwi();
        bird1.setFlySpeed(120);
        bird2.setFlySpeed(120);
        System.out.println("If you fly 300 kilometers:");
        try {
            System.out.println("Swallows spend." + bird1.getFlyTime(300) + "Hours.");
            System.out.println("Kiwi expenses" + bird2.getFlyTime(300) + "Hours.");
        } catch (Exception err) {
            System.out.println("An error has occurred!);
        }
    }
}
</a>
Copy the code

Running results:

If you fly 300 km:

Swallows spend 2.5 hours.

Kiwi takes Infinity hours.

The reason for the error is that the Kiwi has overwritten the bird setSpeed(double speed) method, which violates the Richter’s substitution principle. Instead: cancel the original kiwi inheritance and define birds and kiwis’ more general parents, such as animals, that are capable of running. Although the flying speed of the kiwi is 0, the running speed is not 0, so the time it takes to run 300 kilometers can be calculated. Its class diagram is shown in Figure 2.

The source code:

/** * animal */
public class Animal {
    private double runSpeed;

    public double getRunTime(double distance) {
        return distance/runSpeed;
    }

    public void setRunSpeed(double runSpeed) {
        this.runSpeed = runSpeed; }}/ * * * * / bird
public class Bird {
    // The speed of flight
    private double flySpeed;

    public void setFlySpeed(double flySpeed) {
        this.flySpeed = flySpeed;
    }

    public double getFlyTime(double distance) {
        returndistance/flySpeed; }}/** ** */
public class Swallow extends Bird {}/** * Kiwi */
public class Kiwi extends Animal {
    <a href="/profile/992988" data-card-uid="992988" class="" target="_blank" from-niu="default" data-card-index="Seven">@Override
    public void setRunSpeed(double runSpeed) {
        super.setRunSpeed(runSpeed); }}/** * Test flights take time */
public class BirdTest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Animal bird2 = new Kiwi();
        bird1.setFlySpeed(120);
        bird2.setRunSpeed(110);
        System.out.println("If you fly 300 kilometers:");
        try {
            System.out.println("Swallows spend." + bird1.getFlyTime(300) + "Hours.");
            System.out.println("Kiwi costs." + bird2.getRunTime(300) + "Hours.");
        } catch (Exception err) {
            System.out.println("An error has occurred!);
        }
    }
}
</a>
Copy the code

Running results:

If you fly 300 km:

Swallows spend 2.5 hours.

Kiwi takes 2.727272727272727 hours.

summary

In the object-oriented programming thought provides the inheritance and polymorphism is we can very good implementation code reusability and extensibility, but inherit it is not without faults, because of the inheritance is in itself a intrusive, if improper use will greatly increase the coupling of the code, and reduce the flexibility of the code, increase our maintenance costs, However, the abuse of inheritance often occurs in the actual use process, and the li substitution principle can help us to design parent-child class in the inheritance relationship.

3. Reliance inversion principle

1. What is the dependency inversion principle

1.1 concept

Dependence Inversion Principle (DIP), its meaning:

(1) High-level modules should not depend on low-level modules; both should depend on their abstractions

(2) Abstraction should not depend on detail, but detail should depend on abstraction

(3) Program for interfaces, not implementations

1.2 What is dependency

Dependencies here are understood as dependencies in UML relationships. To put it simply, A uses B, so A becomes dependent on B. See the following example for details.

From the above figure, we can see that class B is used in method A () of class A. In fact, this is A dependency relationship. It is important to note that A is not called A dependency if B is declared in A. If B is referenced but no method is actually called, it is called zero-coupling. The diagram below:

1.3 Types of dependencies

(1) Zero coupling relationship

(2) Direct coupling: Concrete coupling occurs between two concrete (instantiable) classes, through a direct reference from one class to the other.

(3) Abstract coupling: Abstract coupling occurs between a concrete class and an abstract class (or Java interface), allowing maximum flexibility between two classes that must be identical.

The dependency inversion principle is to program for interfaces, not implementations. That is, you should use interfaces or abstract classesVariable type declaration, parameter type declaration, method return type description, and data type conversion, etc.

2. Dependency inversion cases

2.1 Preliminary design scheme

public class Benz {
    public void run(a) {
        System.out.println("The Mercedes is running!); }}public class Driver {
    private String name;
    public Driver(String name) {
        this.name = name;
    }

    public void driver(Benz benz) { benz.run(); }}public class CarTest {
    public static void main(String[] args) {
        Benz benz = new Benz();
        Driver driver = new Driver("Zhang"); driver.driver(benz); }}Copy the code

There’s a driver John who can drive a Mercedes, so at first we thought, there’s a driver class, there’s a Mercedes class. As the business grew, we discovered that driver Zhang SAN could also drive a BMW.

So, we define a BM class:

public class BM {
    public void run(a) {
        System.out.println("BMW is running!"); }}Copy the code

At this time, if Zhang SAN wants to drive a BMW, he will register the BMW in his name:

public class Driver {
    private String name;
    public Driver(String name) {
        this.name = name;
    }

    public void driver(Benz benz) {
        benz.run();
    }

    public void driver(BM bm) { bm.run(); }}public class CarTest {
    public static void main(String[] args) {
        Benz benz = new Benz();
        BM bm = new BM();
        Driver driver = new Driver("Zhang"); driver.driver(benz); driver.driver(bm); }}Copy the code

That seems to work, but what’s wrong with that?

(1) If Zhang SAN wants to drive Volkswagen one day, a Volkswagen car category should be added, and the driver’s name should also be attached.

(2) Not everyone should drive a Mercedes, a BMW or a Volkswagen.

That’s implementation-oriented programming, and then we’re going to think about interface oriented programming.

2.2 Improved scheme

public interface ICar {
    public void run(a);
}

public class Benz implements ICar{
    public void run(a) {
        System.out.println("The Mercedes is running!); }}public class BM implements ICar{
    public void run(a) {
        System.out.println("BMW is running!"); }}public interface IDriver {
    public void driver(ICar car);
}

public class Driver implements IDriver{

    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void driver(ICar car) { car.run(); }}public class CarTest {
    public static void main(String[] args) {
        IDriver driver = new Driver();
        driver.driver(new Benz());
        driver.driver(new BM());
    }
}</a>
Copy the code

After the modification of the code, extracted an IDriver interface and ICar interface, interface programming. The IDriver implementation class driver can drive any type of car, so the passed parameter is also an interface ICar. Any type of car can be registered as a new type of car by implementing the ICar interface. When the client calls, just pass in the corresponding car.

3, the way of dependence

3.1 There are three main methods of dependency injection

(1) construct injection, which inject dependencies at construct time.

(2) Setter method injection

(3) Injection into the interface method (the car example uses this method)

3.2 Embodiment of dependency inversion principle in design pattern

(1) Simple factory design mode, using interface method injection

(2) Strategy design pattern: inject in constructor. For specific usage, check out the following two articles:

(1) Simple factory design mode; ② Strategy design mode;

4. Interface isolation principle

1, define,

Clients should not be forced to depend upon interfaces that they don’t use. The client only relies on the interface it needs; It provides all the interfaces it needs and excludes those it doesn’t.

The dependency of one class to another one should depend on the smallest possible interface. Dependencies between classes should be established on the smallest interface.

In other words, the interface should be as detailed as possible and the methods in the interface should be as few as possible.

Interface isolation principle and single responsibility principle

From the perspective of function, the interface isolation principle and the single responsibility principle both aim to improve the cohesion of classes and reduce the coupling between classes, reflecting the idea of encapsulation. But there is a difference.

(1) From the perspective of principle constraints: interface isolation principle pays more attention to the isolation of interface dependency degree; The principle of single responsibility pays more attention to the division of interface responsibility.

(2) From the level of interface refinement: the principle of single responsibility divides interfaces more precisely, while the principle of interface isolation focuses on the isolation of interfaces with the same function. The smallest interface in interface isolation can sometimes be multiple common interfaces with a single responsibility.

(3) The principle of single responsibility is more inclined to the constraint of business: the principle of interface isolation is more inclined to the constraint of design architecture. This should make sense. Responsibilities are divided according to business functions, so the single principle is more business oriented; Interface isolation, on the other hand, is more about “high cohesion,” favoring architectural design.

3. Advantages of interface isolation

The interface isolation principle is used to constrain interfaces and reduce the dependency of classes on interfaces. It has the following five advantages.

(1) Decompose the bloated interface into multiple small-grained interfaces, which can prevent the diffusion of external changes and improve the flexibility and maintainability of the system.

(2) Interface isolation improves the cohesion of the system, reduces external interaction and reduces the coupling of the system.

(3) If the interface granularity is reasonably defined, the system stability can be guaranteed; However, if the definition is too small, it will result in too many interfaces and complicate the design. If the definition is too large, flexibility is reduced and customization services cannot be provided, bringing unpredictable risks to the overall project.

(4) The use of multiple specialized interfaces can reflect the hierarchy of the object, because you can achieve the definition of the total interface through the inheritance of the interface.

(5) It can reduce code redundancy in project engineering. A large interface that is too large often places a lot of unnecessary methods inside it, forcing redundant code to be designed when implementing the interface.

4. Implementation method of interface isolation principle

When applying the interface isolation principle, it should be measured according to the following rules.

(1) The Interface should be as small as possible and Fat Interface should not appear; However, there are limits. First of all, the principle of single responsibility cannot be violated (one interface cannot correspond to half of the responsibility).

(2) The interface should be highly cohesive and public methods should be disclosed as little as possible in the interface. Interfaces are external commitments, and fewer commitments are better for system development.

(3) Custom services only provide methods that visitors need. For example, IComplexSearcher interfaces are provided for administrators and ISimpleSearcher interfaces are provided for public networks.

(4) The design of the interface is to understand the environment to a certain extent and refuse to follow blindly. Each project or product has selected environmental factors. Different environments require different standards for interface separation, requiring in-depth understanding of business logic.

5. Suggestions on interface isolation principles

(1) An interface serves only one submodule or business logic;

(2) Compress the public method in the interface through business logic;

(3) Try to modify the interface that has been polluted; If the risk of change is high, adaptor mode transformation is adopted.

(4) Refuse to follow blindly

6. Case analysis

The following uses student score management as an example to illustrate the interface isolation principle:

Analysis: The student score management program generally includes query scores, add scores, delete scores, modify scores, calculate total scores, calculate average scores, print scores information and other functions, usually what we do?

(1) The original design

The way we usually design interfaces is as follows:

public interface IStudentScore {
    // query results
    public void queryScore(a);

    // Modify the score
    public void updateScore(a);

    // Add score
    public void saveScore(a);

    // Delete the result
    public void delete(a);

    // Calculate the total score
    public double sum(a);

    // Calculate the average score
    public double avg(a);

    // Print the transcript
    public void printScore(a);

}
Copy the code

We will put all the functions in one interface. What kind of problems does that create?

First, the interface has many methods that make it difficult to extend. For example, students can only view their scores and print their transcripts, but not add, delete or modify them. The teacher has all the privileges.

Query transcript:

package com.lxl.www.designPatterns.sixPrinciple.interfaceSegregationPrinciple.score;

public class QueryScore implements IStudentScore{
    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void queryScore(a) {
        // query results
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void updateScore(a) {
         // No permissions
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void saveScore(a) {
        // No permissions
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void delete(a) {
        // No permissions
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public double sum(a) {
        // No permissions
        return 0;
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public double avg(a) {
        // No permissions
        return 0;
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void printScore(a) {
        // Print the transcript
    }
}</a>
Copy the code

Operating transcript:

package com.lxl.www.designPatterns.sixPrinciple.interfaceSegregationPrinciple.score;

public class Operate implements IStudentScore{
    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void queryScore(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void updateScore(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void saveScore(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void delete(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public double sum(a) {
        return 0;
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public double avg(a) {
        return 0;
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void printScore(a) {

    }
}
</a>
Copy the code

We will only use two methods to query the report card, but we will have to rewrite all of them because we implemented the interface.

If you add a request at this time – send it to the parent, only the teacher has this permission, not the student. However, after adding an abstract method to the interface, all implementation classes have to override that method. This violates the open close principle.

(2) Design using interface isolation principle

The UML diagram of the interface designed using the interface isolation principle is as follows:

public interface IQueryScore {
    // query results
    public void queryScore(a);

    // Print the transcript
    public void printScore(a);
}

public interface IOperateScore {

    // Modify the score
    public void updateScore(a);

    // Add score
    public void saveScore(a);

    // Delete the result
    public void delete(a);

    // Calculate the total score
    public double sum(a);

    // Calculate the average score
    public double avg(a);

}

public class StudentOperate implements IQueryScore{
    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void queryScore(a) {
        // query results
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void printScore(a) {
        // Print the transcript}}public class TeacherOperate implements IQueryScore.IOperateScore{
    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void queryScore(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void updateScore(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void saveScore(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void delete(a) {

    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public double sum(a) {
        return 0;
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public double avg(a) {
        return 0;
    }

    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void printScore(a) {

    }
}
</a>
Copy the code

We split one of the original interfaces. It is divided into query interface and operation interface. In this way, the student does not need to rewrite the interface that is not relevant to him.

It doesn’t make sense to put all of these functions into one interface, but the correct way is to put them into three modules: the input module, the statistics module, and the print module.

5. Demeter’s Rule

1. What is Demeter’s Law

The Law of Demeter is also called the least knowledge principle, which states that one object should know as little as possible about other objects. Don’t talk to strangers. Short for LoD.

The purpose of Demeter’s rule is to reduce coupling between classes. Because each class minimizes its dependence on the others, it is easy to make the functional modules of the system functionally independent, with no (or few) dependencies on each other.

Demeter’s rule does not want direct connections between classes. If you really need to make a connection, you want to communicate it through its friend class. Therefore, one possible consequence of applying Demeter’s rule is the existence of a large number of mediation classes, which exist solely for the purpose of passing on the intercalling relationships between classes — which increases the complexity of the system to some extent.

2. Why follow Demeter’s Rule

There are well-known abstractions in object-oriented programming, such as encapsulation, cohesion, and coupling, that can theoretically be used to produce clean designs and good code. While these are very important concepts, they are not practical enough to be applied directly to a development environment; they are subjective and rely heavily on the experience and knowledge of the user.

The same is true for other concepts, such as the principle of single liability, the open-closed principle, etc. What makes Demeter’s law unique is its concise and precise definition, which allows it to be applied directly when writing code, almost automatically applying appropriate encapsulation, low cohesion, and loose coupling.

3. The broad and narrow sense of Demeter’s rule

3.1 Demeter’s rule in a narrow sense

If two classes do not have to communicate directly with each other, then the two classes should not interact directly. If one of these classes needs to call a method of another class, the call can be forwarded by a third party.

To determine the “friend” conditions in the circle of friends:

(1) The current object itself (this)

(2) An object passed as a parameter to the current object method. The method entry is an object that is friends with the current class

(3) The object directly referenced by the instance variable of the current object. Define a class whose properties refer to other objects whose instance is friends with the current instance

(4) If the instance variable of the current object is an aggregation, then the elements in the aggregation are also friends. If the property is an object, then the property and the elements in the object are friends

(5) The object created by the current object. Any object that meets one of the above conditions is a “friend” of the current object; Otherwise, it’s a stranger.

Disadvantages of the narrow Demeter’s rule:

Make a lot of small methods in the system that just pass indirect calls and have nothing to do with the business logic of the system.

Following Demeter’s rule between classes would be a local design simplification of a system, since each part would not have a direct connection to distant objects. However, this will also reduce the communication efficiency between different modules of the system, and make it difficult to coordinate between different modules of the system.

3.2 The embodiment of generalized Demeter’s law in the design of classes

(1) Give priority to making a class immutable.

(2) Minimize access to a class.

(3) Use Serializable with caution.

(4) Try to reduce the access rights of members.

4. Application of Demeter’s law in design pattern

Design pattern Facade and Mediator are both applications of Demeter’s law.

So let’s take the example of renting a house, and look at Demeter’s rule.

Usually, customers want to find a house to live in, we directly build a house class, build a customer class, customers can find a house.

public interface IHouse {
    / / house
    public void Housing(a);
}

public class House implements IHouse{

    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void Housing(a) {
        System.out.println("Live in a house"); }}public class Customer {
    public String name;

    public void findHourse(IHouse house) {
        house.Housing();
    }
}</a>
Copy the code

The client is looking for a house to live in, the logic is very simple, this is OK. It violates Demeter’s law, but it makes sense.

But usually when we look for a house, we don’t find it all at once. We have to find a lot of houses, so it’s a lot of trouble. The agent has a lot of house resources, and the landlord gives the house to the agent, without caring who the tenant is. The tenant leaves the job of finding a house to the landlord, and he doesn’t have to care who the landlord is. Moreover, both the tenant and the landlord are very convenient.

/** * house */
public interface IHouse {
    / / house
    public void Housing(a);
}

public class House implements IHouse{

    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void Housing(a) {
        System.out.println("Live in a house"); }}public interface ICustomer {

    void findHourse(IHouse house) ;
}

public class Customer implements ICustomer {

    public void findHourse(IHouse house) { house.Housing(); }}/** ** intermediate */
public class Intermediary {
    / / looking for a house
    public IHouse findHouse(ICustomer customer){
        // Help tenants find apartments
        return null;
    }
}</a>
Copy the code

The house, the client is independent of each other, no reference to each other. The relationship between them is established through the intermediary, that is, the client rents the house from the intermediary, the landlord gives the house to the tenant, and the intermediary finally gives the good house to the client. Clients and landlords are segregated from each other, in accordance with Demeter’s law.

5. Realization of Demeter’s rule

So how in practice should one object know the least about the other? If we think of an object as a person, then two things are enough to achieve “a person should know the least about another person” :

(1) Only talk to direct friends

Another English interpretation of Demeter’s rule is: Talk only to your immediate friends.

What is a friend?

Every object is bound to be coupled to other objects, and the coupling between the two objects becomes a friend. So what is a direct friend? Classes that appear in input and output parameters of member variables and methods are immediate friends. Demeter’s Law requires that you correspond only with direct friends.

Note:

Classes that only appear inside the method body are not direct friends, and if a class talks to a friend that is not direct, it violates Demeter’s law.

Let’s take an example of what a friend is and what a direct friend is. A simple example: The teacher asked the monitor to count the number of the class. There are three classes in this example: Teacher, monitor GroupLeader, and Student.

public interface ITeacher {
    void command(IGroupLeader groupLeader);
}

public class Teacher implements ITeacher{
    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void command(IGroupLeader groupLeader) {
        // The whole class
        List<Student> allStudent = new ArrayList<>();
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());
        // The monitor counted the number of peoplegroupLeader.count(allStudent); }} ** * Class monitor */public interface IGroupLeader {

    // The monitor counted the number of people
    void count(List<Student> students);
}

/**
 * 班长类
 */
public class GroupLeader implements IGroupLeader{
    /** * The monitor counts the number *@param students
     */
    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void count(List<Student> students) {
        // The monitor counted the number of people
        System.out.println("The number of students in class is:"+ students.size()); }}/** ** class */
public interface IStudent {}/** ** class */
public class Student implements IStudent {}/** * client */
public class Client {
    public static void main(String[] args) {
        / / the teacher class
        ITeacher wangTeacher = new Teacher();

        / / monitor
        IGroupLeader zhangBanzhang = new GroupLeader();
        wangTeacher.command(zhangBanzhang);
    }
}

</a>
Copy the code

Running results:

The number of students in class is: 5

In this example, how many friends does our Teacher have? GroupLeader is an input parameter to Teacher’s command() method. The other is Student, because Student is used in the body of the Teacher’s command() method.

How many direct friends does Teacher have? By the definition of an immediate friend

Classes that appear in input and output parameters of member variables and methods are immediate friends

Only group Reader is Teacher’s direct friend.

Teacher creates an array of students in the command() method and communicates with the indirect friend Student, so the above example violates Demeter’s rule. A method is a behavior of a class, and a class is not allowed to know that its behavior is dependent on another class, which is a serious violation of Demeter’s law!

To make the above example comply with Demeter’s rule, we can modify it as follows:

public interface ITeacher {
    void command(IGroupLeader groupLeader);
}

public class Teacher implements ITeacher {
    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void command(IGroupLeader groupLeader) {
        // The monitor counted the number of peoplegroupLeader.count(); }}/**
 * 班长类
 */
public interface IGroupLeader {
    // The monitor counted the number of people
    void count(a);
}

/**
 * 班长类
 */
public class GroupLeader implements IGroupLeader {

    private List<Student> students;

    public GroupLeader(List<Student> students) {
        this.students = students;
    }

    /** * The monitor counts the number */
    </a><a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void count(a) {
        // The monitor counted the number of people
        System.out.println("The number of students in class is:"+ students.size()); }}/** ** class */
public interface IStudent {}/** ** class */
public class Student implements IStudent {}/** * client */
public class Client {
    public static void main(String[] args) {
        / / the teacher class
        ITeacher wangTeacher = new Teacher();

        List<Student> allStudent = new ArrayList(10);
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());
        allStudent.add(new Student());

        / / monitor
        IGroupLeader zhangBanzhang = new GroupLeader(allStudent);
        wangTeacher.command(zhangBanzhang);
    }
}
</a>
Copy the code

Running results:

The number of students in class is: 4

This modification allows each class to communicate only with its immediate friends, effectively reducing the coupling between classes.

(2) Know less about your friends

How to know less about your friends? If your friend is a big talker, he will talk in front of you and tell you all his stories even if you don’t ask him. So, to reduce the knowledge of a friend, please switch to a more introverted friend ~ in a class, is to minimize a class exposed methods.

Take a simple example of a class that exposes too many methods. The process of a person using a coffee machine to make coffee, there are only two classes in the example, one is a person, the other is a coffee machine.

The first is CoffeeMachine, which only needs three methods to make coffee: 1. Add coffee beans; 2. Add water; 3. Make coffee:

/** ** coffee machine abstract interface */
public interface ICoffeeMachine {

    // Add coffee beans
    void addCoffeeBean(a);

    / / add water
    void addWater(a);

    // Make coffee
    void makeCoffee(a);
}


/** ** coffee machine implementation class */
public class CoffeeMachine implements ICoffeeMachine{

    // Add coffee beans
    public void addCoffeeBean(a) {
        System.out.println("Put the coffee beans.");
    }

    / / add water
    public void addWater(a) {
        System.out.println("Water");
    }

    // Make coffee
    public void makeCoffee(a) {
        System.out.println("Make coffee."); }}/** * people, make coffee */
public interface IMan {
    /** ** make coffee */
    void makeCoffee(a);
}

/** * people make coffee */
public class Man implements IMan {
    private ICoffeeMachine coffeeMachine;

    public Man(ICoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    /** ** make coffee */
    public void makeCoffee(a) { coffeeMachine.addWater(); coffeeMachine.addCoffeeBean(); coffeeMachine.makeCoffee(); }}/** * client */
public class Client {
    public static void main(String[] args) {
        ICoffeeMachine coffeeMachine = new CoffeeMachine();

        IMan man = newMan(coffeeMachine); man.makeCoffee(); }}Copy the code

Running results:

Add water

Put the coffee beans

Making coffee

In this example, CoffeeMachine is Man’s direct friend, but the problem is that Man knows too much about CoffeeMachine, and actually he doesn’t care about the specific process of coffee making. So we can optimize as follows:

Make addCoffeeBean, addWater, and makeCoffee private: addCoffeeBean, addWater, makeCoffee

/** ** coffee machine abstract interface */
public interface ICoffeeMachine {

    // The coffee machine works
    void work(a);

}

/** ** coffee machine implementation class */
public class CoffeeMachine implements ICoffeeMachine {

    // Add coffee beans
    public void addCoffeeBean(a) {
        System.out.println("Put the coffee beans.");
    }

    / / add water
    public void addWater(a) {
        System.out.println("Water");
    }

    // Make coffee
    public void makeCoffee(a) {
        System.out.println("Make coffee.");
    }

    <a href="/profile/992988" data-card-uid="992988" class="js-nc-card" target="_blank" from-niu="default">@Override
    public void work(a) { addCoffeeBean(); addWater(); makeCoffee(); }}/** * people, make coffee */
public interface IMan {
    /** ** make coffee */
    void makeCoffee(a);
}

/** * people make coffee */
public class Man implements IMan {
    private ICoffeeMachine coffeeMachine;

    public Man(ICoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    /** ** make coffee */
    public void makeCoffee(a) { coffeeMachine.work(); }}/** * client */
public class Client {
    public static void main(String[] args) {
        ICoffeeMachine coffeeMachine = new CoffeeMachine();

        IMan man = new Man(coffeeMachine);
        man.makeCoffee();

    }
}
</a>
Copy the code

After such modification, Man’s understanding of CoffeeMachine is reduced by reducing CoffeeMachine’s external exposure, thus reducing the coupling between them.

In practice, Demeter’s law can be satisfied by communicating only with immediate friends and knowing less about them. So it’s not hard to imagine that the purpose of Demeter’s rule is to turn our classes into fat nerds. “Fat” is that a class may have few exposed methods, but its internal implementation may be very complex (this explanation is a stretch ~). Indoorsy because it only communicates with immediate friends. Fat otaku is a derogatory term in real life, but it has become a social problem in Japan. But in the program, a “fat nerd” class is an example of a good class.

6. Some matters needing attention

(1) In the division of classes, we should create classes with weak coupling. The weaker the coupling between classes, the more beneficial it is to achieve the goal of reusable.

(2) In class structure design, each class should reduce the access rights of members.

(3) In the design of classes, whenever possible, a class should be designed to be invariant.

(4) In reference to other classes, the reference of an object to another class should be minimized.

(5) Try to limit the effective range of local variables and reduce the access rights of classes.

Six, open and close principle

1. What is the open and close principle

The Open Closed Principle (OCP) is at the heart of all object-oriented principles. The goal of software design itself is to encapsulate change and reduce coupling, and the open and closed principle is the most direct embodiment of this goal. Other design principles often serve this purpose.

1.1 Definition of open and close principle

Software entities like classes,modules and functions should be open for extension but closed for modifications. A software entity, such as a class, module, function, etc., should be open to extension but closed to modification.

1.2 Meaning of the Open and close principle

(1) Open to extension means that existing code can be extended to adapt to new situations when there are new requirements or changes.

(2) Closed to changes, meaning that once the class is designed, it can do its work independently without making any changes to the existing code.

2. How to implement the open and close principle

“Requirements are always changing.” “No software in the world is constant.” Requirements are always changing, but for software designers, how to achieve flexible extension without modifying the original system. That’s what the open close principle does.

When we design systems, we cannot assume that once the requirements are identified, they will not change later. It’s not scientific or realistic. Since requirements are bound to change, how do we deal with them gracefully? How can the software be designed so that it is relatively easy to change, so that a change in requirements does not require the entire program to be started from scratch?

The e open-seal principle: The best way to design software that is easy to maintain and not prone to problems is to extend it more and change it less.

2.1 Dependency and abstraction

The core idea of open closure is to deal with abstract programming, not concrete programming, because abstraction is relatively stable.

Make classes dependent on fixed abstractions, so they are closed to change; And through the object-oriented inheritance and polymorphic mechanism, can realize the inheritance of the abstract body, by overwriting its method to change the inherent behavior, to achieve a new extension method, so for extension is open. This is the basic idea for implementing the open and closed principle.

2.2 How to Open and Close the Floor

If the current design does not meet the open and closed principle, you must refactor. The commonly used design patterns mainly include Template Method and Strategy design patterns. Encapsulating changes is an important way to achieve this principle, by encapsulating the parts that often change into a class.

2.3 Importance of the open and close principle

(1) The impact of the open and close principle on the test

The open and close principle keeps the original test code running, so we only need to test the extended code.

(2) Open and close principle can improve reusability

In object-oriented design, all logic is composed from atomic logic, rather than implementing a single business logic in a single class. Only then can code be reused, and the smaller the granularity, the more likely it is to be reused.

(3) Open and close principle can improve maintainability

Requirements for object-oriented development.

3. How to use the open and close principle

(1) Abstract constraints

(1) Restrict the extension through the interface or abstract class, restrict the extension boundary, do not allow the public method that does not exist in the interface or abstract class;

② Parameter types and reference objects should use interfaces or abstract classes rather than implementation classes;

③ The abstraction layer should be kept as stable as possible, and no modification is allowed once it is determined.

(2) Metadata controls module behavior

Metadata is the data used to describe the environment and data. In plain English, it is called configuration parameters, which can be obtained from files or databases.

The Spring container is a typical example of metadata Control module behavior, culminating in Inversion of Control.

(3) Formulate the project charter

In a team, it is important to have a project charter, because the charter specifies conventions that all personnel must follow, and conventions are superior to configurations for the project.

(4) Packaging changes

The encapsulation of change has two implications:

① Encapsulate the same changes into an interface or abstract class;

(2) Encapsulate different changes into different interfaces or abstract classes. There should not be two different changes in the same interface or abstract class.

4. Case analysis

Case 1: Draw shapes

Requirements: there are circles and ellipses, draw the corresponding shapes according to the requirements.

public class GraphicEditor { 

    public void draw(Shape shape) { 
        if (shape.m_type == 1) { 
            drawRectangle(); 
        } else if(shape.m_type == 2) { drawCircle(); }}public void drawRectangle(a) { 
        System.out.println("Draw a rectangle."); 
    } 

    public void drawCircle(a) { 
        System.out.println("Draw circles"); 
    } 

    class Shape { 
        int m_type; 
    } 

    class Rectangle extends Shape { 
        Rectangle() { 
            super.m_type=1; }}class Circle extends Shape { 
        Circle() { 
            super.m_type=2; }}}Copy the code

Let’s see, this code, at first glance, looks good, but think about it, what if I add a shape? Like adding triangles.

① To add a triangle class, inherit Shape

② To add a method to draw a triangle, drawTrriage()

③ Add a processing scheme of type type=3 to draw method

This violates the open closed principle – for extension development, closed for modification. One e type was added and three code changes were made.

Let’s look at the right design:

public class GraphicEditor1 { 

    public void draw(Shape shape) { 
        shape.draw(); 
    } 



    interface Shape { 
        void draw(a); 
    } 

    class Rectangle implements Shape { 

        @Override 
        public void draw(a) { 
            System.out.println("Draw a rectangle."); }}class Circle implements Shape { 

        @Override 
        public void draw(a) { 
            System.out.println("Draw circles"); }}}Copy the code

The various types of shapes regulate their own behavior, and graphicEditor.draw () is only responsible for drawing. When adding a type triangle. Only need to:

① Add a triangle class to implement Shape interface

② Call draw and draw it.

The entire process is to extend, not modify the original class, the design is open and closed principle.

Case 2:

For example, now there is a banking service, saving money, withdrawing money and transferring money. What do we initially think?

  • First, there is a banking class, which handles the banking business
  • What does a bank do? Depositing money, withdrawing money, transferring money, these are all operations performed by banks
  • So the outside says I’m going to deposit, I’m going to withdraw, I’m going to transfer, telling us by a type

The code is generated

package com.lxl. www.designPatterns.sixPrinciple.openclosePrinciple.bank; 

/** * banking */ 
public class BankBusiness { 

    public void operate(int type) { 
        if (type == 1) { 
            save(); 
        } else if(type == 2) { 
            take(); 
        } else if(type == 3) { transfer(); }}public void save(a){ 
        System.out.println("Save"); 
    } 

    public void take(a){ 
        System.out.println("Withdraw money"); 
    } 

    public void transfer(a) { 
        System.out.println("Transfer"); }}Copy the code

At first glance, the requirements have been met. But now there is a new need for banks to add functions-wealth management. Financial management is a kind of banking business, naturally is a new method.

Then add a type to the operate() method. This is bad design, adding new functionality but changing the original code.

We designed it as an interface abstraction. The code:

public interface Business { 
    public void operate(a); 
} 

public class Save implements Business{ 
    @Override 
    public void operate(a) { 
        System.out.println("Savings service"); }}public class Take implements Business { 
    @Override 
    public void operate(a) { 
        System.out.println("Withdrawal service"); }}public class Transfer implements Business { 
    @Override 
    public void operate(a) { 
        System.out.println("Transfer service"); }}/** * banking */ 
public class BankBusinesses { 
    /** * Handle banking business *@param business 
     */ 
    public void operate(Business business) { 
        System.out.println("Dealing with banking."); business.operate(); }}Copy the code

Through the form of interface abstraction to facilitate expansion, to add new financial functions. Only need to add a financial class, other business code does not need to change.

In fact, in the daily work, often encountered this situation. Because we usually write more business logic, and business is like a ledger, today and tomorrow, bit by bit increase. So, as the business grew to three, we had to think about how to write something that would be easy to scale.

5, summary

(1) Following the open and closed principle can improve software scalability and maintainability.

(2) Most of the design patterns and design principles are in the implementation of the open and closed principle.

Two | write at the bottom

All right! This article will come to an end here. Sorting is not easy, if this article is helpful to you, just click your little finger, like, share, bookmark three links. Thank you for your support!

If you want to continue to see the interview questions I compiled and updated materials, click follow. Check for updates as soon as possible.

At this point, the number of words has exceeded 40,000. There will inevitably be errors and deficiencies in the article. If you find any problems in this article, please correct them. Thank you very much.