25个问题,你会几个
-
如何理解单一职责原则?
-
如何判断职责是否足够单一?
-
职责是否设计得越单一越好?
-
什么是开闭原则?
-
修改代码就一定意味着违反开闭原则吗?
-
怎样的代码改动才被定义为扩展或者说是修改?
-
如何做到对扩展开放、修改关闭?
-
如何在项目中灵活运用开闭原则?
-
什么是依赖反转(倒置)原则 ?
-
高层模块和低层模块是啥意识?
-
如何理解反转两个字?
-
什么依赖被反转了?
-
什么是控制反转
IOC
(Inversion Of Control
)? -
什么是依赖注入
DI
(Dependency Injection
)? -
IOC
和DI
有什么区别? -
代码行数越少就越简单吗?
-
代码逻辑复杂就违背
KISS
原则吗? -
如何写出满足
KISS
原则的代码? -
如何判断是否满足
KISS
原则? -
重复的代码就一定违背
DRY
吗? -
如何提高代码的复用性?
-
什么是迪米特法则?
-
高内聚、松耦合是什么意识?
-
如何理解高内聚和松耦合?
-
如何用好迪米特法则?
看完这些问题,是不是激动的一笔,激动了,说明你又要进步成长了。
行文方式
先哈两句,活跃一下气氛。然后通过提出问题,回答问题,然后结合生活例子和代码,来全方位阐述设计原则知识。
为什么要学习设计原则
给你的自由过了火
做人需要原则,那写代码的时候,大家有没有讲原则呢?
按照正常剧情,这时候有小伙伴要开始表演了:
高赞回答:不好意识,我做人没有原则。
哈哈哈哈,那我只能说,你是光,你是电,你是唯一的神话。
越下游,越自由
大家有没有这种感觉,没有没关系,我举几个例子,大家就明白了。
例子如下所示:
-
产品经理写产品策划文档,给开发测试看,起码要有点人样
-
交互设计写交互设计文档,给开发产品看,起码要有点人样
-
ui
设计师产出设计稿,给开发产品看,起码要有点人样
看完上面例子,再说我们:
当骄傲的前端工程师写完代码,然后 two days later
。惊喜的发现,自己写的代码已经不认识了,这就非常尴尬了。然心中窃喜,毕竟我们处于最下游,不存在把代码给谁阅读之说,最多也就是走下 code review
。
这种感觉是非常危险的,当我们处于非常下游的地方,也意味着我们非常自由,它有很多负面影响。
所以,我们通过什么来约束这种自由呢?这个答案就是本文想详细阐述的:
通过设计原则来约束这种过火的自由。
设计原则有哪些呢
大家请看下图:
图中设计原则一栏,涵盖了所有重要的设计原则,如 SOLID
、 KISS
、 YANGI
、 DRY
、LOD
。
活不多说,下面大家跟着我,一步步掌握设计原则吧!
用好设计原则的目的
上面说了为什么要学习设计原则,那大家再想一下,我们用好设计原则的目的是什么?
目的如下:让代码或者项目具备:
-
可读性
-
可扩展性
-
复用性
-
可维护性
总结一句话就是:降低软件开发的复杂度,让迭代的难度保持在合理区间内。
OK
, 说完目的,我们开始逐一介绍这些设计原则,小伙伴们请往下阅读。
注意:下文所说的类,也代指模块,这样我就不再单独写一遍模块了。
SOLID
这是第一个介绍,也是最重要的。
重要的事说三遍:
请记住:设计原则中, SOLID
是重点, 而 SO
是重点中的重点。
S
名称
-
SRP:Single Responsibility Principle
-
中文:单一职责原则
QUESTION
如何理解单一职责原则?
一个类只负责完成一个职责或者功能,不要设计大而全的类,要设计粒度小、功能单一的类,简单点说,就是要小而美。单一职责原则的目的是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
如何判断职责是否足够单一?
这里有 5
个技巧:
-
类中的私有方法过多
-
比较难给类起一个合适的名字
-
类中的代码行数、函数或者属性过多
-
类中大量的方法都是集中操作类中的某几个属性
-
类依赖的其他类过多,或者依赖类的其他类过多
职责是否设计得越单一越好?
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类的职责单一,其依赖的和被依赖的其他类也会变少,从而实现代码的高内聚、松耦合。
注意: 如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
生活中例子
社会分工: 写代码的,不会同时去写策划文档。
code 展示
例子1:符合单一职责
`useFuncA()`
`useFuncB()`
可以把上面的函数看成是 hooks
, 一个函数( hooks
)完成一个功能。
例子2:不同业务层面,可以符合单一职责,也可以不符合单一职责
`const userInfo = {`
`userId: '',`
`username: '',`
`email: '',`
`telephone: '',`
`createTime: '',`
`lastLoginTime: '',`
`avatarUrl: '',`
`provinceOfAddress: '',`
`cityOfAddress: '',`
`regionOfAddress: '',`
`detailedAddress: ''`
`}`
-
从用户业务层面看,满足单一职责原则。
-
从用户展示信息、地址信息、登录认证信息这些更细粒度的业务层面来看,就不满足单一职责原则。
例子3:符合单一职责
从上图可以看出,一个功能只由一个模块目录完成。
例子4:不符合单一职责
`function bindEvent(elem, type, selector, fn) {`
`if (fn == null) {`
`fn = selector`
`selector = null`
`}`
`}`
`bindEvent(elem, 'click', '#div', fn)`
`bindEvent(elem, 'click', fn)`
我们发现,bindEvent
函数可以传很多参数,不符合单一职责原则,它是外观模式思想的体现。
PS: 外观模式如下图所示:
SRP 总结
上面的问题回答,进行了总结,这里再补充句:
SRP
要结合业务场景去看待,角度不同,结果不同。
O
-
OCP: Open Closed Principle
-
中文:开闭原则
QUESTION
什么是开闭原则?
软件实体(类、模块、函数)都应当对扩展具有开放性,但对于修改具有封闭性。
也就是说:添加一个新功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码。
修改代码就一定意味着违反开闭原则吗?
不一定,这个我们要灵活看待:
-
第一:开闭原则并不是说完全杜绝修改,而是以最小修改代码的代价来完成新功能的开发
-
第二:同样的代码改动,在粗代码粒度下,可能被认定为修改,在细代码粒度下,可能又被认定为扩展
-
第三:尽量让最核心、最复杂的那部分逻辑代码满足开闭原则
怎样的代码改动才被定义为扩展或者说是修改?
通常情况下,只要它没有破坏原有代码的正常运行,没有破坏原有的单元测试,我们就可以认为它是符合开闭原则的。如果破坏了,那我们就可以认为它不符合开闭原则。
如何做到对扩展开放、修改关闭?
-
保持函数、类和模块当前本身的状态,或是近似于他们一般情况下的状态(即不可修改性)
-
使用组合的方式(避免使用继承方式)来扩展现有的类、函数或模块,以使它们可能以不同的名称来暴露新的特性或功能
如何在项目中灵活运用开闭原则?
时刻具备扩展意识、抽象意识、封装意识。
生活中例子
高考试卷: 比如明天就要高考了,但是老师发现没法区分高分学生和低分学生,必须得在试卷里面增加两个难度比较大的题,但是明天就高考了,如果现在去修改高考中的试卷,显然是不合理的。经过思考,最好的办法就是给高考的试卷加一个附加题【你可以加附加题,但是你不能修改原来的卷子,这就是对扩展开放,对修改关闭】。
code
例子1:中间件
`app.use(A).use(B).use(C)`
例子2:function optional
`fn(f1(),f2(),f3())`
例子3:插件
`Vue.use(PluginA)`
`Vue.use(PluginB)`
例子4:装饰器
`@get('/hello')`
`async hello() {`
`// ...`
`}`
总结
有以下两点:
-
开闭原则是最重要的设计原则,很多设计模式都是以开闭原则为指导原则的
-
它的核心是为了提高代码的扩展性
L
-
LSP: Liskov Substitution Principle
-
中文:里氏替换原则
额,这缩写怎么有点搞笑,嗯?
QUESTION
什么是里氏替换原则?
子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。
这里的行为约定包括:函数声明要实现的功能、对输入、输出、异常的约定、甚至包括注释中所罗列的任何特殊说明。
如何判断是否满足里氏替换原则?
拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全遵守父类的约定,子类有可能违背了里氏替换原则。
生活中例子
盗版光盘: 原来人家的光盘是正版的,但现在你弄了一个盗版的光盘,我们有两张光盘,放到 DVD
里面,都可以单独运行【盗版光盘把正版光盘全部 copy
过来,子类父类行为预期一致】
code
例子1:代码执行重复,不符合
LSP
原则
`class People {`
`constructor(name, age) {`
`this.name = name`
`this.age = age`
`}`
`eat() {`
`// ...`
`}`
`}`
`class Student extends People {`
`constructor(name, age) {`
`super(name, age)`
`}`
`eat() {`
`// ...`
`}`
`}`
`const kuangXiangLu = new Student('KuangXiangLu', 10)`
`kuangXiangLu.eat()`
为什么不符合 LSP
呢?有以下两个原因:
第一:Student
类继承了 People
类,同时修改了 People
类的 eat
方法,这时就违背了 LSP
原则
第二:没有遵循父类的设计,修改了输出
总结
里氏替换原则的核心是用来指导,继承关系中子类该如何设计的一个原则,也就是 design by contract
(按照协议来设计)。
I
-
ISP: Interface Segregation Principle
-
中文:接口隔离原则
QUESTION
什么是接口隔离原则 ?
接口的调用者或者使用者,不应该强迫依赖它不需要的接口。
接口隔离原则中的接口是指什么?
接口可以理解为下列三种东西:一组 API
接口集合、单个 API
接口或函数、OOP
中的接口概念
生活中例子
汽车 USB 插口: 汽车上有很多插口,但是你想插 usb
接口,你想让它有 usb
功能,又想让它有三线插头的功能,这就是不科学的事情【每一个接口都应该有自己的一种角色,只负责自己的角色】。
code
代码1:
`const obj = {`
`login() {`
`// 用户登录`
`},`
`delete() {`
`// 删除用户信息`
`}`
`}`
delete
是不常用且危险的操作,如果和 login
放在一起,就存在被不需要调 delete
的业务误调的可能,违背了 ISP
原则。
代码2:
`function main() {`
`// 处理加法`
`// 处理减法`
`// 处理乘法`
`// 处理...`
`}`
一个函数里面处理了很多逻辑,也违背了 ISP
原则。
总结
兄弟们, 细细品,重在隔离。
D
-
DIP: Dependency Inversion Principle
-
中文:依赖反转(倒置)原则
QUESTION
什么是依赖反转(倒置)原则 ?
高层模块( high-level modules
)不要依赖低层模块( low-level
)。高层模块和低层模块应该通过抽象( abstractions
)来互相依赖。除此之外,抽象( abstractions
)不要依赖具体实现细节( details
),具体实现细节 ( details
)依赖抽象( abstractions
)。
高层模块和低层模块是啥意识?
在调用链上,调用者属于高层,被调用者属于低层。
如何理解反转两个字?
反转指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。
流程的控制权从程序员反转到了框架。
什么依赖被反转了?
高层模块被反转了。
什么是控制反转 `IOC (Inversion Of Control)` ?
控制反转,控制是指对程序执行流程的控制,在没有反转前,控制权在程序员手里,经过反转后,控制权到了框架手里。
控制反转并不是一种具体的实现(编码)技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。
什么是依赖注入 `DI (Dependency Injection)` ?
不通过 new()
的方式在类内部创建 依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递 (或注入)给类使用。
依赖注入是一种具体的(实现)编码技巧。
`IOC` 和 `DI` 有什么区别?
IOC
是设计思想, DI
是具体(实现)编码技巧。
生活中例子
-
三个和尚打水: 正常操作是直接用桶从井里面打水,但是现在非要加一个环节,先用桶把井里的水打到大桶里,然后再从大桶里面打水【不需要中间操作环节,直接用底层操作】。
-
`CPU` 内存: 硬盘都是针对接口设计的,如果针对实现来设计,内存就要对应到具体的某个品牌的主板,明显不合理。
code
代码和解读如下图所示:
12.png
总结
DIP
是一个抽象难懂的设计原则,从 IOC
和 DI
的中文命名就可以看出来。大家在运用 DIP
的时候,要理解透彻 反转 一词。记住将流程交给框架控制,然后再实现它。
KISS
-
英文:Keep It Simple and Stupid
-
中文:保持简单愚蠢
-
俗解:保持代码简单
QUESTION
代码行数越少就越简单吗?
不一定,如一些较长的正则表达式,三位运算符,这些都是违背了 KISS
原则。
代码逻辑复杂就违背 `KISS` 原则吗?
不一定,如果是复杂的问题,用复杂的方法解决,并不违反 KISS
原则。
如何写出满足 `KISS` 原则的代码?
-
不要使用同事可能不懂的技术来实现代码
-
不要重复造轮子,要善于使用已经有的工具类库
-
不要过度优化
如何判断是否满足 `KISS` 原则?
KISS
是一个主观的评判,可以通过 code review
来做,如果大多数同事对你的代码有很多疑问,基本就说明不够 KISS
。
code
如下代码所示:不符合 KISS
原则
`let a = b ? c : d ? e : f`
总结
-
关注如何做
-
我们在做开发的时候,一定不要过度设计,不要觉得简单的东西就没有技术含量。实际上,越是能用简单的方法解决复杂的问题,越能体现一个人的能力。
YANGI
-
英文:You Ain’t Gonna Need It
-
中文:你不会需要它
-
俗解:不要做过度设计
生活中例子
双11剁手: 卧槽,好便宜啊,下单下单下单,然后… 自行想象。
总结
-
永远不要因为:预计你会用到某个功能就去写一段代码去实现
-
而是:真的需要这个功能时才去实现它
DRY
-
英文:
Don’t Repeat Yourself
-
中文:不要重复你自己
-
俗解:不要写重复的代码
QUESTION
重复的代码就一定违背 `DRY` 吗?
重复的代码不一定违背 DRY
原则,代码重复有三种典型情况,分别是:
-
实现逻辑重复
-
功能语义重复
-
代码执行重复
如何提高代码的复用性?
减少代码耦合、满足单一职责原则、模块化、业务与非业务逻辑分离、通用代码抽离、抽象和封装、使用设计模式。
code
例子1:实现逻辑重复
代码 1 :
`function isValidUserName() {`
`// 内容一样`
`}`
`function isValidPassword() {`
`// 内容一样`
`}`
`function main() {`
`isValidUserName()`
`isValidPassword()`
`}`
代码2:
`function isValidUserNameOrPassword() {`
`// 内容一样`
`}`
`function main() {`
`isValidUserNameOrPassword()`
`}`
大家看, 代码 1
中的两个函数代码都是一样的,所以我们通过去除重复,变成一个函数,变成了代码 2
。这里问大家一个问题,你们觉得这样做是否违背了 DRY
呢?
-
结果:代码
1
不违背DRY
,代码2
违背了DRY
的初衷。 -
原因:虽然实现逻辑重复,但是语义不重复。从功能上看,他们是做的是两件完全不同的事情。合并后,一个函数做了两件事情,违反了
SRP
和ISP
。 -
改善:抽象出更细粒度函数
例子 2 :功能语义重复
`function sayHello() {`
`//`
`}`
`function speakHello() {`
`//`
`}`
都是表达 hello
的意识,虽然代码没重复,但是语义重复了,违背 DRY
。
例子 3 :代码执行重复
`function isLogin() {`
`//`
`}`
`function main() {`
`if (xxx) {`
`isLogin()`
`}`
`// 代码省略...`
`if (yyy) {`
`isLogin()`
`}`
`}`
在一个函数中,多次执行同一个函数,违背了 DRY
。
总结
不重复并不代表可复用,要辩证思考和灵活应用。
LOD
英文:Law of Demeter
中文:迪米特法则
俗解:高内聚、松耦合
QUESTION
什么是迪米特法则?
不该有直接依赖关系的类之间,不要有依赖。有依赖关系的类之间,尽量只依赖必要的接口
高内聚、松耦合是什么意识?
-
高内聚是指:相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。因为相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。
-
松耦合是指:在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
如何理解高内聚和松耦合?
结合下图来理解:
它是一个通用的设计思想,可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。
如何用好迪米特法则?
减少代码耦合、满足单一职责原则、模块化。
生活中例子
现实中的对象:你对你的对象肯定了解的很多,但是你要是对别人的对象也了解很多,那就出大事了【一个对象应该对其他对象有尽可能少的了解】。
code
如上图所示:我们用 lerna
去开发一个框架,将框架的不同功能放到不同的 package
中进行维护迭代,符合 LOD
。
总结
高内聚、松耦合是一个非常重要的设计思想,它能够有效地提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。
设计原则总结
好啦,看到这,设计原则基本就阐述完了,我对主要的设计原则进行了阐述,大家读会发现,一些设计原则虽然起名不同,但是其目标都是类似和相同的。学习和掌握主要的设计原则,可以帮助我们更好的进行软件设计、开发和迭代。也是为我们学习和掌握设计模式打下坚实的基础。
交流
本文对设计原则的阐述定有不当之处,有不对的地方,欢迎小伙伴们在评论处指出和交流。也非常欢迎加我微信进行技术交流。
小伙伴们也可以进 前端狂想录群 大家一起头脑风暴。