Six principles of Unity design pattern

Six principles of design mode

1. Purpose of design mode
Design pattern is for better code reusability, readability, reliability and maintainability.

2. Six commonly used design modes
1) Single responsibility principle
2) Richter's substitution principle
3) Dependency Inversion Principle
4) Interface isolation principle
5) Dimitt's law
6) Opening and closing principle

3. Principle of single responsibility
This principle is for classes, that is, a class should be responsible for only one responsibility.
For example, class T is responsible for two different responsibilities: responsibility P1 and responsibility P2. When the requirements of responsibility P1 change and change t, it may cause the failure of responsibility P2. Therefore, the granularity of class T needs to be decomposed into T1 and T2.
Examples are as follows:
Breathe the scene with an animal like second

class Animal {
    public void breathe(string animal)
    {
        Console.WriteLine(animal+"Breathe air");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("cattle");
        animal.breathe("sheep");
        animal.breathe("pig");
        animal.breathe("fish");
        Console.ReadLine();
    }
}

Output result:

We found that not all animals breathe air. For example, fish breathe water. According to the principle of single responsibility, we subdivide Animal into terrestrial animals and aquatic animals, as shown below:

class Terrestrial
{
    public void breathe(string animal)
    {
        Console.WriteLine(animal+"Breathe air");
    }
}
class Aquatic
{
    public void breathe(string animal)
    {
        Console.WriteLine(animal + "Breathing water");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("cattle");
        terrestrial.breathe("sheep");
        terrestrial.breathe("pig");
        Aquatic aquatic = new Aquatic();
        aquatic.breathe("fish");
        Console.ReadLine();
    }
}

We find that such modification costs a lot. We need to decompose the original class and modify the client. The direct modification of Animal class violates the principle of single responsibility, but the cost is much smaller, as shown below:

class Animal
{
    public void breathe(string animal)
    {
        if ("fish".Equals(animal))
        {
            Console.WriteLine(animal + "Breathing water");
        }
        else {
            Console.WriteLine(animal + "Breathe air");
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("cattle");
        animal.breathe("sheep");
        animal.breathe("pig");
        animal.breathe("fish");
        Console.ReadLine();
    }
}

As you can see, this modification method is much simpler. However, there are hidden dangers. One day, the fish need to be divided into freshwater fish and seawater fish, and the breathe method of Animal needs to be modified. It may bring risks to "pig, cow and sheep" and other related functions. This modification directly violates the principle of single responsibility at the code level. Although it is the simplest to modify, it has the greatest hidden danger. There is another modification method:

class Animal
{
    public void breathe(string animal)
    {
         Console.WriteLine(animal + "Breathe air");
    }
    public void breathe2(string animal)
    {
        Console.WriteLine(animal + "Breathing water");
    }
}
class Program
{
    static void Main(string[] args)
    {
        Animal animal = new Animal();
        animal.breathe("cattle");
        animal.breathe("sheep");
        animal.breathe("pig");
        animal.breathe2("fish");
        Console.ReadLine();
    }
} 

This modification method does not change the original method, but adds a new method to the class. Although it violates the principle of single responsibility, it conforms to the principle of single responsibility at the method level. So which one is used in actual programming? My principle is that only when the logic is simple enough can the single responsibility principle be violated at the code level; Only when the number of methods in the class is small enough can the principle of single responsibility be violated at the method level.

Advantages of following a single responsibility:
1) Reduce the complexity of classes. A class is responsible for only one responsibility.
2) Improve the readability and maintainability of the class
3) Reduce the risk caused by changes.

4. Richter substitution principle
The principle was proposed by a woman surnamed Li at MIT in 1988.
If there is object o2 of type T2 for each object o1 of type T1, so that the behavior of program P does not change when all objects o1 are replaced with o2, then type T2 is a subtype of type T1.
In other words, all references to the base class must be able to use the objects of its subclasses transparently.

According to the definition, when using inheritance, follow the Richter substitution principle and try not to override and overload the methods of the parent class in the subclass.
Inheritance contains such a layer of meaning: all implemented methods in the parent class (compared with abstract methods) are actually setting a series of specifications and contracts. Although it does not force all subclasses to follow these contracts, if the subclass arbitrarily modifies these non extraction methods, it will destroy the whole inheritance system. The Richter substitution principle expresses this meaning.
Inheritance, as one of the three characteristics of object-oriented, not only brings great traversal to program design, but also brings disadvantages. For example, using inheritance will bring invasiveness to the program, reduce the portability of the program and increase the coupling between objects. If a class is inherited by other classes, all subclasses must be considered when the class needs to be modified, and all functions involving subclasses may fail after the parent class is modified.
To illustrate the risk of inheritance, we need to complete a function of subtracting two numbers, and class A is responsible for it.

class A{
    public int func1(int a,int b){
        return a-b;
    }
}
public class Client{
    public static void main(string[] args){
        A a=new A();
        System.out.println("100-50="+a.func1(100,50));
        System.out.println("100-80="+a.func1(100,80));
    }
}

Operation results:

100-50=50
100-80=20

Later, we need to add a new function: complete the addition of two numbers, and then sum with 100. Class B is responsible for it.

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 a=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));
    }
}

Operation results:

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, resulting in errors in the original function. In actual programming, we often complete new functions by rewriting the parent class. Although it is simple to write, the reusability of the whole inheritance system will be poor. Especially when running polymorphism frequently, if you have to rewrite the method of the parent class, the general practice is: the original parent class and child class inherit a more popular base class, remove the original inheritance relationship, and replace it with dependency, aggregation, combination and other relationships.

5. Reliance reversal principle
High level modules should not rely on low-level modules, and both should rely on their abstraction; Abstract should not rely on details, details should rely on abstraction.
Class a directly depends on class B. If you want to change class A to dependent class C, you must modify the code of class A. At this time, class A is generally a high-level module responsible for complex business logic, while class B and class C are low-level modules responsible for basic atomic operations; Modifying a will bring risks to the program.
Modifying class A does not depend on interface I. class B and class C implement interface I respectively. Class A is indirectly connected with class B or class C through interface I, which will greatly reduce the probability of modifying class A.
The dependency inversion principle is based on the fact that abstract things are much more stable than the variability of details. Abstract based architecture is much more stable than detail based architecture. In java, abstraction refers to interfaces or abstract classes. Details are concrete implementation classes. The purpose of using interfaces or abstract classes is to formulate specifications without involving any specific operations, and hand over the task of showing details to their implementation classes.
The central idea of dependency inversion is interface oriented programming.

Code examples are as follows:

class Book {
    public string getContent() {
        return "long time ago.....";
    }
}
class Mother {
    public void narrate(Book book)
    {
        Console.WriteLine(book.getContent());
    }
}
class Program
{
    static void Main(string[] args)
    {
        Mother monther = new Mother();
        monther.narrate(new Book());
        Console.ReadLine();
    }
}

Operation results:

If you read newspapers and magazines, you find that the client is not applicable.
We introduce an abstract interface IReader, which represents reading materials

interface IReader{
    public string getContent();
}

In this way, the Mother class has a dependency relationship with the interface IReader, while Book and Newspaper belong to the category of reading materials. They each implement the IReader interface, which complies with the principle of dependency inversion. The modified code is as follows:

interface IReader {
         string getContent();
    }
    class Newspaper: IReader
    {
    public string getContent()
    {
        return "Chelsea won 12 consecutive victories";
    }
}
class Book:IReader
{

    public string getContent()
{
    return "long time ago....";
}
}
class Mother
{
    public void narrate(IReader reader)
    {
        Console.WriteLine(reader.getContent());
    }
}
class Program
{
    static void Main(string[] args)
    {
        Mother monther = new Mother();
        monther.narrate(new Book());
        monther.narrate(new Newspaper());
        Console.ReadLine();
    }
}

Operation results:

Using the dependency inversion principle brings great convenience to multi person parallel development. For example, the mother class and the Book class in the above column are directly coupled. Mother can only code after the Book class is encoded, because the mother class depends on the Book class. The modified procedures can be started at the same time without affecting each other.
There are three ways to transfer dependencies: interface transfer, construction method transfer and setter method transfer.
Interface transfer:

interface IDriver{
    public void drive(ICar car);
}
public class Driver:IDriver{
    public void drive(ICar car){
        car.run();
    }
}

Construction method transfer:

interface IDriver{
    public void drive();
}
public class Driver implements IDriver{
    public ICar car;
    public Driver(ICar _car){
        this.car=_car;
    }
    public void drive(){
        this.car.run();
    }
}

setter mode transmission:

interface IDriver{
    public void setCar(ICar car);
    public void drive();
}
public class Driver:IDriver{
    PRIVATE ICar car;
    public void setCar(ICar car){
        this.car=car;
    }
    public void drive(){
        this.car.run();
    }
}

In actual programming, the following three points are generally required:
Low level modules should have abstract classes or interfaces, or both.
The declaration type of variables should be abstract classes or interfaces.
Follow the Richter substitution principle when using inheritance

6. Interface isolation principle
The client should not rely on interfaces it does not need; The dependence of one class on another should be based on the smallest interface.
Class a depends on class B through interface I, and class C depends on class D through interface I. If interface I is not the smallest interface for class A and class C, class B and class D must implement methods they do not need.
The bloated interface I is divided into several independent interfaces, and class A and class C establish dependencies with the interfaces they need respectively. That is, the principle of interface isolation is adopted.
Give an example to illustrate the interface isolation principle:

The meaning of this figure is: Class A depends on Method 1, method 2 and method 3 in interface I. class B is the implementation of class a dependence; Class C depends on Method 1, method 4, method 5 and class D in interface I, which are the implementation of class C dependence. For class B and class D, although there are unused methods (shown by the red mark), since interface I is implemented, these unused methods must also be implemented. The code is as follows:

interface I{
    void method1();
    void method2();
    void method3();
    void method4();
    void method5();
}
class A{
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method2();
    }
    public void depend3(I i){
        i.method3();
    }
}
class C{
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method4();
    }
    public void depend3(I i){
        i.method5();
    }
}
class B:I{
    public void method1(){
        Console.WriteLine("class B Implementation interface I Method 1");
    }
    public void method2(){
        Console.WriteLine("class B Implementation interface I Method 2");
    }
    public void method3(){
        Console.WriteLine("class B Implementation interface I Method 3");
    }
    public void method4(){}
    public void method5(){}
}
class D:I{
    public void method1(){
        Console.WriteLine("class B Implementation interface I Method 1");
    }
    public void method2(){}
    public void method3(){}
    public void method4(){
        Console.WriteLine("class B Implementation interface I Method 4");
    }
    public void method5(){
        Console.WriteLine("class B Implementation interface I Method 5");
    }
}
class Program
{
    static void Main(string[] args)
    {
        A a=new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());
        
        C c=new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
        Console.ReadLine();
    }
}

It can be seen that the methods in the interface must be implemented in the implementation class regardless of whether they have an effect on the classes that depend on them. So we split the original interface I into three interfaces:

The code is as follows:

interface I1{
    void method1();
}
interface I2{
    void method2();
    void method3();
}
interface I3{
    void method4();
    void method5();
}
class A{
    public void depend1(I1 i){
        i.method1();
    }
    public void depend2(I2 i){
        i.method2();
    }
    public void depend3(I2 i){
        i.method3();
    }
}
class C{
    public void depend1(I1 i){
        i.method1();
    }
    public void depend2(I3 i){
        i.method4();
    }
    public void depend3(I3 i){
        i.method5();
    }
}
class B:I1,I2{
    public void method1(){
        Console.WriteLine("class B Implementation interface I1 Method 1");
    }
    public void method2(){
        Console.WriteLine("class B Implementation interface I2 Method 2");
    }
    public void method3(){
        Console.WriteLine("class B Implementation interface I2 Method 3");
    }
}
class D:I1,I3{
    public void method1(){
        Console.WriteLine("class B Implementation interface I Method 1");
    }
    public void method4(){
        Console.WriteLine("class B Implementation interface I Method 4");
    }
    public void method5(){
        Console.WriteLine("class B Implementation interface I Method 5");
    }
}
class Program
{
    static void Main(string[] args)
    {
        A a=new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());
        
        C c=new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
        Console.ReadLine();
    }
}

At this point, you may feel that the interface isolation principle is very similar to the previous single responsibility principle, but it is not. 1, The single responsibility pays attention to the responsibility, while the interface isolation principle pays attention to the isolation of interface dependence; 2, The single responsibility is the constraint class, followed by the method, which aims at the implementation and details of the program; The principle of interface isolation restricts the interface and aims at the abstraction and the construction of the overall framework of the program.

7. Dimitri's law
One object should have minimal knowledge of other objects.
The closer the relationship between classes, the greater the degree of coupling.
Dimitri's law is also called the least known principle, that is, the less a class knows about the class it depends on, the better. In other words, no matter how complex the dependent class is, try to encapsulate the logic inside the class. No information will be disclosed except the public method provided.
There is a simpler definition of Dimitri's Law: only communicate with direct friends.
What is a direct friend: each object is coupled with other objects. As long as there is a coupling relationship between two objects, we say that the two objects are friends. There are many ways of coupling, such as dependency, association, combination, aggregation and so on. Among them, we call the classes that appear in member variables, method parameters and method return values as direct friends, while the classes that appear in local variables are not direct friends. In other words, unfamiliar classes should not appear inside the class in the form of local variables.
For example, there is a group company whose subordinate companies have branches and departments directly under it. Now it is required to print the employee ID of all subordinate companies.

class Employee{
    private string id;
    public void setId(string id){
        this.id=id;
    }
    public string getId(){
        return id;
    }
}
class SubEmployee{
    private string id;
    public void setId(string id){
        this.id=id;
    }
    public string getId(){
        return id;
    }
}
class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list=new ArrayList(SubEmployee);
        for(int i=0;i<100;i++){
            SubEmployee emp=new SubEmployee();
            emp.setId("branch office"+i);
            list.add(emp);
        }
        return list;
    }
}
class CompanyManager{
    public List<Employee> getAllEmployee(){
        List<Employee> list=new ArrayList<Employee>();
        for(int i=0;i<30;i++)
        {
            Employee emp=new Employee();
            emp.setId("headquarters"+i);
            list.add(emp);
        }
        return list;
    }
    publi void printAllEmployee(SubCompanyManager sub){
        List<SubEmployee> list1=sub.getAllEmployee();
        foreach(SubEmployee e in list1){
            Console.WriteLine(e.getId());
        }
        List<Employee> list2=this.getAllEmployee();
        foreach(Employee e in list2){
            Console.WriteLine(e.getId());
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        CompanyManager e=new CompanyManager();
        e.printAllEmployee(new SubCompanyManager());
        Console.ReadLine();
    }
}

The problem with this design is that in CompanyManager, the SubEmployee class is not a direct friend of the CompanyManager class. According to Demeter's law, such coupling of indirect friends should be avoided in the class. The modified code is as follows:

class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list = new ArrayList<SubEmployee>();
        for(int i=0; i<100; i++){
            SubEmployee emp = new SubEmployee();
            //Assign an ID to branch personnel in order
            emp.setId("branch office"+i);
            list.add(emp);
        }
        return list;
    }
    public void printEmployee(){
        List<SubEmployee> list = this.getAllEmployee();
        for(SubEmployee e:list){
            System.out.println(e.getId());
        }
    }
}
class CompanyManager{
    public List<Employee> getAllEmployee(){
        List<Employee> list = new ArrayList<Employee>();
        for(int i=0; i<30; i++){
            Employee emp = new Employee();
            //Assign an ID to the head office personnel in order
            emp.setId("headquarters"+i);
            list.add(emp);
        }
        return list;
    }
    
    public void printAllEmployee(SubCompanyManager sub){
        sub.printEmployee();
        List<Employee> list2 = this.getAllEmployee();
        for(Employee e:list2){
            System.out.println(e.getId());
        }
    }
}

The original intention of dimitt's law is to reduce the coupling between classes. Because each class reduces unnecessary dependencies, it can indeed reduce the coupling relationship.

8. Opening and closing principle
A software entity such as classes, modules and functions should be open to extensions and closed to modifications. Build the framework with abstraction and extend the details with implementation.
When the software needs to change, try to realize the change by expanding the behavior of the software entity, rather than by modifying the existing code.
When we follow the above five principles and use the design patterns in 23, the purpose is to follow the opening and closing principles.

Design mode various modes reference website: Design pattern | rookie tutorial

 

Keywords: C# Unity Design Pattern

Added by dancingbear on Sat, 12 Feb 2022 10:15:20 +0200