乐趣区

关于前端:聊聊OOP中的设计原则以及访问者模式

一  设计准则 (SOLID)

1.  S – 繁多职责准则(Single Responsibllity Principle)

1.1  定义

一个类或者模块只负责实现一个职责(或性能), 认为“对象应该仅具备一种繁多性能”的概念, 如果一个类蕴含了两个或两个以上业务没有关联的性能,就被认为是职责不够繁多,能够差分成多个性能繁多的类

1.2 举个栗子

Employee 类外面蕴含了多个不同的行为, 违反了繁多指摘准则

通过拆分出 TimeSheetReport 类, 依赖了 Employee 类, 遵循繁多指摘准则

2.  O – 凋谢敞开准则(Open-Closed Principle)

2.1 定义

软件实体(包含类、模块、性能等)应该对扩大凋谢,然而对批改敞开, 满足以下两个个性

  • 对扩大凋谢

模块对扩大凋谢,就意味着需要变动时,能够对模块扩大,使其具备满足那些扭转的新行为

  • 对批改敞开

模块对批改敞开,示意当需要变动时,应该尽量在不批改源代码的根底下面扩大性能

2.2 举个栗子

在订单中须要依据不同的运输形式计算运输成本

Order

类中计算运输成本,如果后续再减少新的运输形式,就须要批改 Order 原来的办法getShippingCost() , 违反了OCP

依据 多态 的思维,能够将 shipping 形象成一个类, 后续新增运输形式, 毋庸批改 Order 类 原有的办法 ,
只须要在减少一个 Shipping 的派生类就能够了

3.  L – 里氏替换准则(Liskov Substitution Principle)

3.1 定义

应用父类的中央都能够用子类代替,子类可能兼容父类

  • 子类办法的参数类型应该比父类办法的参数类型更形象或者说范畴更广
  • 子类办法的返回值类型应该比父类办法的返回值类型更具体或者说范畴更小

3.2 举个栗子

子类办法的参数类型应该比父类办法的参数类型更形象或者说范畴更广
演示 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);
  • 子类办法的返回值类型应该比父类办法的返回值类型更具体或者说范畴更小
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();
  • 子类不应该强化前置条件
  • 子类不应该弱化后置条件

4.  I – 接口隔离准则(Interface Segregation Principle)

4.1 定义

客户端不应该依赖它不须要的接口, 一个类对另一个类的依赖应该建设在 最小的接口

4.2 举个栗子

类 A 通过接口 I 依赖类 B,类 C 通过接口 I 依赖类 D,如果接口 I 对于类 A 和类 B 来说不是最小接口,则类 B 和类 D 必须去实现他们不须要的办法

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();
  }
}

将臃肿的接口 I 拆分为独立的几个接口,类 A 和类 C 别离与他们须要的接口建设依赖关系

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 事实中的栗子

以电动自行车为例

一般的电动自行车并没有定位和查看历史行程的性能,但因为实现了接口 ElectricBicycle,所以必须实现接口中本人不须要的办法。更好的形式是进行拆分

5.   D – 依赖倒置准则

5.1 定义

依赖一个形象的服务接口,而不是去依赖一个具体的服务执行者,从依赖具体实现转向到依赖形象接口,倒置过去
在软件设计中能够将类分为两个级别:高层模块 , 低层模块 , 高层模块不应该依赖低层模块,两者都应该依赖其形象。 高层模块 指的是调用者,低层模块 指的是一些根底操作

依赖倒置 基于这个事实:相比于实现细节的多变性,形象的内容要稳固的多

5.2 举个栗子

SoftwareProject类间接依赖了两个低级类, FrontendDeveloperBackendDeveloper, 而此时来了一个新的低层模块, 就要批改 高层模块 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();}
}

能够遵循依赖倒置准则, 因为 FrontendDeveloper 和 BackendDeveloper是类似的类, 能够形象出一个 develop 接口, 让 FrontendDeveloperBackendDeveloper 去实现它, 咱们不须要在 SoftwareProject 类中以繁多形式初始化 FrontendDeveloper 和 BackendDeveloper,而是将它们作为一个列表来遍历它们,别离调用每个 develop() 办法

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();
    });
  }
}

二  访问者模式 (Visitor Pattern)

1.  用意

示意一个作用于某对象构造中的各元素的操作。它使你能够在不扭转各元素的类的前提下定义作用于这些元素的新操作

  • Visitor的作用,即 作用于某对象构造中的各元素的操作,也就是 Visitor 是用于操作对象元素的
  • 它使你能够在不扭转各元素的类的前提下定义作用于这些元素的新操作 也就是说,你能够只修Visitor 自身实现新操作的定义,而不须要批改本来对象, Visitor 设计微妙之处, 就是将对象的操作权移交给了 Visitor

2. 场景

  • 如果你须要对一个简单对象构造(例如对象树)中的所有元素执行某些操作,可应用访问者模式
  • 访问者模式通过在访问者对象中为多个指标类提供雷同操作的变体,让你能在属于不同类的一组对象上执行同一操作

    3.  访问者模式构造

  • Visitor:访问者接口
  • ConcreteVisitor:具体的访问者
  • Element: 能够被访问者应用的元素,它必须定义一个 Accept 属性,接管 visitor 对象。这是实现访问者模式的要害

能够看到,要实现操作权转让到 Visitor,外围是元素必须实现一个 Accept 函数,将这个对象抛给 Visitor

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

从下面代码能够看出这样一条链路:Element 通过 accept函数接管到 Visitor 对象,并将本人的实例抛给 Visitor 的 visit函数,这样咱们就能够在 Visitor 的 visit 办法中拿到对象实例,实现对对象的操作

4 . 实现形式以及伪代码

在本例中,访问者 模式为几何图像层次结构增加了对于 XML 文件导出性能的反对

4.1  在访问者接口中申明一组“拜访”办法,别离对应程序中的每个具体元素类

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

4.2  申明元素接口。如果程序中已有元素类档次接口,可在层次结构基类中增加形象的“接管”办法。该办法必须承受访问者对象作为参数

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

4.3  在所有具体元素类中实现接管办法, 元素类只能通过访问者接口与访问者进行交互, 不过访问者必须通晓所有的具体元素类,因为这些类在访问者办法中都被作为参数类型援用

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 创立一个具体访问者类并实现所有的访问者办法

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  客户端必须创立访问者对象并通过“接管”办法将其传递给元素

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 残缺代码预览

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. 访问者模式优缺点

劣势:

  • 开闭准则。你能够引入在不同类对象上执行的新行为,且无需对这些类做出批改
  • 繁多职责准则 可将同一行为的不同版本移到同一个类中

有余:

  • 每次在元素层次结构中增加或移除一个类时, 你都要更新所有的访问者
  • 在访问者同某个元素进行交互时,它们可能没有拜访元素公有成员变量和办法的必要权限
退出移动版