Seven principles of object-oriented design (including SOLID principles)

overview

The SOLID principle is the first 5 initials spelled in the following seven principles

1. 单一职责原则(Single Responsibility Principle)
2. 开闭原则(Open Close Principle)
3. 里氏替换原则(Liskov Substitution Principle)
4. 接口隔离原则(Interface Segregation Principle)
5. 依赖倒置原则(Dependence Inversion Principle)
6. 迪米特法则(Law Of Demeter)
7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)

1. A single principle

The Singularity Principle states that if you have multiple reasons to change a class, then you should separate those reasons for the change and split the class into multiple classes, each responsible for only one change. When you make some kind of change, you only need to modify the class responsible for handling that change. When we change a class with multiple responsibilities, it may affect other functions of the class.

That is, a class should not contain many functions, and try to only complete a single function, so that when you modify a class, it will not affect other functions and avoid error correction. For example, for a class A with multiple functions, when you need to modify class A because of the function of a1, you also need to modify class A because of the function of a2, which violates the single principle, because when you repair the function of a1, you may The function of a2 is affected. On the contrary, if the function of a2 is repaired, the function of a1 may be affected. Because in a class, a public method that both a1 and a2 depend on may be modified, it is easy to cause the above problem.

Furthermore, a method is only responsible for handling one thing.

The Single Responsibility Principle represents a great way to identify classes when designing applications, and it reminds you to think about all the ways a class can evolve. A good separation of responsibilities can only be achieved with a good understanding of how the application works.

2. Liskov Substitution Principle

Only an OO design that satisfies the following two conditions can be considered to satisfy the LSP principle:

  • (1) Conditions for judging derived class types such as if/else should not appear in the code.

  • (2) The derived class should be able to replace the base class and appear anywhere the base class can appear, or if we replace the place where the base class is used in the code with its derived class, the code can still work normally.

    Generally speaking, the Liskov substitution principle is: subclasses can extend the functions of the parent class, but cannot change the original functions of the parent class. That is to say: When a subclass inherits the parent class, in addition to adding new methods to complete the new functions, try not to rewrite the 非抽象methods of the parent class.

The Lie substitution principle requires the following:

  • Subclasses can extend the functions of the parent class, but cannot change the original functions of the parent class.

    That is, subclasses can implement the abstract methods of the parent class, but cannot override the non-abstract methods of the parent class.

  • Subclasses can add their own unique methods.

  • When the method of the subclass overrides the method of the parent class, the preconditions of the method (that is, the input/input parameters of the method) are more relaxed than the input parameters of the parent class method.

  • When a method of a subclass implements a method of the parent class (overloading/overriding or implementing an abstract method), the postconditions (that is, the output/return value of the method) are stricter or equal to those of the parent class.

Advantages and disadvantages of the Liskov substitution principle:

advantage:

  • Code sharing, that is, common code is extracted to the parent class, improving code reusability.
  • Subclasses can have their own characteristics based on the parent class. Improve code scalability.

shortcoming:

  • invasive. Once inherited, all properties and methods of the parent class are owned by the subclass
  • binding. The subclass needs to have the properties and methods of the parent class, and the subclass has some constraints.
  • Coupling. When the parent class is modified, the modification of the subclass needs to be considered.

Therefore, the Liskov substitution principle does not encourage the use of inheritance, but when you have to use inheritance, you need to add some constraints to avoid adverse effects. For example, it cannot affect the original function of the parent class

3. Dependency Inversion Principle

The principle of dependency inversion is to encourage the use of interfaces

What is dependency? : In programming, if a module a uses/calls another module b, we say that module a depends on module b.

High-level modules and low-level modules: Often in an application, we have some low-level classes that implement some basic or elementary operations, which we call low-level modules; there are also some high-level classes, these classes Encapsulates some complex logic and depends on low-level classes, which we call high-level modules.

Dependency Inversion (Dependency Inversion):

In object-oriented programming, dependencies are inverted relative to procedural (structured) programming. Because in traditional structured programming, high-level modules always depend on low-level modules.

An abstract interface is an abstraction of a low-level module, and the low-level module inherits or implements the abstract interface.

In this way, the high-level modules do not directly depend on the low-level modules, but rely on the abstract interface layer. The abstract interface also does not depend on the implementation details of the low-level modules, but the low-level modules depend on (inherit or implement) the abstract interface.

The relationship between classes is established through the abstract interface layer.

For example, the AutoSystem class is a high-level class that references HondaCar and FordCar. The latter two Car classes are both low-level classes. The program can indeed realize unmanned driving for Ford and Honda cars:

public class HondaCar{
    
    
    public void Run(){
    
    
        Console.WriteLine("本田开始启动了");
    }
    public void Turn(){
    
    
        Console.WriteLine("本田开始转弯了");
    }
    public void Stop(){
    
    
        Console.WriteLine("本田开始停车了");
    }
}
public class FordCar{
    
    
    public void Run(){
    
    
        Console.WriteLine("福特开始启动了");
    }
    public void Turn(){
    
    
        Console.WriteLine("福特开始转弯了");
    }
    public void Stop(){
    
    
        Console.WriteLine("福特开始停车了");
    }
}

public class AutoSystem{
    
    
    public enum CarType{
    
    
        Ford,Honda
    };
    private HondaCar hcar=new HondaCar();
    private FordCar fcar=new FordCar();
    private CarType type;
    public AutoSystem(CarType type){
    
    
        this.type=type;
    }
    public void RunCar(){
    
    
        if(type==CarType.Ford){
    
    
            fcar.Run();
        } else {
    
    
            hcar.Run();
        }
    }
    public void TurnCar(){
    
    
        if(type==CarType.Ford){
    
    
            fcar.Turn();
        } else {
    
     
            hcar.Turn();
        }
    }
    public void StopCar(){
    
    
        if(type==CarType.Ford){
    
    
            fcar.Stop();
            } else {
    
    
                hcar.Stop();
            }
    }
}

But software is constantly changing, and software requirements are constantly changing.

Assumption: The company's business has expanded, and it has become a gold medal partner of GM, Mitsubishi, and Volkswagen, so the company requires that the automatic driving system can also be installed on the cars produced by these three companies. So we had to change AutoSystem:

   public class AutoSystem {
    
    
        public enum CarType {
    
    
            Ford, Honda, Bmw
        }

       
        HondaCar hcar = new HondaCar();   //使用new 
        FordCarf car = new FordCar();
        BmwCar bcar = new BmwCar();
        private CarType type;

        public AutoSystem(CarTypetype) {
    
    
            this.type = type;
        }

        public void RunCar() {
    
    
            if (type == CarType.Ford) {
    
    
                fcar.Run();
            } else if (type == CarType.Honda) {
    
    
                hcar.Run();
            } else if (type == CarType.Bmw) {
    
    
                bcar.Run();
            }
        }

        public void TurnCar() {
    
    
            if (type == CarType.Ford) {
    
    
                fcar.Turn();
            } else if (type == CarType.Honda) {
    
    
                hcar.Turn();
            } else if (type == CarType.Bmw) {
    
    
                bcar.Turn();
            }
        }

        public void StopCar() {
    
    
            if (type == CarType.Ford) {
    
    
                fcar.Stop();
            } else if (type == CarType.Honda) {
    
    
                hcar.Stop();
            } else if (type == CarType.Bmw) {
    
    
                bcar.Stop();
            }
        }
    }

Analysis: This adds new interdependencies to the system. As time goes by, more and more car types must be added to AutoSystem, this "AutoSystem" module will be messed up with if/else statements, and depends on many low-level modules, as long as the low-level modules change, AutoSystem must be changed accordingly

So how to solve it? In the interface form, the AutoSystem system depends on the abstraction of ICar, but has nothing to do with the specific implementation details of HondaCar, FordCar, and BMWCar, so changes in the implementation details will not affect AutoSystem. As for the implementation details, it is only necessary to implement ICar, that is, the implementation details depend on the ICar abstraction.

    public interface ICar
    {
    
    
        void Run();
        void Turn();
        void Stop();
    }
    public class BmwCar:ICar
    {
    
    
        public void Run()
        {
    
    
            Console.WriteLine("宝马开始启动了");
        }
        public void Turn()
        {
    
    
            Console.WriteLine("宝马开始转弯了");
        }
        public void Stop()
        {
    
    
            Console.WriteLine("宝马开始停车了");
        }
    }
    public class FordCar:ICar
    {
    
    
        publicvoidRun()
        {
    
    
            Console.WriteLine("福特开始启动了");
        }
        public void Turn()
        {
    
    
            Console.WriteLine("福特开始转弯了");
        }
        public void Stop()
        {
    
    
            Console.WriteLine("福特开始停车了");
        }
    }
    public class HondaCar:ICar
    {
    
    
        publicvoidRun()
        {
    
    
            Console.WriteLine("本田开始启动了");
        }
        public void Turn()
        {
    
    
            Console.WriteLine("本田开始转弯了");
        }
        public void Stop()
        {
    
    
            Console.WriteLine("本田开始停车了");
        }
    }
    public class AutoSystem
    {
    
    
        private ICar icar;
        public AutoSystem(ICar icar)    //使用构造函数作为入参,不再使用new创建具体的CaR实例
        {
    
    
            this.icar=icar;
        }
        private void RunCar()
        {
    
    
            icar.Run();
        }
        private void TurnCar()
        {
    
    
            icar.Turn();
        }
        private void StopCar()
        {
    
    
            icar.Stop();
        }
    }

Applying this principle means that upper-level classes do not directly use lower-level classes, they use interfaces as an abstraction layer. In this case, the code for creating an object of the underlying class in the upper class cannot directly use the new operator. For example, in the above example, AutoSystem finally uses the constructor to pass in an instance of Car. Some creational design patterns can be used such as factory method, abstract factory and prototype pattern. 模版设计模式是应用依赖倒转原则的一个例子. Of course, using this pattern requires extra effort and more complex code, but it can lead to a more flexible design. This principle should not be used indiscriminately, and if we have a class whose functionality has a high probability that it will not change in the future, then we don't need to use it.

Template mode, see [Design Mode] The difference between strategy mode and template mode , JDBCTemplate, RedisTemplate, MongoTemplate, etc. are typical template modes

4. Interface Segregation Principle (Interface Segregation Principle, ISP)

Users cannot be forced to rely on interfaces they do not use.

Interface design principles: Interface design should follow the principle of the smallest interface, and don't stuff methods that users don't use into the same interface. If the method of an interface is not used, it means that the interface is too fat, and it should be divided into several functional interfaces.

The interface segregation principle states that clients should not be forced to implement some interfaces they will not use, and should group the methods in a fat interface and replace it with multiple interfaces, each serving a submodule.

If you have designed a fat interface, you can use the adapter pattern to isolate it. Like other design principles, the interface segregation principle requires additional time and effort and increases code complexity, but can result in a more flexible design. If we use it excessively, it will generate a large number of interfaces containing a single method, so we need to use it based on experience and identify those codes that need to be extended in the future.

Generally speaking, the principle of interface separation encourages the use of interfaces, but certain constraints are imposed to better play the role of interfaces!

For example, there are many interfaces in the swing component event listener. When you want to implement a certain event, you must implement all interfaces, and you can use the WindowAdapter adapter to avoid this situation:

General event interface:

 public interface EventListener {
    
    
 }


WindowListener extends this interface, and there are too many abstract methods:

public interface WindowListener extends EventListener {
    
    
    /**
     * Invoked the first time a window is made visible.
     */
    public void windowOpened(WindowEvent e);

    public void windowClosing(WindowEvent e);

    public void windowClosed(WindowEvent e);

    public void windowIconified(WindowEvent e);

    public void windowDeiconified(WindowEvent e);

    public void windowActivated(WindowEvent e);

    public void windowDeactivated(WindowEvent e);
}

WindowAdapter is an abstract class. But in this abstract class 没有抽象方法:

public abstract class WindowAdapter
    implements WindowListener, WindowStateListener, WindowFocusListener
{
    
    
    /**
     * Invoked when a window has been opened.
     */
    public void windowOpened(WindowEvent e) {
    
    }

    public void windowClosing(WindowEvent e) {
    
    }

    public void windowClosed(WindowEvent e) {
    
    }

    public void windowIconified(WindowEvent e) {
    
    }

    public void windowDeiconified(WindowEvent e) {
    
    }

    public void windowActivated(WindowEvent e) {
    
    }

    public void windowDeactivated(WindowEvent e) {
    
    }

    public void windowStateChanged(WindowEvent e) {
    
    }

    public void windowGainedFocus(WindowEvent e) {
    
    }

    /**
     * Invoked when the Window is no longer the focused Window, which means
     * that keyboard events will no longer be delivered to the Window or any of
     * its subcomponents.
     *
     * @since 1.4
     */
    public void windowLostFocus(WindowEvent e) {
    
    }
}

5. Law of Demeter

Definition: If two software entities do not need to communicate directly, then a direct mutual call should not occur, and the call can be forwarded through a third party. Its purpose is to reduce the degree of coupling between classes and improve the relative independence of modules.

The Law of Demeter is also called The Least Knowledge Principle. Generally speaking, it means that the less a class knows about the classes it depends on, the better. That is to say, for the dependent class, no matter how complex the logic is, try to encapsulate the logic inside the class as much as possible, and do not leak any information to the outside world except for the public methods provided.

There is an even simpler definition of Demeter's Law: only communicate with immediate friends. First, let’s explain what a direct friend is: We call the classes that appear in member variables, method parameters, and method return values ​​direct friends, while the classes that appear in local variables are not direct friends. That is to say, unfamiliar classes are best not to appear inside the class as local variables.

The purpose of Dimit's law is to reduce the coupling between classes. Because each class minimizes its dependence on other classes, it is easy to make the functional modules of the system independent of each other, and there is no dependency relationship between them.

One possible consequence of applying Dimit's law is that there are a large number of intermediary classes in the system. These classes exist only to transfer the mutual calling relationship between classes-this increases the complexity of the system to a certain extent.

The facade mode (Facade) and intermediary mode (Mediator) in the design mode are examples of the application of Dimit's law.

Disadvantages of Dimit's law in the narrow sense:

  • Following Dimit's law between classes will simplify the local design of a system, because each part will not be associated with distant objects. However, this also reduces the communication efficiency between different modules of the system, and also makes it difficult to coordinate between different modules of the system.

The embodiment of the generalized Demeter law in the design of the class:

  • Try to reduce the access rights of a class.
  • Keep members' access privileges as low as possible.

example

The company wants to print the personnel information of a certain department, the code is as follows, a counter example:

/**
 * 雇员
 */
public class Employee {
    
    
    private String name;

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }
}
/**
 * 部门经理
 */
public class Manager {
    
    
    public List<Employee> getEmployees(String department) {
    
    
        List<Employee> employees = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
    
    
            Employee employee = new Employee();
            // 雇员姓名
            employee.setName(department + i);
            employees.add(employee);
        }
        return employees;
    }
}
/**
 * 公司
 */
public class Company {
    
    
	private Manager manager = new Manager();

    public void printEmployee(String name){
    
    
        List<Employee> employees = manager.getEmployees(name);
        for (Employee employee : employees) {
    
    
            System.out.print(employee.getName() + ";");
        }
    }
}

The printEmployee in the Company class does successfully print the personnel information, but the Employee class only appears in the printEmployee() method as a local variable, and is an indirect friend of the Company class (only communicates with direct friends 违背了迪米特法则)

The correct way is as follows : Put the method of printing employee information in the Company class in the Manager class, and only call the printEmployee() method in Manager in the Company, and the Employee class will no longer appear in the Company class

/**
 * 雇员
 */
public class Employee {
    
    
    private String name;

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }
}
/**
 * 部门经理
 */
public class Manager {
    
    
    public List<Employee> getEmployees(String department) {
    
    
        List<Employee> employees = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
    
    
            Employee employee = new Employee();
            // 雇员姓名
            employee.setName(department + i);
            employees.add(employee);
        }
        return employees;
    }

    public void printEmployee(String name){
    
    
        List<Employee> employees = this.getEmployees(name);
        for (Employee employee : employees) {
    
    
            System.out.print(employee.getName() + ";");
        }
    }
}
/**
 * 公司
 */
public class Company {
    
    
    private Manager manager = new Manager();

    public void printEmployee(String name){
    
    
        manager.printEmployee(name);
    }
}

You can even set the permissions of the Employee class to be visible only to the Manager, not to the Company

6. Combination/aggregation reuse principle

Aggregation means the relationship between the whole and the part, and means "contains". The whole is composed of parts, and the part can exist as an independent individual without the whole.
Combination is a stronger aggregation. Parts form a whole and are inseparable. Parts cannot exist independently of the whole. In a composite relationship, parts have the same lifecycle as the whole, and the composite new object has full control over its constituent parts, including their creation and destruction.

Composition/aggregation and inheritance are two basic ways to achieve reuse. The principle of synthetic reuse refers to using composition/aggregation as much as possible instead of inheritance. Inheritance relationships should only be used when all of the following conditions are met:

  • A subclass is a special kind of a superclass, not a role of a superclass, which is to distinguish between "Has-A" and "Is-A". Only the "Is-A" relationship conforms to the inheritance relationship, and the "Has-A" Relationships should be described using aggregations.
  • There should never be a need to replace a subclass with a subclass of another class. If you are not sure whether it will become another subclass in the future, don't use inheritance.
  • A subclass has the responsibility to extend the superclass, not to replace or unregister the superclass. If a subclass needs to substantially replace the behavior of the superclass, then the class should not be a subclass of the superclass.

Difference Between Composition and Aggregation

7. The principle of opening and closing

The principle of opening and closing is the general chapter of the other six principles. We deliberately put it at the end. That is to say, when your code meets the first 6 principles, it basically meets the principle of opening and closing!

  • Open to extension ------- The behavior of the module can be extended to meet new requirements.
  • Closed for modification ------- Do not allow modification of the source code of the module (or try to minimize the modification)

The open-closed principle says that we should strive to design modules that don't need to be modified. In practical applications, the changed code is isolated from the code that does not need to be changed, the changed code is abstracted into a stable interface, and programming is performed on the interface. When extending the behavior of the system, we only need to add new code without modifying existing code. This can generally be achieved by adding new subclasses and overriding methods of the parent class.

开闭原则是面向对象设计的核心,满足该原则可以达到最大限度的复用性和可维护性

reference

7 Principles of Object-Oriented Design
7 Principles of Object-Oriented Design
Software Development: The Seven Principles of Object-Oriented Design!
Liskov Substitution Principle of Object-Oriented Design Principles with English Definition

Guess you like

Origin blog.csdn.net/m0_45406092/article/details/129697093