likes
comments
collection
share

提升前端代码质量之SOLID设计原则-LSP里氏替换原则

作者站长头像
站长
· 阅读数 17

前言

在提升前端代码质量之SOLID设计原则系列中文章提升前端代码质量之SOLID设计原则-SRP单一职责已经简单介绍了SOLID五种设计模式以及我们为什么要在项目中使用SOLID,不清楚可以移步到上文,本文不再赘述。

SOLID相关的文章索引:

本文结合代码(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,否则就重新请求接口。子类BaseRetransmissionfn的返回结果做了增强,它替代父类的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
评论
请登录