概述

本世纪初,美国计算机专家和作者 Robert Cecil Martin 针对 OOP 编程,提出了可以很好配合的五个独立模式;后由重构等领域的专家 Michael Feathers 根据其首字母组合成 SOLID 模式,并逐渐广为人知,直至成为了公认的 OOP 开发的基础准则。

  • S – Single Responsibility Principle 单一职责原则
  • O – Open/Closed Principle 开放/封闭原则
  • L – Liskov Substitution Principle 里氏替换原则
  • I – Interface Segregation Principle 接口隔离原则
  • D – Dependency Inversion Principle 依赖倒转原则

人称 Uncle Bob 的 Robert Cecil Martin 又是一位大叔 Michael Feathers,实际上本次中老年专场还不止于此

作为一门弱类型并在函数式和面向对象之间左右摇摆的语言,JavaScript 中的 SOLID 原则与在 Java 或 C# 这样的语言中还是有所不同的;不过 SOLID 作为软件开发领域通用的原则,在 JavaScript 也还是能得到很好的应用。

React 应用就是由各种 React Component 组成的,本质上都是继承自 React.Component 的子类,也可以靠继承或包裹实现灵活的扩展。虽然不应生硬的套用概念,但在 React 开发过程中延用并遵守既有的 SOLID 原则,能让我们创建出更可靠、更易复用,以及更易扩展的组件。

注:文中各定义中提到的“模块”,换做“类”、“函数”或是“组件”,都是一样的意义。

单一职责(Single responsibility)

每个模块应该只专注于做一件事情

该原则意味着,如果承担的职责多于一个,那么代码就具有高度的耦合性,以至其难以被理解,扩展和修改。

在 OOP 中,如果一个类承担了过多职责,一般的做法就是将其拆解为不同的类:

class CashStepper {constructor() {this.num = 0;}plus() {this.num++;}minus() {this.num--;}checkIfOverage() {if (this.num > 3) {console.log('超额了');} else {console.log('数额正常');}}
}const cs = new CashStepper;
cs.plus();
cs.plus();
cs.plus();
cs.plus();
cs.checkIfOverage();
复制代码

很明显,原先这个类既要承担步进器的功能,又要关心现金是否超额,管的事情太多了。

应将其不同的职责提取为单独的类,如下:

class Stepper {constructor() {this.num = 0;}plus() {this.num++;}minus() {this.num--;}
}class CashOverageChecker {check(stepper) {if (stepper.num > 3) {console.log('超额了');} else {console.log('数额正常');}}
}const s = new Stepper;
s.plus();
s.plus();
s.plus();
s.minus();
s.plus();console.log('num is', s.num);const chk = new CashOverageChecker;
chk.check(s);
复制代码

如此就使得每个组件可复用,且修改某种逻辑时不影响其他逻辑。

而在 React 中,也是类似的做法,应尽可能将组件提取为可复用的最小单位:

class ProductsStepper extends React.Component {constructor(props) {super(props);this.state = {value: 0};}render() {return (this.props.onhand > 0? <div><button ref="minus" onClick={this.onMinus.bind(this)}> - </button><span ref="val">{this.state.value}</span><button ref="plus"onClick={this.onPlus.bind(this)}> + </button></div>: "无货");}onMinus() {this.setState({value: this.state.value - 1});}onPlus() {this.setState({value: this.state.value + 1});}
}ReactDOM.render(<ProductsStepper onhand={1} />,document.getElementById('root')
);
复制代码

同样是一个步进器的例子,这里想在库存为 0 时做出提示,但是逻辑和增减数字糅杂在了一起;如果想在项目中其他地方只想复用一个数字步进器,就要额外捎上很多其他不相关的业务逻辑,这显然是不合理的。

解决的方法同样是提取成各司其职的单独组件,比如可以借助高阶组件(HOC)的形式:

class Stepper extends React.Component {constructor(props) {super(props);this.state = {value: 0};}render() {return (<div><button ref="minus" onClick={this.onMinus.bind(this)}> - </button><span ref="val">{this.state.value}</span><button ref="plus"onClick={this.onPlus.bind(this)}> + </button></div>);}onMinus() {this.setState({value: this.state.value - 1});}onPlus() {this.setState({value: this.state.value + 1});}
}const HOC = (StepperComp)=>{return (props)=>{if (props.onhand > 0) {return <StepperComp />;} else {return "无货";}}
};const ProductsStepper2 = HOC(Stepper);ReactDOM.render(<ProductsStepper2 onhand={1} />,document.getElementById('root2')
);
复制代码

这样,项目中其他地方就可以直接复用 Stepper,或者借助不同的 HOC 扩展其功能了。

关于 HOC 的更多细节可以关注文章结尾公众号中的其他文章。

“单一职责”原则类似于 Unix 中提倡的 “Do one thing and do it well” ,理解起来容易,但做好不一定简单。

从经验上来讲,这条原则可以说是五大原则中最重要的一个;理解并遵循好该原则一般就可以解决大部分的问题。

开放/封闭(Open/closed)

模块应该对扩展开放,而对修改关闭

换句话说,如果某人要扩展你的模块,应该可以在不修改模块本身源代码的前提下进行。

例如:

let iceCreamFlavors=["巧克力","香草"];
let iceCreamMaker={makeIceCream (flavor) {if(iceCreamFlavors.indexOf(flavor)>-1){console.log(`给你${flavor}口味的冰淇淋~`)}else{console.log("没有!")}}
};
export default iceCreamMaker;
复制代码

对于这个模块,如果想定义并取得新的口味,显然无法在不修改源代码的情况下完成;可改为如下形式:

let iceCreamFlavors=["巧克力","香草"];
let iceCreamMaker={makeIceCream (flavor) {if(iceCreamFlavors.indexOf(flavor)>-1){console.log(`给你${flavor}口味的冰淇淋~`)}else{console.log("没有!")}},addFlavor(flavor){iceCreamFlavors.push(flavor);}
};
export default iceCreamMaker;
复制代码

通过增加 addFlaver() 方法重新定义此模块,就满足了“开放/封闭”原则,在外界需要扩展时(增加新口味)并不用修改原来的内部实现。

具体到 React 来说,提倡通过不同组件间的嵌套实现聚合的行为,这会在一定程度上防止频繁对已有组件的直接修改。自己定义的组件也应该谨记这一原则,比如在一个 <RedButton> 里包裹 <Button> ,并通过修改 props 来实现扩展按钮颜色的功能,而非直接找到 Button 的源码并增加颜色逻辑。

另外,“单一职责”中的两个例子也可以很好地解释“开放/封闭”原则,职责单一的情况下,通过继承或包裹就可以扩展新功能;反之就还要回到原模块的源代码中修修补补,让局势更混乱。

君子纳于言而敏于行,模块纳于改代码而敏于扩展。

里氏替换(Liskov substitution)

程序中的对象都应该能够被各自的子类实例替换,而不会影响到程序的行为

作为五大原则里唯一以人名命名的,其实是直接引用了更厉害的两位大姐大的成果:

芭芭拉·利斯科夫(Barbara Liskov),图灵奖得主、约翰·冯诺依曼奖得主,于 1987 年提出里氏替换理论的设想 微软全球资深副总裁周以真(Jeannette M. Wing)博士,在 1994 年与 Liskov 一起发表了里氏替换原则

类的继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

里氏替换原则通俗的来讲就是:子类对象能够替换其基类对象被使用;引申开来就是 子类可以扩展父类的功能,但不能改变父类原有的功能

"龙生龙,凤生凤,杰瑞的儿子会打洞" 关我毛事...

用于解释这个原则的经典例子就是长方形和正方形:

class Rectangle {set width(w) {this._w = w;}set height(h) {this._h = h;}get area() {return this._w * this._h;}
}const r = new Rectangle;
r.width = 2;
r.height = 5;
console.log(r.area); //10class Square extends Rectangle {set width(w) {this._w = this._h = w;}set height(h) {this._w = this._h = h;}
}const s = new Square;
s.width = 2;
s.height = 5;
console.log(s.area); //25
复制代码

对于正方形的设置,到底以宽还是高为准,上面的代码就产生了歧义;并且关键在于,如果基于现有的 API(允许分别设置宽高)有一个 “设置宽2高5就能得到面积10” 的假设,则正方形子类就无法实现该假设,而这样的实现就是违背里氏替换原则的不良实践。

一种可行的更改方案为:

class Rectangle2 {constructor(width, height) {this._w = width;this._h = height;}get area() {return this._w * this._h;}
}const r2 = new Rectangle2(2, 5);
console.log(r2.area); //10class Square2 extends Rectangle2 {constructor(side) {super(side, side);}
}const s2 = new Square2(5);
console.log(s2.area); //25
复制代码

通过重写父类的方法来完成新的功能,写起来虽然简单,但是整个继承体系的可复用性会比较差。

在 React 中,大部分时候是靠父子元素正常的组合嵌套来工作,而非继承,天然的就有了无法修改被包裹组件细节的一定保障;组件间互相的接口就是 props,通过向下传递增强或修改过的 props 来实现通信。这里关于里氏替换原则的意义很好理解,比如类似 <RedButton> 的组件,除了扩展样式外不会破坏且应遵循被包裹的 <Button> 的点击功能。

再举一个直观点的例子就是:如果一个地方放置了一个 Modal 弹窗,且该弹窗右上角有一个可以关闭的 [close] 按钮;那么无论以后在同样的位置替换 Modal 的子类或是用 Modal 包裹组合出来的组件,即便不再有 [close] 按钮,也要提供点击蒙版层、ESC 快捷键等方式保证能够关闭,这样才能履行 “能弹出弹窗且能自主关闭” 的原有契约,满足必要的使用流程。

接口隔离(Interface segregation)

多个专用的接口比一个通用接口好

在一些 OOP 语言中,接口被用来描述类必须实现的一些功能。原生 JS 中是没有这码事的,这里用 TypeScript 来举例说明:

interface IClock {currentTime: Date;setTime(d: Date);
}interface IAlertClock {alertWhenPast: Function
}class Clock implements IClock, IAlertClock {currentTime: Date;setTime(d: Date) {this.currentTime = d;console.log(this.currentTime);}alertWhenPast() {if ( this.currentTime <= Date.now() ) {console.log('time has pasted!'); }}constructor() {}
}const c = new Clock;
c.setTime( Date.now() - 2000 );
c.alertWhenPast();// 1527227168790
// "time has pasted!"
复制代码

一个时钟要能够 setTime,还要能够获得 currentTime,这些是核心功能,放在 IClock 接口中;只要实现了 IClock 接口,就是合法的时钟。

其他接口被认为是可选功能或增强包,根据需要分别实现,互不干扰;当然 TS 接口中有可选的语法,在此仅做概念演示,不展开说明。

而 React 类似中的做法是靠 PropTypes 的必选/可选设定,以及搭配 DefaultProps 实现的。

class Clock extends React.Component {static propTypes = {hour: PropTypes.number.isRequired,minute: PropTypes.number.isRequired,second: PropTypes.number,onClick: PropTypes.func};static defaultProps = {onClick: null};constructor(props) {super(props);}render() {return <div onClick={this._onClick.bind(this)}>{this.props.hour}:{this.props.minute}{this.props.second ? ':' + this.props.second: null}</div>;}_onClick() {if (this.props.onClick) {this.props.onClick(this.props.hour)}}
}ReactDOM.render(<Clock hour={20} minute={33} />,document.querySelector('.root')
);ReactDOM.render(<Clock hour={18} minute={23} second={50} />,document.querySelector('.root2')
);ReactDOM.render(<Clock hour={10} minute={15} onClick={hour=>alert("hour is "+hour)} />,document.querySelector('.root3')
);
复制代码

只需要 hour 和 minute,一个最基本的时钟就能显示出来;而是否显示秒数、是否在点击时响应等,就都归为可选的接口了。

依赖倒转(Dependency inversion)

依赖抽象,而不是依赖具体的实现

解释起来就是,一个特定的类不应该直接依赖于另外一个类,但是可以依赖于这个类的抽象(接口)。

这和同样闻名已久的 “控制反转(Inversion of Controls)” 概念其实是一回事。

一个例子,渲染传入的列表而不负责构建具体的项目:

const Team = ({name,points})=>(<li>{name}'s points is {points}</li>
);const List1 = ({data})=>(<ul>{data.map(team=>(<Team key={team.name} name={team.name} points={team.points} />))}</ul>
);ReactDOM.render(<List1 data={[{name:"广州队",points:15},{name:"武汉队",points:40},{name:"新疆队",points:30}]} />,document.getElementById('root')
);
复制代码

看起来问题不大甚至一切正常,不过如果有另一个页面也使用 List1 组件时,希望使用另一种增强版的列表项,就要去改列表的具体实现甚至再弄一个另外的列表出来了。

const TeamWithLevel = ({name,points})=>(<li>⚽️ {name} - {points > 30? <strong>{ points }</strong>: points > 20? <em>{ points }</em>: points }</li>
);const List1 = ({data})=>(<ul>{data.map(team=>(//???))}</ul>
);
复制代码

此处用“依赖倒转”原则来处理的话,可以解开两个“依赖具体而非抽象”的点,分别是列表项的组件类型以及列表项上的属性。

const List2 = ({data, ItemComp})=>(<ul>{data.map(team=>(<ItemComp key={team.name} {...team} />))}</ul>
);ReactDOM.render(<List2 data={[{name:"河北队",points:20},{name:"福建队",points:30},{name:"香港队",points:40}]}ItemComp={TeamWithLevel}/>,document.getElementById('root2')
);
复制代码

如此一来,<List2> 就成了可以真正通用在各种页面的一个较通用的组件了;比如电商场景的已选货品列表、后台管理报表筛选项等场景,都是高度适用此方案的。

总结

面向对象思想在 UI 层面的自然延伸,就是各种界面组件;用 SOLID 指导其开发同样稳妥,会让组件更健壮可靠,并拥有更好的可扩展性

和设计模式一样,这些“原则”也都是一些“经验法则”(rules of thumb),且几个原则互为关联、相辅相成,并非完全独立的。

简单的说:照着这些原则来,代码就会更好;而对于一些习以为常的做法,不遵循 SOLID 原则 -- 写出的代码出问题的几率将会大大增加。

参考资料

  • https://dev.to/kayis/is-react-solid-630
  • https://blog.csdn.net/zhengzhb/article/details/7281833
  • https://github.com/xitu/gold-miner/blob/master/TODO/solid-principles-the-definitive-guide.md
  • http://www.infoq.com/cn/news/2014/01/solid-principles-javascript
  • https://www.guokr.com/article/439742/
  • https://baike.baidu.com/item/Barbara%20Liskov
  • https://www.csdn.net/article/2011-03-07/293173
  • https://thefullstack.xyz/solid-javascript/
  • https://en.wikipedia.org/wiki/Robert_C._Martin#cite_note-3
  • https://softwareengineering.stackexchange.com/questions/170138/is-this-a-violation-of-the-liskov-substitution-principle
  • https://medium.com/@samueleresca/solid-principles-using-typescript-adb76baf5e7c

(end)
----------------------------------------

长按二维码或搜索 fewelife 关注我们哦

用 SOLID 原则保驾 React 组件开发相关推荐

  1. react复习总结(1)--react组件开发基础

    这次是年后第一次发文章,也有很长一段时间没有写文章了.准备继续写.总结是必须的. 最近一直在业余时间学习和复习前端相关知识点,在一个公司呆久了,使用的技术不更新,未来真的没有什么前景,特别是我们这种以 ...

  2. 从零开始的 React 组件开发之路 (一):表格篇

    React 下的表格狂想曲 0. 前言 欢迎大家阅读「从零开始的 React 组件开发之路」系列第一篇,表格篇.本系列的特色是从 需求分析.API 设计和代码设计 三个递进的过程中,由简到繁地开发一个 ...

  3. 【天赢金创】我是如何看待React 组件开发

    从 auto-ellipsis 看 React 组件开发 auto-ellipsis 是一个用于解决文本超长溢出截断并加 ... 的 React 组件. 关于 React 随着 React 的火热,随 ...

  4. Vite + React 组件开发实践

    简介: 毫不夸张的说,Vite 给前端带来的绝对是一次革命性的变化.或者也可以说是 Vite 背后整合的 esbuild . Browser es modules.HMR.Pre-Bundling 等 ...

  5. React组件开发流程——利用React构建简单的可检索产品数据表

    tip:有问题或者需要大厂内推的+我脉脉哦:丛培森 ٩( 'ω' )و [本文源址:http://blog.csdn.net/q1056843325/article/details/54755521 ...

  6. React组件开发-仿哔哩哔哩移动端首页

    前言 什么是React? React是一个简单的javascript UI库,用于构建高效.快速的用户界面.它是一个轻量级库,因此很受欢迎.它遵循组件_设计模式.声明式编程范式_ 和 函数式编程 概念 ...

  7. Taro React组件开发(2) —— RuiEditor 富文本编辑器【兼容H5和微信小程序】

    1. 富文本编辑器需求分析 需要实现图片上传显示,上传使用Taro的 chooseImage 和 uploadFile,完成图片的上传!!! 文字的居左.居中.居右展示,使用格式化方法 format! ...

  8. 独家 | Python中的SOLID原则(附链接)

    作者:Mattia Cinelli翻译:朱启轩校对:欧阳锦本文约3500字,建议阅读15分钟本文通过一些Python示例代码介绍了可以提高代码可靠性的SOLID编码准则. 标签:数据结构,编程,数据科 ...

  9. 星级评价组件--引发对React组件的思考

    机缘巧合之下,我在接到我司产品星级评价需求的前一天,看到了蜗牛老湿的<★tiny-rate 东半球最小的评级组件☆>,在大佬的启发下我就十分顺手的撸了一个移动端用的星级评价组件. 本篇会涉 ...

最新文章

  1. ORB_SLAM2 PnPSolver
  2. 联想正遭遇第四道坎 柳传志对症下药
  3. JFinal 1.1.4 发布,JAVA极速WEB+ORM框架
  4. [bzoj3625][Codeforces Round #250]小朋友和二叉树 (生成函数)
  5. codeforces gym-101745 C-Infinite Graph Game 分块
  6. JavaScript学习随记——属性类型
  7. 台阶问题(洛谷P1192题题解,Java语言描述)
  8. push declined due to email privacy restrictions
  9. Centos系统创建用户oracle后,用该用户登陆系统,页面加载报错GConf error
  10. Atitit.pdf 预览 转换html attilax总结
  11. MySQL药品管理系统设计_药店药品管理系统的设计与实现(SSH,MySQL)(含录像)
  12. 对CMSIS的学习(第1-3部分)
  13. 红色警戒2兼容性补丁、联网补丁、全屏显示设置
  14. skyeye与uClinux的安装
  15. 点餐系统mysql设计,SpringBoot 微信点餐系统 1:数据库表设计
  16. 小程序,微信支付:支付失败,商户号该产品权限未开通,请前往商户平台 产品中心检查后重试
  17. OpenCL基础(一)
  18. 局域网内,如何使用命令行关闭别人的电脑
  19. matlab对比两个文件,比较两个文本文件、MAT-file、二进制文件、Zip 文件或文件夹...
  20. 【计算机视觉40例】案例40:识别性别与年龄

热门文章

  1. easyui下拉框用法
  2. redis cluster迁移相关
  3. 【转】VB中NEW的用法(申请内存空间)
  4. VB获取快捷方式原文件路径
  5. J2SE:Java环境搭建探究环境变量
  6. 苹果要弃用LCD屏,便宜的iPhone XR面临绝版
  7. 景驰无人车余波:王劲“嫡系”忽然发难,官方回应辞退员工所述不实
  8. “深度学习已死,可微编程万岁!”LeCun老师为何又语出惊人?
  9. 在终端设备上实现语音识别:ARM开源了TensorFlow预训练模型
  10. 设置日志不记录指定类型的文件,日志文件的切割,配置静态元素过期时间