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
By splitting out the TimeSheetReport class, it relies on the Employee class and follows the single blame principle
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
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 .
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
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
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
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。