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