TypeScript - 开发圣经SOLID设计原则

1. 什么是SOLID设计原则

SOLID设计原则 代指五大设计原则,这些设计原则的首字母缩写刚好能组成SOLID这个英文单词。因此,我们就简称这五大设计原则为SOLID设计原则。这五大设计原则分别是:

Single Responsibility Principle - 单一职责原则

Open-Closed Principle - 开闭原则

Liskov Substitution Principle - 里氏替换原则

Interface Segregation Principle - 接口分离原则

Dependency Inversion Principle - 依赖倒置原则

2. 为什么要遵循SOLID设计原则

遵循SOLID设计原则很容易写出高内聚、低耦合 的代码。并且在实际开发中,同一项目可能经手多个开发。如果每个开发都有不同的开发理念的话,就很容易让项目代码变成屎山代码。于是SOLID设计原则制定了一个统一的、基础的规则。每个开发只需要遵循SOLID设计原则,就可以使写出来的代码大体保持一致,降低项目代码成为屎山代码的概率

3. 关于五大设计原则

Single Responsibility Principle - 单一职责原则

单一职责原则是指一个类只实现单一的一种功能。可以想象到一个类如果职责过多,会使得代码难以复用。其实单一职责原则就是颗粒度的细分。

typescript 复制代码
class HotelOrder {
  protected amount: number;
  constructor(amount: number) {
   this.amount = amount;
  }
  pay(){
   //调用支付接口
   console.log('amount:',this.amount);
  },
  //其它一些实现的方法...
}

const hotelOrder = new HotelOrder(100);
hotelOrder.pay();

我在HotelOrder类中实现了一个pay方法,目前看来没有什么问题。但如果随着业务增多,现在需要新增一个TicketOrder类,那么就只能把pay方法再复制一份到TicketOrder类中。一旦支付逻辑发生变更,那么就得同时维护多份代码。

typescript 复制代码
//新定义支付模块
class Payment {
  pay(amount:number){
   console.log('amount:',amount);
  }
}

class HotelOrder {
  protected amount: number;
  constructor(amount: number) {
   this.amount = amount;
  }
  getAmount(){
   return this.amount;
  }
  //其它一些实现的方法...
}

class TicketOrder {
  protected amount: number;
  constructor(amount: number) {
   this.amount = amount;
  }
  getAmount(){
   return this.amount;
  }
  //其它一些实现的方法...
}
const payment = new Payment();

const hotelOrder = new HotelOrder(100);
const ticketOrder = new TicketOrder(200);

payment.pay(hotelOrder.getAmount());
payment.pay(ticketOrder.getAmount());

因此按照单一职责原则,一个类只做单一的一种功能。HotelOrder就只应该负责酒店订单的业务,需要把支付业务给从HotelOrder中抽出,使得能够共用支付。

Open-Closed Principle - 开闭原则

开闭原则指的是对扩展开发,对修改关闭。简单来说,就是不轻易改动原有代码,而是在原有代码上进行扩展。

typescript 复制代码
class Payment {
  async pay(type: String, amount: number) {
   if (type === 'hotel') {
    const res = await hotelPay(amount);
   } else if (type === 'ticket') {
    const res = await ticketPay(amount);
   }
  }
}

我在Payment类中使用if区分了调用的接口。但现在如果需要新增一个支付方式的话,那么又得在原有基础上新增一个else if判断。使得每增加一种type,都需要修改一次原有的代码,没有做到对修改关闭。由于发生了修改,代码也得重新进行测试,增加了很多的工作量。要解决这种情况,可以使用驱动注入的方式重构Payment类

typescript 复制代码
interface DriverInterface {
  pay(amount: number):void;
}

class Payment {
  async pay(driver:DriverInterface, amount: number) {
   driver.pay(amount);
  }
}

class HotelPayDriver implements DriverInterface{
  async pay(amount:number){
   await hotelPay(amount)
  }
}

class TicketPayDriver implements DriverInterface{
  async pay(amount:number){
   await ticketPay(amount)
  }
}

const payment = new Payment();
const hotelPayDriver = new HotelPayDriver();
const ticketPayDriver = new TicketPayDriver();

payment.pay(hotelPayDriver,100);
payment.pay(ticketPayDriver,200);

我新建了HotelPayDriver类与TicketPayDriver类作为驱动,里面实现了各自的pay方法。Payment类只负责调用驱动的pay方法,而不需要关心pay方法的实现。Payment类不需要每多一种type都要修改一次,也不需要修改原有驱动,完美遵循了开闭原则。

Liskov Substitution Principle - 里氏替换原则

里氏替换原则的定义是子类可以替换父类,并且替换后系统结果一致

这里对于系统结果一致的理解有两点需要注意:

1. 子类继承的方法的输出结果必须与父类一致

2. 子类不对继承来的方法、属性产生额外副作用

输出结果一致很好理解,而这里的额外副作用是比如:子类重写了继承得来的方法,并使得调用属于父类的属性、方法不与原本一致,例如代码所示:

typescript 复制代码
class BaseClass {
  protected addCount: number;
  constructor() {
   this.addCount = 0;
  }
  sum(a:number,b:number){
   this.addCount++;
   return a + b;
  }
  getCount(){
   return this.addCount;
  }
}

class SubClass extends BaseClass{
  //重写了父类的sum方法,并导致了产生额外副作用
  sum(a:number,b:number){
   this.addCount += 2;
   return a + b;
  }
}

const base = new BaseClass();
const sub = new SubClass();

let baseSumResult = base.sum(1,2);
let subSumResult = sub.sum(1,2);

let baseCountResult = base.getCount();
let subCountResult = sub.getCount();

//返回结果一致
console.log("baseSumResult:",baseSumResult);
console.log("subSumResult:",subSumResult);

//产生了额外副作用导致了调用getCount结果不一致
console.log("baseCountResult:",baseCountResult);
console.log("subCountResult:",subCountResult);

这两点也就规定了 不要轻易的重写父类方法 即使要重写也要保证输出结果一致和不产生副作用

Interface Segregation Principle - 接口分离原则

接口分离原则规定了接口实现方不应该被迫实现用不上的方法。在一个接口定义的方法应该足够少、接口颗粒度尽可能小,如果不遵循接口分离原则就会导致出现冗余代码,让别人看的一脸懵B,不知道这个方法在这里是干啥的,比如以下代码示例:

typescript 复制代码
//正确做法是分别定义getOrderNo方法的接口和removeOrder方法的接口
interface OrderActionInterface {
  getOrderNo():string;
  removeOrder(orderNo: string):void;
}

class Log {
  // LogCreateOrderNo只需要一个实例具有getOrderNo方法,但开发过程中可能下意识使用了OrderActionInterface进行约束
  LogCreateOrderNo(order:OrderActionInterface){
   console.log(order.getOrderNo());
  }
}

// 由于LogCreateOrderNo的参数约束,导致HotelOrder被迫实现了OrderActionInterface里约束的所有方法,但其实只有一个获取订单号方法是有用的
class HotelOrder implements OrderActionInterface{
  getOrderNo(){
   return 'a108';
  };
  // 需要额外实现用不上的removeOrder方法
  removeOrder(orderNo: string){};
}

Dependency Inversion Principle - 依赖倒置原则

依赖倒置原则的定义是高阶模块不应依赖低阶模块,两者都应依赖于抽象

这句话简单来说就是:

1. 不应该在一个类中去new另一个类,而是应该使用传参的形式注入类实例

2. 两者都应依赖于抽象:应该定义一个接口来约束这个传入的类实例

typescript 复制代码
//模拟请求
function verifyRequest(code:string){
  return new Promise((resolve) => {
   resolve({
    code:200,
    success: true,
    message: code + '验证通过'
   })
  })
}

//定义接口
interface SendInterface {
  send():string;
}

//邮件类
class Mail{
  send(){
   return "Mail-code"
  }
}

//手机类
class Phone{
  send(){
   return "Phone-code"
  }
}

//验证消息类
class InfoVerification {
  //传入实例的约束不应该是具体的实现类,例如mail:Mail,这会导致其它类的实例无法传入
  async verify(sendObj: SendInterface){
   //不应该在内部创建外部类实例,例如const mail = new Mail(),这会导致扩展困难
   const res = await verifyRequest(sendObj.send());
   console.log("res:",res);
  }
}

const mail = new Mail();
const phone = new Phone();
const infoVerification = new InfoVerification();

//可以传入任何含有send方法的实例,方便扩展
infoVerification.verify(mail);
infoVerification.verify(phone);
相关推荐
golang学习记5 小时前
从0死磕全栈第4天:使用React useState实现用户注册功能
前端
bug_kada5 小时前
深入理解事件捕获与冒泡(详细版)
前端·javascript
wanghao6664555 小时前
如何从chrome中获取会话id
前端·chrome
As33100105 小时前
Chrome 插件开发入门:打造个性化浏览器扩展
前端·chrome
小妖6665 小时前
怎么用 tauri 创建一个桌面应用程序(Electron)
前端·javascript·electron
2501_930104045 小时前
Chrome 插件开发入门:从基础到实践
前端·chrome
曾经的三心草6 小时前
微服务的编程测评系统22-项目部署结束
微服务·云原生·架构
IT_陈寒7 小时前
Python异步编程的7个致命误区:90%开发者踩过的坑及高效解决方案
前端·人工智能·后端