It's really dry! This 40,000-character super-long dry goods thoroughly analyzes the "six principles of design patterns"! It is recommended to read repeatedly.

It's really dry! This 40,000-character super-long dry goods thoroughly analyzes the "six principles of design patterns"! It is recommended to read repeatedly.

Today I am finishing and updating an article on the six principles of design patterns, mainly about learning paths. I will look for opportunities to update interview articles in the future. So, pay attention, don t get lost, you can see my updates first!

One | Super Dry Goods

1. Single responsibility principle

1. Definition

1. let's take a look at the definition of a single responsibility.

The single responsibility principle, the full name is Single Responsibility Principle, or SRP for short.

A class should have only one reason to change.

As far as a class is concerned, there should be only one reason for its change. There should be only one responsibility.

If a class has more than one responsibility, these responsibilities are coupled together. A change in one responsibility may weaken or inhibit the ability of this class to perform other responsibilities. This can lead to fragile designs.

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

The weather is getting hotter and hotter, don't forget to study after you rest! Combination of work and rest can reap the greatest efficiency. I have compiled a lot of detailed Java learning materials, face-to-face materials of a number of Internet companies and a number of big guys, and more interview skills. Friends in need can click to enter and get it . Code: Nuggets.

2. Why should we observe the single responsibility principle

Usually, we must know why we are doing something before we do it. We also have the confidence to do it, so why do we use the single responsibility principle?

(1) Improve the maintainability and readability of classes

The responsibilities of a class are reduced, the complexity is reduced, the code is less, the readability is better, and the maintainability is naturally higher.

(2) Improve the maintainability of the system

The system is composed of classes, and the maintainability of each class is high. Relatively speaking, the maintainability of the entire system is high. Of course, the premise is that there is no problem with the system architecture.

(3) Reduce the risk of change

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

If there may be multiple things that change in a class, this design will bring risks. We try to ensure that only one can change, and the other changes are placed in other classes. The advantage is to improve cohesion and reduce Coupling .

3. The scope of application of the single responsibility principle

The scope of application of the single responsibility principle includes interfaces, methods, and classes. According to everyone's statement, interfaces and methods must guarantee a single responsibility, and classes do not have to guarantee, as long as they conform to the business.

3.1 Application of the single responsibility principle (methodological level)

Now there is a scenario where the user's username and password need to be modified. For this function, we can have multiple implementations.

The first:

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

The second type:

public interface UserOperate { void updateUserName (UserInfo userInfo) ; void updateUserPassword (UserInfo userInfo) ; } public class UserOperateImpl implements UserOperate { href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "4"> @Override public void updateUserName (UserInfo userInfo) { //modify user name logic } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "5"> @ Override public void updateUserPassword (UserInfo userInfo) { //modify password logic } }</a> Copy code

Let's take a look at the difference between these two implementations:

The first implementation is to distinguish based on the type of operation, and different types execute different logic. The two things of modifying the user name and modifying the password are coupled together. If the client transmits the wrong type during operation, an error will occur .

The second implementation is the one we recommend. Modifying the user name and modifying the password are logically separate. Each performs its own responsibilities without interfering with each other. The functions are clear and clear.

It can be seen that the second design conforms to the single responsibility principle. This is to implement the single responsibility principle at the method level.

3.2 Application of the single responsibility principle (interface level)

Let's assume a scenario where everyone is doing housework together, Zhang San sweeps the floor, and Li Si buys vegetables. Li Si has to cook after buying vegetables. How can this logic be realized?

method one:

/** * Do housework */ public interface HouseWork { // void sweepFloor () ; //shopping void shopping () ; } public class Zhangsan implements HouseWork { href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "6"> @Override public void sweepFloor () { //sweep the floor } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "7"> @ Override public void shopping () { } } public class Lisi implements HouseWork { <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "8"> @ Override public void sweepFloor () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "9"> @ Override public void shopping () { //shopping } } </a> Copy code

1. define an interface for doing housework, and define two methods for sweeping and buying food. Zhang San sweeps the floor and implements the sweeping interface. Li Si buys vegetables, and then implements the grocery shopping interface. Then Li Si returns to cook after buying food, so It is necessary to add a method cooking to the interface class. Zhang San and Li Si both rewrite this method, but only Li Si has a concrete implementation.

Such a design itself is unreasonable.

1. Zhang San only sweeps the floor, but he needs to rewrite the grocery shopping method. Li Si does not need to sweep the floor, but Li Si also needs to rewrite the sweeping method.

2. this does not conform to the principle of opening and closing. Adding one type of cooking requires modifying three types. In this way, when the logic is very complicated, it is easy to cause unexpected errors.

The above design does not conform to the single responsibility principle. Modifying one area affects other areas that do not need to be modified.

Method Two:

/** * Do housework */ public interface Hoursework { } public interface Shopping extends Hoursework { //shopping void shopping () ; } public interface SweepFloor extends Hoursework { //Sweep the floor void sweepFlooring () ; } 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 () { //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 () { //Li Si shopping } }</a> Copy code

The above housework is not defined as an interface, but separates sweeping and housework.

Zhang San sweeps the floor, then Zhang San implements the sweeping interface; Li Si shopping, Li Si implements the shopping interface; Later Li Si will add a function for cooking.

Then add a cooking interface, this time only Li Si needs to implement the cooking interface.

public interface Cooking extends Hoursework { void cooking () ; } public class Lisi implements Shopping , Cooking { href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "12"> @Override public void Shopping () { //Li Si shopping } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "13"> @ Override public void cooking () { //Li Si cooks } }</a> Copy code

As above, we see that Zhang San did not implement redundant interfaces, and neither did Li Si. And when new features were added, only Li Si was affected, but Zhang San was not affected.

This is in line with the single responsibility principle. A class only does one thing, and its modification will not bring other changes.

3.3 Application of the single responsibility principle (class level)

At the class level, there is no way to completely split it according to the original single responsibility. In other words, the responsibilities of a class can be large or small, and you don't want interfaces to be clearly split according to the principle of single responsibility, as long as it is logical and reasonable.

For example, we can register, log in, WeChat log in, register and log in on the homepage of the website. Our usual practice is

public interface UserOperate { void login (UserInfo userInfo) ; void register (UserInfo userInfo) ; void logout (UserInfo userInfo) ; } public class UserOperateImpl implements UserOperate { href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "14"> @Override public void Login (UserInfo userInfo) { //User login } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "15"> @ Override public void register (UserInfo userInfo) { //User registration } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "16"> @ Override public void logout (UserInfo userInfo) { //User logout } }</a> Copy code

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

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

Is it possible to write like the above? In fact, it is also possible, but there are many types. If there are many operation codes for login, registration, and logout, you can write it like this.

4. How to comply with the single responsibility principle

4.1 Reasonable responsibilities

Putting the same responsibilities together and decomposing different responsibilities into different interfaces and implementations is the easiest and most difficult principle to apply. The key is to identify the same type of responsibilities from the business and requirements.

Example: Human behavior analysis includes the analysis of life and work behaviors. Life behaviors include eating, running, sleeping and other behaviors. Work behaviors include commuting, meeting and other behaviors, as shown in the following figure:

Human behavior is divided into two interfaces: life behavior interface, work behavior interface, and two implementation classes. If you use one implementation class to assume the responsibilities of these two interfaces, it will lead to bloated code and difficult to maintain. If you add other behaviors in the future, such as learning behavior interfaces, there will be a risk of change (combination mode is also used here) .

4.2 Look at the implementation of simple code

The first step: define a behavior interface

/** * Human behavior * Human behavior includes two types: life behavior, work behavior */ public interface IBehavior { } Copy code

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

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

Life behavior interface:

public interface LivingBehavior extends IBehavior { /** eat*/ void eat () ; /** Running*/ void running () ; /** Sleep*/ void sleeping () ; } Copy code

Work behavior interface:

public interface WorkingBehavior extends IBehavior { /** Go to work*/void goToWork () ; /** Off work*/void goOffWork () ; /** Meeting*/ void meeting () ; } Copy code

Step 3: Define the implementation classes of the work behavior interface and the life behavior interface

Life behavior interface implementation class:

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

Work behavior interface implementation class:

public class WorkingBehaviorImpl implements WorkingBehavior { href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "23"> @Override public void goToWork () { System.out.println( "Work" ); } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "24"> @ Override public void goOffWork () { System.out.println( "off work" ); } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "" target= "_blank" from-niu= "default" data-card-index= "25"> @ Override public void meeting () { System.out.println( "meeting" ); } }</a> Copy code

The fourth step: behavior combination call

The behavior interface is defined, and then a behavior set will be defined. Different users have different behaviors. Some users only use life behaviors, and some users have both life behaviors and work behaviors. ewgni

We don't know what behaviors a specific user will have. Therefore, we usually use a collection to receive user behaviors. What behaviors the user has will add which behaviors to it.

(1) Behavior combination interface BehaviorComposer

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

(2) Behavior combination interface implementation class IBehaviorComposerImpl

public class IBehaviorComposerImpl implements BehaviorComposer { private List<IBehavior> behaviors = new ArrayList<>(); href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "26"> @Override public void the Add (IBehavior behavior) { System.out.println( "Add Behavior" ); behaviors.add(behavior); } public void doSomeThing () { behaviors.forEach(b->{ if (b instanceof LivingBehavior) { LivingBehavior li = (LivingBehavior)b; //deal with life behavior } else if (b instanceof WorkingBehavior) { WorkingBehavior wb = (WorkingBehavior) b; //handle work behavior } }); } }</a> Copy 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 with only life behavior, and Li Si is a working mother with both life behavior and work behavior.

public static void main (String[] args) { //Zhang San---full-time mother LivingBehavior zslivingBehavior = new LivingBehaviorImpl(); BehaviorComposer zsBehaviorComposer = new IBehaviorComposerImpl(); zsBehaviorComposer.add(zslivingBehavior); //Li Si- Working Mom LivingBehavior lsLivingBehavior = new LivingBehaviorImpl(); WorkingBehavior lsWorkingBehavior = new WorkingBehaviorImpl(); BehaviorComposer lsBehaviorComposer = new IBehaviorComposerImpl(); lsBehaviorComposer.add(lsLivingBehavior); lsBehaviorComposer.add(lsWorkingBehavior); } Copy code

The benefits of single responsibility can be seen.

5. The advantages and disadvantages of the single responsibility principle

  • The complexity of the class is reduced: the responsibilities of a class are clearly defined, and the complexity is naturally reduced

  • Improved readability: As complexity is reduced, readability naturally improves

  • Improved maintainability: Improved readability makes the code easier to maintain

  • Risk reduction caused by changes: Changes are indispensable. If the single responsibility of the interface is done well, an interface modification will only affect the corresponding implementation class, and has no effect on other interfaces and classes. This will affect the scalability, Maintainability is very helpful

2. the principle of Richter substitution

How many friends still don't know the principle of Chinese substitution? We have written code for many years, and we use inheritance and subclassing every day. But, don't know the principle of Chinese substitution? Come and have a look.

1. What is the principle of Richter substitution

1.1 Definition of Richter's Substitution Principle

The Li substitution principle is used to help us design the parent-child class in the inheritance relationship.

The Liskov Substitution principle (Liskov Substitution principle) is a special definition of subtypes. Why is it called the Liskov Substitution principle? Because this principle was first introduced in 1988 by a woman named Barbara from the Massachusetts Institute of Technology. Liskov).

The Richter's Substitution Principle mainly elaborates on some principles of inheritance, that is, when inheritance should be used and when should not be used, and the principles behind it. The Richter replacement was originally the basis of inheritance and reuse. It reflected the relationship between the base class and the subclasses, was a supplement to the principle of opening and closing, and a specification of the concrete steps to achieve abstraction.

The Richter 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, then objects of T can be replaced with objects 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 it. All references to the methods of its parent class can be transparently replaced with its subclass objects.

These two definitions actually have the same meaning, that is, wherever an object of the parent class appears in the application, we can replace it with an object of its subclass, and the logical behavior and correctness of the original program can be guaranteed.

1.2 There are at least two definitions of Richter substitution

(1) The Richter substitution principle is for inheritance. If inheritance is to achieve code reuse, that is, to share methods, then the shared parent class method should remain unchanged and cannot be redefined by subclasses. Subclasses can only extend functions by adding new methods. Both the parent class and the subclass can be instantiated, and the subclass inherits the same method as the parent class. Where the parent class calls the method, the subclass can also call the same inheritance. The resulting method is logically consistent with the parent class. At this time, when the parent class object is replaced with the subclass object, of course the logic is consistent and there is nothing wrong with each other.

(2) If the purpose of inheritance is for polymorphism, and the premise of polymorphism is that the subclass overrides and redefines the method of the parent class, in order to conform to the LSP, we should define the parent class as an abstract class and define abstract methods so that the child The class redefines these methods. When the parent class is an abstract class, the parent class cannot be instantiated, so there is no instantiable parent class object in the program. There is no possibility of logical inconsistency when the subclass replaces the superclass instance (there is no superclass instance at all).

The most common case that does not conform to the LSP is that the parent class and the child class are non-abstract classes that can be instantiated, and the method of the parent class is redefined by the child class. The implementation inheritance of this class will cause the parent class and the child class Strong coupling, that is, properties and methods that are not actually related are far-fetched together, which is not conducive to program expansion and maintenance.

2. The purpose of using the Richter substitution principle

The Richter replacement principle is adopted to reduce the shortcomings brought by inheritance, enhance the robustness of the program, and maintain good compatibility when the version is upgraded. Even if you add subcategories, the original subcategories can continue to run.

3. The relationship between Richter's substitution principle and inheritance polymorphism

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

public class Cache { public void set (String key, String value) { } } public class Redis extends Cache { href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "3"> @Override public void SET (String key, String value) { } } public class Memcache extends Cache { <a href= "/profile/992988" data-card-uid= "992988" </a> 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) { //Parent class objects can receive 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 code

Through the above example, we can see that Cache is the parent class, and Redis and Memcache are subclasses. They inherit from Cache. This is the idea of inheritance and polymorphism. And these two subclasses are also in line with the Li substitution principle so far, and can replace any position where the parent class appears, and the logical behavior of the original code remains unchanged and the correctness is not destroyed.

Looking at the last CacheTest class, we can use the cache of the parent class to receive any type of cache object, including the parent class and the child class.

But if we check the length of the set method in Redis

public class Redis extends Cache { href= "/profile/992988" data-card-uid= "992988" class <a = "" 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 the requirements" ); throw new IllegalArgumentException (The length of the key does not meet the requirements); } } } public class CacheTest { public static void main (String[] args) { //Parent class objects can receive subclass objects Cache cache = new Cache(); cache.set( "key123" , "key123" ); cache = new Redis(); cache.set( "key123" , "key123" ); } }</a> Copy code

As in the above situation, if we use the parent class object to replace the subclass object, then an exception will be thrown. The logic behavior of the program has changed, although the code after the transformation can still replace the parent class by the subclass, but from In terms of design ideas, the design of Redis subclasses does not conform to the principle of Li substitution.

Inheritance and polymorphism are a kind of syntax provided by object-oriented language, which is the idea of code realization, while Li substitution is a kind of thought and a design principle, which is used to guide the design of subclasses in the inheritance relationship. The design of the subclass should ensure that when replacing the parent class, it does not change the logic of the original program and does not destroy the correctness of the original program.

4. The rules of Li substitution

The core of the Li-style substitution principle is the "convention", the convention between the parent class and the child class. The Richter substitution principle requires the subclass to comply with some behavioral conventions of the parent class when designing. The behavior agreement here includes: the function to be implemented by the function, the agreement on input, output, and exception, and even some special instructions in the comments.

4.1 The subclass method cannot violate the convention of the parent class method for input and output exceptions

(1) Preconditions cannot be strengthened

The precondition is that the input parameter cannot be enhanced. Just like the Cache example above, the Redis subclass has enhanced the requirement of the input parameter Key. At this time, replacing the parent object with the subclass object at the call site may cause an exception.

In other words, the sub-category checks the input data more strictly than the parent category, and the design of the sub-category violates the principle of Li substitution.

(2) Post-conditions cannot be weakened

The post-condition is output. Assuming that our parent method agrees that the output parameter is greater than 0, the program that calls the parent method verifies that the output parameter is greater than 0 according to the convention. When the subclass is implemented, it outputs a value less than or equal to 0. At this time, the involvement of sub-categories violates the Richter substitution principle.

(3) The agreement on exceptions cannot be violated

In the parent class, a certain function stipulates that only ArgumentNullException will be thrown. In the design and implementation of the subclass, only ArgumentNullException is allowed to be thrown. Throwing of any other exceptions will cause the subclass to violate the principle of substitution.

4.2 The subclass method cannot violate the function defined by the parent class method

public class Product { private BigDecimal amount; private Calendar createTime; public BigDecimal getAmount () { return amount; } public void setAmount (BigDecimal amount) { this .amount = amount; } public Calendar getCreateTime () { return createTime; } public void setCreateTime (Calendar createTime) { this .createTime = createTime; } } public class ProductSort extends Sort < Product > { public void sortByAmount (List<Product> list) { //Sort according to time list.sort((h1, h2)->h1.getCreateTime().compareTo(h2.getCreateTime())); } } Copy code

The sortByAmount() sorting function provided in the parent class is sorted according to the amount from small to large, and the sub-class rewrites the sortByAmount() sorting function, but it is sorted according to the creation date. The design of that sub-category violates the principle of Li substitution.

In fact, there is actually a little trick to verify whether the subclass design conforms to the Richter substitution principle, that is, you can use the single test of the parent class to run the code of the subclass, if it can t run normally, then you have to consider yourself Is the design reasonable?

4.3 The subclass must fully implement the abstract method of the parent class

If the subclass you design cannot fully implement the abstract methods of the parent class, then your design does not satisfy the principle of substitution.

//define abstract class gun public abstract class AbstractGun { //shoot public abstract void shoot () ; //kill public abstract void kill () ; } Copy code

For example, we define an abstract gun class that can shoot and kill people. Whether it is a rifle or a weapon can shoot and kill, we can define a subclass to inherit the parent class.

//Define ***, rifle, machine gun public class Handgun extends AbstractGun { public void shoot () { //***Shooting } public void kill () { //***kill } } public class Rifle extends AbstractGun { public void shoot () { //Rifle shooting } public void kill () { //Kill with a rifle } } Copy code

But if we add a toy gun to this inheritance system, there will be a problem, because toy guns can only shoot and cannot kill. But many people often write code like this.

public class ToyGun extends AbstractGun { public void shoot () { //Toy gun shooting } public void the kill () { //because the toy gun can not kill, they returned empty, or just throw out an exception throw new new Exception ( "I was a toy gun, no surprise surprise, meaning no accident, not stab stimulus?" ); } } Copy code

At this time, if we replace the place where the parent object is used with the child object, obviously there will be a problem (the soldier went to the battlefield and found that he was holding a toy).

This situation not only does not satisfy the Richter substitution principle, but also does not satisfy the interface isolation principle. For this scenario, it can be solved by **interface isolation + delegation**.

5. The role of the Richter substitution principle

(1) The Richter substitution principle is one of the important ways to realize the opening and closing principle.

(2) It overcomes the shortcomings of poor reusability caused by rewriting the parent class in inheritance.

(3) It is the guarantee of the correctness of the action. That is, the extension of the class will not introduce new errors to the existing system, reducing the possibility of code errors.

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

Try not to inherit from instantiable parent classes, but use inheritance based on abstract classes and interfaces.

6. The realization method of the Richter substitution principle

In general, the Richter substitution principle is: a subclass can extend the functions of the parent class, but cannot change the original functions of the parent class. That is to say: when the subclass inherits the parent class, in addition to adding new methods to complete the new functions, try not to override the methods of the parent class.

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

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

(2) You can add your own unique methods in the subclass

(3) When the method of the subclass overloads the method of the parent class, the precondition of the method (that is, the input parameters of the method) is more relaxed than the method of the parent class

(4) When the method of the subclass implements the method of the parent class (override/overload or implement the abstract method), the post-condition of the method (that is, the output/return value of the method) is more stringent than the method of the parent class or equal

Completing new functions by rewriting the parent class is simple to write, but the reusability of the entire inheritance system will be poor, especially when polymorphism is used frequently, the probability of program operation errors will be very high.

If the program violates the Richter substitution principle, the objects of the inherited class will run into errors in the place where the base class appears. At this time, the correction method is: cancel the original inheritance relationship and redesign the relationship between them.

Regarding the example of the Richter substitution principle, the most famous one is "a square is not a rectangle". Of course, there are many similar examples in life. For example, penguins, ostriches, and kiwis are classified from a biological point of view, and they belong to birds; but from the perspective of the inheritance relationship of classes, they cannot inherit "birds" and can fly. Functions, so they cannot be defined as subcategories of "birds". Similarly, because "balloon fish" cannot swim, it cannot be defined as a subtype of "fish"; "toy cannon" cannot blow up enemies, so it cannot be defined as a subtype of "cannon".

7. Case analysis

Case 1: Subtract two numbers

When using inheritance, follow the Richter substitution principle. When class B inherits class A, in addition to adding a new method to complete the new function P2, try not to override the method of the parent class A, and try not to override the method of the parent class A.

Inheritance contains such a meaning: all methods that have been implemented in the parent class (as opposed to abstract methods) are actually setting a series of specifications and contracts, although it does not force all subclasses to comply with these Contract, but if the subclasses arbitrarily modify these non-abstract methods, it will cause damage to the entire inheritance system. The Richter substitution principle expresses this meaning.

As one of the three characteristics of object-oriented, inheritance brings great convenience to programming, but also brings disadvantages. For example, the use of inheritance will bring intrusiveness to the program, reduce the portability of the program, and increase the coupling between objects. If a class is inherited by other classes, when this class needs to be modified, all sub-classes must be considered. Class, and after the parent class is modified, all functions related to the subclass may fail. Copy code
class A { public int func1 ( int a, int b) { return ab; } } 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 code

operation result:

100-50=50

100-80=20

Later, we need to add a new function: to complete the addition of two numbers, and then sum with 100, which is the responsibility of class B. That is, class B needs to complete two functions:

1. Subtract two numbers.

2. Add two numbers, and then add 100.

Since class A has implemented the first function, after class B inherits class A, it only needs to complete the second function. 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 code

After class B is completed, the operation result:

100-50=150

100-80=180

100+20+100=220

We found an error in the subtraction function that was working normally. The reason is that class B inadvertently rewrites the method of the parent class when naming the method, causing all the code that runs the subtraction function to call the rewritten method of class B, causing an error in the originally functioning function.

In this example, after referencing the function completed by base class A and replacing it with subclass B, an exception occurred. In actual programming, we often complete new functions by rewriting the parent class. Although this is simple to write, the reusability of the entire inheritance system will be relatively poor, especially when polymorphism is used frequently. The chance of running errors is very high.

If you have to rewrite the method of the parent class, the more general approach is: the original parent class and subclass inherit a more popular base class, remove the original inheritance relationship, and use dependency, aggregation, composition and other relationships instead.

Case 2: "Kiwi is not a bird"

Demand analysis: Birds usually fly, such as swallows 120 kilometers per hour, but New Zealand kiwis cannot fly due to wing degradation. If you want to design an example, calculate the cost of these two birds flying 300 kilometers time. Obviously, using the swallow to test this code, the result is correct, and the required time can be calculated; but using the kiwi bird to test, the result will be "division by zero exception" or "infinity", which obviously does not meet expectations. Its class diagram As 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) { return distance/flySpeed; } } /** * Swallow */ public class Swallow extends Bird { } /** * Kiwi */ public class Kiwi extends Bird { href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "6"> @Override public void setFlySpeed ( double flySpeed) { flySpeed = 0 ; } } /** * Test flight takes 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 flying 300 kilometers:" ); try { System.out.println( "Swallow cost" + bird1.getFlyTime( 300 ) + "hour." ); System.out.println( "Several dimension cost" + bird2.getFlyTime( 300 ) + "hour." ); } catch (Exception err) { System.out.println( "An error has occurred!" ); } } } </a> Copy code

operation result:

If flying 300 kilometers:

The swallow takes 2.5 hours.

Kiwi takes Infinity hours.

The reason for the program running error is: Kiwi Bird has rewritten the bird's setSpeed(double speed) method, which violates the Richter substitution principle. The correct approach is to cancel the original inheritance relationship of the kiwi, and define the more general parent class of the bird and the kiwi, such as animals, which have the ability to run. Although the flying speed of the kiwi is 0, the running speed is not 0, and the time it takes to run 300 kilometers can be calculated. The class diagram is shown in Figure 2.

Source code:

/** * Animals */ 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) { return distance/flySpeed; } } /** * Swallow */ public class Swallow extends Bird { } /** * Kiwi */ public class Kiwi extends Animal { href= "/profile/992988" data-card-uid= "992988" class <a = "" target= "_blank" from-niu= "default" data-card-index= "7"> @Override public void setRunSpeed ( double runSpeed) { super .setRunSpeed(runSpeed); } } /** * Test flight takes 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 flying 300 kilometers:" ); try { System.out.println( "Swallow cost" + bird1.getFlyTime( 300 ) + "hour." ); System.out.println( "Kiwi bird cost" + bird2.getRunTime( 300 ) + "hour." ); } catch (Exception err) { System.out.println( "An error has occurred!" ); } } } </a> Copy code

operation result:

If flying 300 kilometers:

The swallow takes 2.5 hours.

Kiwi spends 2.727272727272727 hours.

summary

Object-oriented programming provides inheritance and polymorphism so that we can achieve code reusability and extensibility, but inheritance is not without its shortcomings, because inheritance itself is intrusive, and if used improperly, it will Greatly increase the coupling of the code, reduce the flexibility of the code, and increase our maintenance cost. However, in the actual use process, there will often be abuse of inheritance, and the principle of substitution can help us in the inheritance relationship. Carry out parent-child design.

3. Dependence inversion principle

1. What is the principle of dependency inversion

1.1 Concept

Dependence Inversion Principle (DIP), its meaning:

(1) High-level modules should not rely on low-level modules, and both should rely on their abstraction

(2) Abstraction should not depend on details, and details should depend on abstraction

(3) Programming for the interface, not for the realization

1.2 What is dependency

We understand the dependency here as the dependency in the UML relationship. Simply put, A use B, then A depends on B. Please see the following example for details.

From the above figure, we can find that the method a() in class A uses class B. In fact, this is the dependency relationship, and A depends on B. It should be noted that: It is not to say that B is declared in A, which is called dependency. If the method is referenced but there is no real call, then it is called a zero coupling relationship. As shown below:

1.3 Types of dependent relationships

(1) Zero coupling relationship

(2) Direct coupling relationship: Specific coupling occurs between two concrete classes (instantiable), caused by a direct reference of one class to another.

(3) Abstract coupling relationship: The abstract coupling relationship occurs between a concrete class and an abstract class (or java interface), so that there is maximum flexibility between two classes that must be broken.

The principle of dependency inversion is to program for the interface, not for the implementation. That is to say, you should use interfaces or abstract classes for variable type declaration, parameter type declaration, method return type description, and data type conversion.

2. The case of relying on inversion

2.1 Preliminary design plan

public class Benz { public void run () { System.out.println( "Benz 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 San" ); driver.driver(benz); } } Copy code

There is a driver Zhang San who can drive a Mercedes-Benz car, so at first we thought that there would be a driver class and a Mercedes-Benz car class. With the development of the business, we found that the driver Zhang San can still drive a BMW.

Therefore, we define a BM class:

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

At this time, if Zhang San wants to drive a BMW, he must register BMW under 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 San" ); driver.driver(benz); driver.driver(bm); } } Copy code

It seems that this is fine, but what's the problem?

(1) If Zhang San wants to drive a Volkswagen one day, he will need to add a Volkswagen vehicle, and he will have to carry the driver's name.

(2) Not all people have to drive Mercedes-Benz, BMW, or Volkswagen.

This is the problem of implementation-oriented programming, and then we will consider interface-oriented programming.

2.2 Improved scheme

public interface ICar { public void run () ; } public class Benz implements ICar { public void run () { System.out.println( "Benz is running!" ); } } public class BM implements ICar { public void run () { System.out.println( "BMW is running!" ); } } public interface IDriver { public void driver (ICar car) ; } public class Driver implements IDriver { href= "/profile/992988" data-card-uid= "992988" class <a = "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 code

The modified code extracts an IDriver interface and an ICar interface, which are interface-oriented programming. The implementation class of IDriver can drive any type of car, so the incoming parameter is also an interface ICar. Any type of car can be implemented by implementing ICar The interface is registered as a new car type. When the client calls it, just pass in the corresponding car.

3. The way of dependence

3.1 There are three main ways of dependency injection

(1) Construction injection, injecting dependencies during construction.

(2) Setter method injection

(3) Injection into the interface method (this method is used in the car example)

3.2 The embodiment of the principle of dependency inversion in the design pattern

(1) Simple factory design pattern, using injection in interface method

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

Simple factory design mode; Strategic design mode;

4. the principle of interface isolation

1. Definition

Clients should not be forced to depend upon interfaces that they don't use. The client only depends on the interfaces it needs; it provides whatever interfaces it needs, and eliminates unnecessary interfaces.

The dependency of one class to another one should depend on the smallest possible interface.

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

2. Interface isolation principle and single responsibility principle

From a functional point of view, the interface isolation principle and the single responsibility principle are both to improve the cohesion of classes and reduce the coupling between classes, reflecting the idea of encapsulation. But there are still differences between the two.

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

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

(3) The single responsibility principle is more biased toward business constraints: The interface isolation principle is more biased toward design architecture constraints. This should be easy to understand. Responsibilities are divided according to business functions, so the single principle is more business-oriented; and interface isolation is more for "high cohesion" and biases the design of the architecture.

3. Advantages of the interface isolation principle

The interface isolation principle is to restrict the interface and reduce the dependence of the class on the interface. Following the interface isolation principle has the following 5 advantages.

(1) Decomposing a bloated interface into multiple interfaces with small granularity can prevent the spread 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 definition of the interface granularity is reasonable, the stability of the system can be guaranteed; however, if the definition is too small, it will cause too many interfaces and complicate the design; if the definition is too large, the flexibility will be reduced and customization cannot be provided. Service brings unforeseen risks to the overall project.

(4) The use of multiple specialized interfaces can reflect the level of the object, because the definition of the overall interface can be realized through the inheritance of the interface.

(5) It can reduce code redundancy in project engineering. A large interface that is too large usually puts many unused methods in it. When implementing this interface, it is forced to design redundant codes.

4. Implementation method of interface isolation principle

In the specific application of the interface isolation principle, it should be measured according to the following rules.

(1) The interface should be as small as possible without Fat Interface; but to a limit, the principle of single responsibility should not be violated (one interface cannot correspond to half of the responsibility).

(2) The interface should be highly cohesive . Public methods should be published as little as possible in the interface. The interface is a promise to the outside world, and the less the promise, the more beneficial the development of the system.

(3) Customized services only provide the methods that visitors need. For example, provide the IComplexSearcher interface for the administrator and the ISimpleSearcher interface for the public network.

(4) The design of the interface is limited to understand the environment and refuse to follow blindly. Each project or product has selected environmental factors. Different environments have different standards for interface splitting. It is necessary to have a deep understanding of business logic.

5. Suggestions on the principle of interface isolation

(1) An interface only serves one sub-module or business logic;

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

(3) The interface that has been contaminated should be modified as much as possible; if the risk of the change is greater, the adapter mode conversion process should be adopted;

(4) Refuse to follow blindly

6. Case analysis

The following takes student achievement management as an example to illustrate the interface isolation principle:

Analysis: The student performance management program generally includes functions such as querying, adding, deleting, modifying, calculating the total score, calculating the average score, and printing the score information. What do we usually do?

(1) Initial design

Usually we design the interface as follows:

public interface IStudentScore { //Query score public void queryScore () ; //modify the score public void updateScore () ; //Add score public void saveScore () ; //delete the results public void delete () ; //Calculate the total score public double sum () ; //Calculate the average score public double avg () ; //Print the score report public void printScore () ; } Copy code

We will put all the functions in one interface. What kind of problems will this cause?

First of all, there are many interface methods, which are not conducive to expansion. For example, students only have the permission to view their scores and print the transcript, but not to add, delete, or modify; the teacher has all the permissions.

Query transcript:

package com.lxl.www.designPatterns.sixPrinciple.interfaceSegregationPrinciple.score; public class QueryScore implements IStudentScore { href= "/profile/992988" data-card-uid= "992988" class <a = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void queryScore () { //Check results } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void updateScore () { //No permission } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void saveScore () { //No permission } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void the Delete () { //No permission } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public Double SUM () { //No permission return 0 ; } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public Double AVG () { //No permission return 0 ; } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void printScore () { //Print transcript } }</a> Copy code

Operation transcript:

package com.lxl.www.designPatterns.sixPrinciple.interfaceSegregationPrinciple.score; public class Operate implements IStudentScore { href= "/profile/992988" data-card-uid= "992988" class <a = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void queryScore () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void updateScore () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void saveScore () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void the Delete () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public Double SUM () { return 0 ; } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public Double AVG () { return 0 ; } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void printScore () { } } </a> Copy code

You can see the problem. To query the transcript, we will only use two methods, but because of the implementation of the interface, we have to rewrite all the methods.

If you increase the demand at this time-send it to the parent, only the teacher has this permission, and the student does not have this permission. However, after adding an abstract method to the interface, all implementation classes must override this method. This violates the opening Closed principle.

(2) Design using the principle of interface isolation

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

public interface IQueryScore { //Query score public void queryScore () ; //Print the score report public void printScore () ; } public interface IOperateScore { //modify the score public void updateScore () ; //Add score public void saveScore () ; //delete the results public void delete () ; //Calculate the total score public double sum () ; //Calculate the average score public double avg () ; } public class StudentOperate implements IQueryScore { href= "/profile/992988" data-card-uid= "992988" class <a = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void queryScore () { //Check results } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void printScore () { //Print transcript } } public class TeacherOperate implements IQueryScore , IOperateScore { <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void queryScore () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void updateScore () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void saveScore () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void the Delete () { } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public Double SUM () { return 0 ; } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public Double AVG () { return 0 ; } <a href= "/profile/992988" data-card-uid= "992988" </a> class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void printScore () { } } </a> Copy code

We split the original interface into a query interface and an operation interface. In this way, the student side does not need to rewrite the interface that is not related to him.

It is obviously unreasonable to put all these functions in one interface. The correct way is to put them in three modules: input module, statistics module and printing module.

5. Dimit's law

1. What is Dimit's law

The Law of Demeter (Law of Demeter) is also called the principle of least knowledge, that is, an object should know as little as possible about other objects. Don't talk to strangers. English abbreviation is: LoD.

The purpose of Dimit's Law is to reduce the coupling between classes. Since each class minimizes its dependence on other classes, it is easy to make the functional modules of the system function independent, and there is no (or very little) dependency relationship between each other.

Dimit's Law does not want to establish a direct connection between classes. If there is a real need to establish a connection, I hope it can be conveyed through its friend class. Therefore, one of the possible consequences of applying Dimit s law is that there are a large number of intermediate classes in the system. The reason why these classes exist is to transfer the mutual calling relationship between the classes. This increases the system's capacity to a certain extent. the complexity.

2. Why should we obey Dimit's law?

There are some well-known abstract concepts in object-oriented programming, such as encapsulation, cohesion, and coupling, which can theoretically be used to generate clear designs and good code. Although these are very important concepts, they are not practical enough and cannot be directly used in the development environment. These concepts are more subjective and rely heavily on the user's experience and knowledge.

The situation is the same for other concepts, such as the single responsibility principle and the opening and closing principle. The uniqueness of Dimit's Law is its concise and accurate definition, which allows direct application when writing code, and almost automatically applies appropriate encapsulation, low cohesion and loose coupling.

3. The broad and narrow sense of Dimit's law

3.1 Dimit's law 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 the classes needs to call a method of another class, the call can be forwarded through a third party.

Conditions for determining "friends" in Moments:

(1) The current object itself (this)

(2) The object passed into the current object method in the form of a parameter. The method parameter is an object, this is the object and the current class are friends

(3) The object directly referenced by the instance variable of the current object. Define a class, and the properties inside refer to other objects, then the instance of this object and the current instance are friends

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

(5) The object created by the current object. Any object, if one of the above conditions is met, is a "friend" of the current object; otherwise, it is a "stranger".

Disadvantages of Dimit's law in a narrow sense:

Create a large number of small methods in the system, these methods only transfer indirect calls, and have nothing to do with the business logic of the system.

Following Dimit's law between classes is to simplify the partial design of a system, because each part will not be directly related to distant objects. However, this will also reduce the communication efficiency between different modules of the system, and it will also make it difficult to coordinate between different modules of the system.

3.2 The embodiment of the generalized Dimit's law in the design of classes

(1) Priority is given to setting a class as an invariant class.

(2) Try to reduce the access authority of a class.

(3) Use Serializable with caution.

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

4. The application of Dimit's rule in design patterns

The Facade and Mediator of design patterns are both the application of Dimit's Law.

Below we have rented a house as an example to study Dimit's law.

Usually, when a client wants to live in a house, we will directly build a house, build a client class, and the client can find a house.

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

The logic of the client looking for a house to live in is very simple. This is ok. Although it violates Dimit's law, it also makes sense in line with business logic.

However, usually we find a house, not all at once. We find a lot of houses, which is very laborious, so it is better to hand it to the agent. The agent has a lot of listings, and the landlord gives the house to the agent. You don t need to care about whether the tenant is Whoever, the tenant gives the landlord the matter of finding a house, he doesn't need to care who the landlord is, and the tenant + landlord is very easy.

/** * House */ public interface IHouse { // public void Housing () ; } public class House implements IHouse { href= "/profile/992988" data-card-uid= "992988" class <a = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void Housing () { System.out.println( "House" ); } } public interface ICustomer { void findHourse (IHouse house) ; } public class Customer implements ICustomer { public void findHourse (IHouse house) { house.Housing(); } } /** * Intermediary */ public class Intermediary { // Find a house public IHouse findHouse (ICustomer customer) { //Help tenants find a house return null ; } }</a> Copy code

The house and the client are independent of each other, and there is no reference to each other. The relationship between them is established through an intermediary, that is, the client finds an intermediary to rent a house, the landlord gives the house to the tenant, and finally the intermediary gives the house to the client. The client and the landlord are isolated from each other, in accordance with Dimit's law.

5. Implementation of Dimit's Law

So in practice, how can one object have the least knowledge of other objects? If we regard an object as a person, then to achieve "a person should have the least understanding of other people", it is enough to do two things:

(1) Talk to direct friends only

There is also an English interpretation of Dimit's law: talk only to your immediate friends.

What is a friend?

Every object must have a coupling relationship with other objects, and the coupling between two objects will become a friend relationship. So what is a direct friend? The classes that appear in the input and output parameters of member variables and methods are direct friends. Dimit's law requires communication only with direct friends.

note:

A class that only appears inside the method body is not a direct friend. If a class communicates with a friend who is not a direct friend, it is a violation of Dimit's law.

Let us give an example to illustrate what is a friend and what is a direct friend. A very simple example: the teacher asks the monitor to count the number of students in the class. There are three categories in this example: Teacher, GroupLeader, and Student.

public interface ITeacher { void command (IGroupLeader groupLeader) ; } public class Teacher implements ITeacher { href= "/profile/992988" data-card-uid= "992988" class <a = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void Command (IGroupLeader groupLeader) { //All classmates 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 counts the number of students groupLeader.count(allStudent); } } ** * Monitor class */ public interface IGroupLeader { //The monitor counts the number of people void count (List<Student> students) ; } /** * Monitor class */ public class GroupLeader implements IGroupLeader { /** * The monitor counts the number of people * @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 counts the number of students System.out.println( "The number of students in class is: " + students.size()); } } /** * Student */ public interface IStudent { } /** * Student */ public class Student implements IStudent { } /** * Client */ public class Client { public static void main (String[] args) { // ITeacher wangTeacher = new Teacher(); // Class leader IGroupLeader zhangBanzhang = new GroupLeader(); wangTeacher.command(zhangBanzhang); } } </a> Copy code

operation result:

The number of students in class is: 5

In this example, how many friends does our Teacher have? 2. one is GroupLeader, which is the input parameter of Teacher's command() method; the other is Student, because Student is used in the body of Teacher's command() method.

So how many direct friends does Teacher have? According to the definition of a direct friend

The classes that appear in the input and output parameters of member variables and methods are direct friends

Only GroupLeader is a direct friend of Teacher.

Teacher created an array of Students in the command() method and communicated with his indirect friend Student. Therefore, the above example violates Dimit's law. A method is a behavior of a class, and a class does not know that its behavior has a dependency relationship with other classes. This is not allowed and it is a serious violation of Dimit's law!

In order to make the above example comply with Dimit's law, we can make the following modifications:

public interface ITeacher { void command (IGroupLeader groupLeader) ; } public class Teacher implements ITeacher { href= "/profile/992988" data-card-uid= "992988" class <a = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void Command (IGroupLeader groupLeader) { //The monitor counts the number of people groupLeader.count(); } } /** * Monitor class */ public interface IGroupLeader { //squad leader counts the number of people void count () ; } /** * Monitor class */ public class GroupLeader implements IGroupLeader { private List<Student> students; public GroupLeader (List<Student> students) { this .students = students; } /** * The monitor counts the number of people */ </a> <a href= "/profile/992988" data-card-uid= "992988" class = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void count () { //The monitor counts the number of people System.out.println( "The number of students in class is: " + students.size()); } } /** * Student */ public interface IStudent { } /** * Student */ public class Student implements IStudent { } /** * Client */ public class Client { public static void main (String[] args) { // 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()); // Class leader IGroupLeader zhangBanzhang = new GroupLeader(allStudent); wangTeacher.command(zhangBanzhang); } } </a> Copy code

operation result:

The number of students in class is: 4

After this modification, each class only communicates with direct friends, effectively reducing the coupling between classes.

(2) Reduce understanding of friends

How to reduce the knowledge of friends? If your friend is a loud speaker, even if you don't take the initiative to ask him, he will keep talking in front of you and tell you all his experiences. Therefore, if you want to reduce your understanding of your friends, please change to a more restrained friend. Changing to a class is to minimize the way a class is exposed.

Give a simple example to illustrate the situation where a class exposes too many methods. The process of a person making coffee with a coffee machine, there are only two types in the example, one is a person and the other is a coffee machine.

The first is CoffeeMachine, the coffee machine 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 () ; //Add water void addWater () ; //Make coffee void makeCoffee () ; } /** * Coffee machine realization class */ public class CoffeeMachine implements ICoffeeMachine { //Add coffee beans public void addCoffeeBean () { System.out.println( "Put coffee beans" ); } //Add water public void addWater () { System.out.println( "Add water" ); } //Make coffee public void makeCoffee () { System.out.println( "making coffee" ); } } /** * People, making coffee */ public interface IMan { /** * Making coffee */ void makeCoffee () ; } /** * People make coffee */ public class Man implements IMan { private ICoffeeMachine coffeeMachine; public Man (ICoffeeMachine coffeeMachine) { this .coffeeMachine = coffeeMachine; } /** * Making coffee */ public void makeCoffee () { coffeeMachine.addWater(); coffeeMachine.addCoffeeBean(); coffeeMachine.makeCoffee(); } } /** * Client */ public class Client { public static void main (String[] args) { ICoffeeMachine coffeeMachine = new CoffeeMachine(); IMan man = new Man(coffeeMachine); man.makeCoffee(); } } Copy code

operation result:

add water

Put coffee beans

Making coffee

In this example, CoffeeMachine is Man's direct friend, but the problem is that Man knows too much about CoffeeMachine. In fact, people don't care about the specific coffee making process of the coffee machine. So we can make the following optimizations:

The optimized coffee machine class only exposes one work method, and the three specific methods for making coffee, addCoffeeBean, addWater, and makeCoffee, are set as private:

/** * Coffee machine abstract interface */ public interface ICoffeeMachine { //Coffee machine work void work () ; } /** * Coffee machine realization class */ public class CoffeeMachine implements ICoffeeMachine { //Add coffee beans public void addCoffeeBean () { System.out.println( "Put coffee beans" ); } //Add water public void addWater () { System.out.println( "Add water" ); } //Make coffee public void makeCoffee () { System.out.println( "making coffee" ); } href= "/profile/992988" data-card-uid= "992988" class <a = "js-nc-card" target= "_blank" from-niu= "default"> @Override public void Work () { addCoffeeBean(); addWater(); makeCoffee(); } } /** * People, making coffee */ public interface IMan { /** * Making coffee */ void makeCoffee () ; } /** * People make coffee */ public class Man implements IMan { private ICoffeeMachine coffeeMachine; public Man (ICoffeeMachine coffeeMachine) { this .coffeeMachine = coffeeMachine; } /** * Making coffee */ public void makeCoffee () { 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 code

After this modification, by reducing the way CoffeeMachine is exposed to the outside, Man's understanding of CoffeeMachine is reduced, thereby reducing the coupling between them.

In practice, as long as you only communicate with your direct friends and reduce your understanding of your friends, Dimit's Law can be satisfied. Therefore, it is not difficult to imagine that the purpose of Dimit's Law is to turn our classes into "fat houses". The "fat" is that a class may have few externally exposed methods, but its internal implementation may be very complicated (this explanation is a bit far-fetched~). "House" is that it only communicates with direct friends. In real life, "fat house" is a derogatory term. In Japan, "fat house" has become a social problem. But in the program, a "fat house" category is a model of an excellent category.

6, some matters needing attention

(1) In the division of classes, weakly coupled classes should be created. The weaker the coupling between classes, the more conducive to achieving the goal of reusability.

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

(3) In the design of a class, whenever possible, a class should be designed as an unchanging class.

(4) In terms of references to other classes, an object's reference to objects of other classes should be minimized.

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

6. the principle of opening and closing

1. What is the principle of opening and closing

The Open Closed Principle (OCP, Open Closed Principle) is the core of all object-oriented principles. The goal pursued by software design itself is to encapsulate changes and reduce coupling, and the open and closed principle is the most direct manifestation of this goal. Other design principles often serve to achieve this goal.

1.1 Definition of opening and closing principle

Software entities like classes, modules and functions should be open for extension but closed for modifications. A software entity like classes, modules and functions should be open for extension but closed for modifications.

1.2 The meaning of the principle of opening and closing

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

(2) The modification is closed, which means that once the design is completed, the class can complete its work independently, without any modification to the existing code.

2. How to realize the principle of opening and closing

"Demand always changes", "No software in the world is constant". The projected meaning here is: the demand is always changing, but for the software designer, how to achieve flexible expansion without modifying the original system. This is the principle of opening and closing.

When we are designing the system, it is impossible to imagine that once the requirements are determined at one time, there will be no changes later. This is not scientific and unrealistic. Since the requirements are bound to change, how do we face this change gracefully What? How to design to make the software relatively easy to modify, so that the requirements will not change, and the entire program must be rebuilt?

Kaifeng-closed principle, the best way to design software to be easy to maintain and not prone to problems is to expand more and modify less.

2.1 Dependence and abstraction

The core idea of achieving openness and closure is to face abstract programming, not to face specific programming, because abstraction is relatively stable.

Let the class rely on a fixed abstraction, so the modification is closed; and through the object-oriented inheritance and polymorphism mechanism, the inheritance of the abstract body can be realized, and the inherent behavior can be changed by overwriting its method, and the new extension method can be realized. So it is open for expansion. This is the basic idea of implementing the open and closed principle.

2.2 How to open and close the principle

If the current design does not comply with the open and closed principle, it must be refactored. The commonly used design patterns mainly include the Template Method design pattern and the Strategy design pattern . Encapsulating changes is an important means to realize this principle, encapsulating the frequently changing part as a class.

2.3 The importance of the principle of opening and closing

(1) The influence of the opening and closing principle on the test

The principle of opening and closing is to keep the original test code still able to run normally, we only need to test the extended code.

(2) The principle of opening and closing can improve reusability

In object-oriented design, all logic is combined from atomic logic, instead of implementing a business logic in a class independently. Only in this way can the code be reused. The smaller the granularity, the greater the possibility of being reused.

(3) The principle of opening and closing can improve maintainability

Requirements for object-oriented development.

3. How to use the principle of opening and closing

(1) Abstract constraints

Constrain the extension through the interface or abstract class, limit the extension of the boundary, and do not allow public methods that do not exist in the interface or abstract class;

The parameter types and reference objects should try to use interfaces or abstract classes instead of implementation classes;

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

(2) Metadata control module behavior

Metadata is data used to describe the environment and data. In layman's terms, it is configuration parameters. Parameters can be obtained from files or databases.

The Spring container is an example of a typical metadata control module behavior, and the ultimate one is Inversion of Control (Inversion of Control)

(3) Formulate the project charter

In a team, it is very important to establish a project charter, because the charter specifies the agreement that all personnel must abide by. For the project, agreement is better than configuration.

(4) Packaging changes

The encapsulation of changes has two meanings:

Encapsulate the same changes into an interface or abstract class;

Encapsulate different changes into different interfaces or abstract classes. Two different changes should not appear in the same interface or abstract class.

4. Case analysis

Case 1: Drawing shapes

Requirements: There are round and elliptical shapes, and the corresponding shapes are drawn 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 () { System.out.println( "Draw a rectangle" ); } public void drawCircle () { System.out.println( "Draw a circle" ); } class Shape { int m_type; } class Rectangle extends Shape { Rectangle() { super .m_type = 1 ; } } class Circle extends Shape { Circle() { super .m_type = 2; } } } Copy code

Let's take a look, this code, at first glance, meets the requirements, and then think about it, what if I add a shape? For example, add a triangle.

To add a triangle class, inherit Shape

To add a method to draw a triangle drawTrriage()

draw type=3

- , . , .

public class GraphicEditor1 { public void draw(Shape shape) { shape.draw(); } interface Shape { void draw(); } class Rectangle implements Shape { @Override public void draw() { System.out.println(" "); } } class Circle implements Shape { @Override public void draw() { System.out.println(" "); } } }

, GraphicEditor.draw() . .

, Shape

draw ,

, , . ?

  • ,
  • ?
  • , , ,

package com.lxl. www.designPatterns.sixPrinciple.openclosePrinciple.bank; /** * */ 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(){ System.out.println(" "); } public void take(){ System.out.println(" "); } public void transfer() { System.out.println(" "); } }

. , --- . ,

operate() . , ,

public interface Business { public void operate(); } public class Save implements Business{ @Override public void operate() { System.out.println(" "); } } public class Take implements Business { @Override public void operate() { System.out.println(" "); } } public class Transfer implements Business { @Override public void operate() { System.out.println(" "); } } /** * */ public class BankBusinesses { /** * * @param business */ public void operate(Business business) { System.out.println(" "); business.operate(); } }

, . ,

, , . , , . , 3 , ,

5

1

2

|