提升前端代码质量之SOLID设计原则-LSP里氏替换原则
前言
在提升前端代码质量之SOLID设计原则系列中文章提升前端代码质量之SOLID设计原则-SRP单一职责已经简单介绍了SOLID
五种设计模式以及我们为什么要在项目中使用SOLID
,不清楚可以移步到上文,本文不再赘述。
SOLID
相关的文章索引:
- 提升前端代码质量之SOLID设计原则-SRP单一职责
- 提升前端代码质量之SOLID设计原则-OCP开放封闭原则
- 提升前端代码质量之SOLID设计原则-LSP里氏替换原则
- 提升前端代码质量之SOLID设计原则-ISP接口隔离原则
- 提升前端代码质量之SOLID设计原则-DIP依赖倒置原则
本文结合代码(React
)主要讲解 SOLID 原则中的LSP - 里氏替换原则
。
里氏替换原则(Liskov Substitution Principle)
子类可以扩展父类的功能,但不能改变父类原有的功能,而且不会产生任何错误。
符合里式替换原则的代码是什么样的呢?通过一个例子来看一下。
// 符合LSP原则
class Retransmission {
public async retry(fn: () => Promise<any>) {
let response = null;
for (let count = 0;; count++) {
try {
response = await fn();
} catch (error) {
throw Error("error");
}
if (response?.code === 200) {
return response;
}
}
}
}
class BaseRetransmission extends Retransmission {
public async retry(fn: () => Promise<any>) {
let response = null;
for (let count = 0;; count++) {
try {
response = await fn();
} catch (error) {
throw Error("error");
}
if (response?.code === 200) {
response.timerCount = count;
return response;
}
}
}
}
上面是一个接口重传的例子,当然这里的实现也是非常的简洁。父类中Retransmission
接口重传的方法retry
中如果接口的值等于200
就返回response
,否则就重新请求接口。子类BaseRetransmission
对fn
的返回结果做了增强,它替代父类的retry
也就是new Retransmission().retry()
完全是没有问题的。但是如果反回来就会出现问题:
// 违反LSP原则
class BaseRetransmission {
public async retry(fn: (...arg) => Promise<any>) {
let response = null;
for (let count = 0;; count++) {
try {
response = await fn();
} catch (error) {
throw Error("error");
}
if (response?.code === 200) {
response.timerCount = count;
return response;
}
}
}
}
class Retransmission extends BaseRetransmission {
public async retry(fn: (...arg) => Promise<any>) {
let response = null;
for (let count = 0;; count++) {
try {
response = await fn();
} catch (error) {
throw Error("error");
}
if (response?.code === 200) {
return response;
}
}
}
}
现在是BaseRetransmission
是父类,而Retransmission
是子类,此时如果new Retransmission().retry()
用来替换父类BaseRetransmission
中的retry
就会出现异常,因为子类在返回结果中缺少timerCount
,子类对父类的返回值做了删减,这会增加程序发生崩溃的风险。
那如果在React组件是怎样存在的呢? 我们先来看一个例子:
// src\page\LSP\CustomInput.tsx
import React from 'react';
interface IInputProps
extends React.InputHTMLAttributes<HTMLElement> {
label?: string;
}
const CustomInput = (props: IInputProps) => {
const { label = '', value, onChange } = props
return <>
<div style={{ display: 'flex' }}>
<span>{label}:</span>
<input
type='search'
onChange={onChange}
value={value}
required
placeholder='请输入'
style={{
border: '1px solid #d9d9d9',
borderRadius: '6px',
width: '100%',
flex:'1'
}}
/>
</div>
</>
}
export default CustomInput;
上面的例子比较简单,我们对原生的input
封装成了一个组件。这里的CustomInput
相当于一个子类,他扩展了真实的input
,此时的input
代表一个超类,在CustomInput
组件内部可能只需要一个props
也就是label属性
,但是实际上我们在原生的input
标签是是需要很多的props
的,这些属性可能我们很多都没有意识到。
为了保证我们封装的组件满足LSP
里式替换原则,我们需要确保也就是从超类InputHTML
上扩展其他属性,因为我们可能还需要使用到自己封装组件特定的props
之外来自于超类中的props
,所以我们改造一下上面的代码,解构来自超类props
:
import React from 'react';
...
const { label = '', value, onChange, ...restProps } = props
return <>
<div style={{ display: 'flex' }}>
...
{...restProps}
/>
</div>
</>
}
export default CustomInput;
虽然这个例子很简单,但是当我们在组合比较复杂的组件时候却非常有用,大部分公司内部都会封装一些符合自身业务的组件或者组件库。这些组件库基本都会遵循里氏替换原则,比如基于 Ant Design 开发的模板组件系列ProComponent。
最后
对于LSP
里氏替换原则我总结以下几点:
符合LSP
- 子类可以增加自己特有的属性和方法
- 子类在覆写父类方法的时候,输入输出必须遵从父类的约定。输入相同或者更宽松,输出相同或者更严格。比如子类参数的类型包含父类参数的类型。
违背LSP
- 子类实现的功能与父类不一样。
- 子类违背父类对输入、输出、异常的约定。比如父类在输入为空的时候不做处理,子类在输入为空的时候抛出异常。
相关的代码已经整理成一个仓库上传至github,感兴趣可以结合仓库代码阅读: REACT-SOLID
转载自:https://juejin.cn/post/7194061916313468988