1

A design principle (SOLID)

1. S - Single Responsibllity Principle

1.1 Definitions

A class or module is only responsible for completing one responsibility (or function), and the concept of "objects should only have a single function", if a class contains two or more functions that are not related to the business, it is considered a responsibility Not single enough, it can be differentiated into multiple classes with a single function

1.2 Take a chestnut

The Employee class contains multiple different behaviors, violating the single-blame principle

file

By splitting out the TimeSheetReport class, it relies on the Employee class and follows the single blame principle
file

2. O - Open-Closed Principle

2.1 Definitions

Software entities (including classes, modules, functions, etc.) should be open for extension, but closed for modification, satisfying the following two characteristics

  • open to extension

Modules are open to extension, which means that when requirements change, modules can be extended to have new behaviors that meet those changes

  • close for modification

The module is closed for modification, which means that when the requirements change, the function should be expanded on the basis of not modifying the source code as much as possible.

2.2 Give a chestnut

Shipping costs need to be calculated according to different shipping methods in the order

Order

The transportation cost is calculated in the class. If a new transportation method is added later, the original method getShippingCost() of Order needs to be modified, which violates OCP
file

According to the idea of polymorphism , shipping can be abstracted into a class, and subsequent transportation methods can be added without modifying the original method of the Order class .
Just add a derived class of Shipping .
file

3. L - Liskov Substitution Principle

3.1 Definitions

Where the parent class is used, it can be replaced by a subclass, and the subclass can be compatible with the parent class

  • The parameter type of the subclass method should be more abstract or broader than the parameter type of the parent class method
  • The return value type of the subclass method should be more specific or narrower than the return value type of the superclass method

3.2 Give a chestnut

The parameter type of the subclass method should be more abstract or broader than the parameter type of the parent class method
demo

 class Animal {}
class Cat extends Animal {
  faviroteFood: string;
  constructor(faviroteFood: string) {
    super();
    this.faviroteFood = faviroteFood;
  }
}

class Breeder {
  feed(c: Animal) {
    console.log("Breeder feed animal");
  }
}

class CatCafe extends Breeder {
  feed(c: Animal) {
    console.log("CatCafe feed animal");
  }
}

const animal = new Animal();

const breeder = new Breeder();
breeder.feed(animal);
// 约束子类能够接受父类入参
const catCafe = new CatCafe();
catCafe.feed(animal);
  • The return value type of the subclass method should be more specific or narrower than the return value type of the superclass method
 class Animal {}

class Cat extends Animal {
  faviroteFood: string;
  constructor(faviroteFood: string) {
    super();
    this.faviroteFood = faviroteFood;
  }
}

class Breeder {
  buy(): Animal {
    return new Animal();
  }
}

class CatCafe extends Breeder {
  buy(): Cat {
    return new Cat("");
  }
}

const breeder = new Breeder();
let a: Animal = breeder.buy();

const catCafe = new CatCafe();
a = catCafe.buy();
  • Subclasses should not enforce preconditions
  • Subclasses should not weaken postconditions

4. I - Interface Segregation Principle

4.1 Definitions

The client should not depend on interfaces it does not need, and the dependencies of one class on another should be built on the smallest interface

4.2 Take a chestnut

Class A depends on class B through interface I, class C depends on class D through interface I, if interface I is not the minimum interface for class A and class B, then class B and class D must implement methods they do not need
 interface I {
  m1(): void;
  m2(): void;
  m3(): void;
  m4(): void;
  m5(): void;
}

class B implements I {
  m1(): void {}
  m2(): void {}
  m3(): void {}
  //实现的多余方法
  m4(): void {}
  //实现的多余方法
  m5(): void {}
}

class A {
  m1(i: I): void {
    i.m1();
  }
  m2(i: I): void {
    i.m2();
  }
  m3(i: I): void {
    i.m3();
  }
}

class D implements I {
  m1(): void {}
  //实现的多余方法
  m2(): void {}
  //实现的多余方法
  m3(): void {}
  
  m4(): void {}
  m5(): void {}
}

class C {
  m1(i: I): void {
    i.m1();
  }
  m4(i: I): void {
    i.m4();
  }
  m5(i: I): void {
    i.m5();
  }
}

Split the bloated interface I into several independent interfaces , and class A and class C establish dependencies with the interfaces they need respectively

 interface I {
  m1(): void;
}

interface I2 {
  m2(): void;
  m3(): void;
}

interface I3 {
  m4(): void;
  m5(): void;
}

class B implements I, I2 {
  m1(): void {}
  m2(): void {}
  m3(): void {}
}

class A {
  m1(i: I): void {
    i.m1();
  }
  m2(i: I2): void {
    i.m2();
  }
  m3(i: I2): void {
    i.m3();
  }
}

class D implements I, I3 {
  m1(): void {}
  m4(): void {}
  m5(): void {}
}

class C {
  m1(i: I): void {
    i.m1();
  }
  m4(i: I3): void {
    i.m4();
  }
  m5(i: I3): void {
    i.m5();
  }
}

4.3 Chestnuts in reality

Take e-bike as an example

file

Ordinary electric bicycles do not have the function of locating and viewing historical trips, but since the interface ElectricBicycle is implemented, methods that are not required in the interface must be implemented. A better way is to split

file

5. D - Dependency Inversion Principle

5.1 Definitions

Instead of relying on a specific service executor, rely on an abstract service interface, and shift from relying on concrete implementations to relying on abstract interfaces. Inversely, in software design, classes can be divided into two levels: high-level modules , low-level modules , and high-level modules. Modules should not depend on low-level modules, both should depend on their abstractions. High-level modules refer to the caller, and low-level modules refer to some basic operations

Dependency inversion is based on the fact that abstractions are much more stable than implementation details are variable

5.2 Take a chestnut

The SoftwareProject class directly depends on two low-level classes, FrontendDeveloper and BackendDeveloper , and when a new low-level module comes, it is necessary to modify the dependencies of the high-level module SoftwareProject
 class FrontendDeveloper {
  public writeHtmlCode(): void {
    // some method
  }
}

class BackendDeveloper {
  public writeTypeScriptCode(): void {
    // some method
  }
}

class SoftwareProject {
  public frontendDeveloper: FrontendDeveloper;
  public backendDeveloper: BackendDeveloper;

  constructor() {
    this.frontendDeveloper = new FrontendDeveloper();
    this.backendDeveloper = new BackendDeveloper();
  }

  public createProject(): void {
    this.frontendDeveloper.writeHtmlCode();
    this.backendDeveloper.writeTypeScriptCode();
  }
}

The dependency inversion principle can be followed. Since FrontendDeveloper and BackendDeveloper are similar classes, a develop interface can be abstracted and let FrontendDeveloper and BackendDeveloper implement it. We do not need to initialize FrontendDeveloper and BackendDeveloper in a single way in the SoftwareProject class, but use them as a list to iterate over them, calling each develop() method separately

 interface Developer {
  develop(): void;
}

class FrontendDeveloper implements Developer {
  public develop(): void {
    this.writeHtmlCode();
  }
  
  private writeHtmlCode(): void {
    // some method
  }
}

class BackendDeveloper implements Developer {
  public develop(): void {
    this.writeTypeScriptCode();
  }
  
  private writeTypeScriptCode(): void {
    // some method
  }
}

class SoftwareProject {
  public developers: Developer[];
  
  public createProject(): void {
    this.developers.forEach((developer: Developer) => {
      developer.develop();
    });
  }
}

Two Visitor Pattern (Visitor Pattern)

1. Intention

Represents an operation that acts on elements in an object structure. It allows you to define new operations that act on elements without changing their classes

  • The role of Visitor , that is 作用于某对象结构中的各元素的操作 , that is, Visitor is used to manipulate object elements
  • 它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作 That is to say, you can only modify the definition of the new operation in the Visitor itself, without modifying the original object. The magic of the Visitor design is that the operation right of the object is handed over to the Visitor

2. Scenario

  • If you need to perform some operation on all elements in a complex object structure (such as an object tree), you can use the visitor pattern
  • The visitor pattern allows you to perform the same operation on a group of objects belonging to different classes by providing variants of the same operation for multiple target classes in the visitor object

    3. Visitor pattern structure

  • Visitor : the visitor interface
  • ConcreteVisitor : concrete visitor
  • Element : The element that can be used by the visitor, it must define an Accept property to receive the visitor object. This is the key to implementing the visitor pattern

file

It can be seen that in order to transfer the operation right to Visitor , the core is that the element must implement a Accept function and throw this object to Visitor :

 class ConcreteElement implements Element {
  public accept(visitor: Visitor) {
    visitor.visit(this)
  }
}

From the above code, we can see such a link: Element receives the Visitor object through the accept function, and throws its own instance to the Visitor 's visit function, so that we can get the object instance in the Visitor's visit method to complete the object operation

4. Implementation and pseudocode

In this example, the visitor schema adds support for XML file export to the geometry image hierarchy

4.1 Declare a set of "access" methods in the visitor interface, corresponding to each specific element class in the program

 interface Visitor {
  visitDot(d: Dot): void;
  visitCircle(c: Circle): void;
  visitRectangle(r: Rectangle): void;
}

4.2 Declare the element interface. If the program already has an element class hierarchy interface, an abstract "receive" method can be added to the hierarchy base class. The method must accept a visitor object as a parameter

 interface Shape {
  accept(v: Visitor): void;
}

4.3 Implement the receiving method in all concrete element classes, the element class can only interact with the visitor through the visitor interface, but the visitor must know all the concrete element classes, because these classes are referenced as parameter types in the visitor method

 class Dot implements Shape {
  public accept(v: Visitor): void {
   return v.visitDot(this)
  }
}

class Circle implements Shape {
  public accept(v: Visitor): void {
   return v.visitCircle(this)
  }
}

class Rectangle implements Shape {
  public accept(v: Visitor): void {
    return v.visitRectangle(this)
  }
}

4.4 Create a concrete visitor class and implement all visitor methods

 class XMLExportVisitor implements Visitor {
    visitDot(d: Dot): void {
      console.log(`导出点(dot)的 ID 和中心坐标`);
    }
    visitCircle(c: Circle): void {
      console.log(`导出圆(circle)的 ID 、中心坐标和半径`);
    }
    visitRectangle(r: Rectangle): void {
      console.log(`导出长方形(rectangle)的 ID 、左上角坐标、宽和长`);
    }
}

4.5 The client must create a visitor object and pass it to the element via the "receive" method

 const application = (shapes:Shape[],visitor:Visitor) => {
  // ......
   for (const shape of  allShapes) {
      shape.accept(visitor);
    }
  // ......
}
    
const allShapes = [
    new Dot(),
    new Circle(),
    new Rectangle()
];

const xmlExportVisitor = new XMLExportVisitor();
application(allShapes, xmlExportVisitor);

4.6 Full code preview

 interface Visitor {
    visitDot(d: Dot): void;
    visitCircle(c: Circle): void;
    visitRectangle(r: Rectangle): void;
}

interface Shape {
   accept(v: Visitor): void;
}

class Dot implements Shape {
  public accept(v: Visitor): void {
     return v.visitDot(this)
  }
}

class Circle implements Shape {
  public accept(v: Visitor): void {
    return v.visitCircle(this)
  }
}

class Rectangle implements Shape {
  public accept(v: Visitor): void {
    return v.visitRectangle(this)
  }
}

class XMLExportVisitor implements Visitor {
    visitDot(d: Dot): void {
      console.log(`导出点(dot)的 ID 和中心坐标`);
    }
    visitCircle(c: Circle): void {
      console.log(`导出圆(circle)的 ID 、中心坐标和半径`);
    }
    visitRectangle(r: Rectangle): void {
      console.log(`导出长方形(rectangle)的 ID 、左上角坐标、宽和长`);
    }
}

const allShapes = [
    new Dot(),
    new Circle(),
    new Rectangle()
];

const application = (shapes:Shape[],visitor:Visitor) => {
  // ......
for (const shape of  allShapes) {
    shape.accept(visitor);
  // .....
}
    
const xmlExportVisitor = new XMLExportVisitor();
application(allShapes, xmlExportVisitor);

5. Advantages and disadvantages of visitor pattern

Advantage:

  • Open closed principle. You can introduce new behaviors that are performed on objects of different classes without modifying those classes
  • Single Responsibility Principle to move different versions of the same behavior into the same class

insufficient:

  • Every time you add or remove a class from the element hierarchy, you update all visitors
  • When visitors interact with an element, they may not have the necessary permissions to access the element's private member variables and methods

袋鼠云数栈UED
277 声望33 粉丝

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。