3. 设计模式之原则
在最先接触设计模式的时候,我就看到了对「开闭原则」的介绍,后续又陆陆续续接触了「单一职责原则」、「迪米特法则」等等,今天在这里对设计模式的各原则进行一个统一的记录。
- 单一职责原则
- 开放-封闭原则
- 里式代换原则
- 依赖倒转原则
- 迪米特法则
- 接口隔离原则
单一职责原则
单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因。
这个原则说的就是一个类只关注一件事情,我们经常会看到这个原则,比如我们开始学习编程的时候,会说到怎么优化代码,其中一项就是一个函数只做一件事情这样的,那时候「只做一件事情」就在我脑海中留下了比较深刻的印象。
现在随着前端组件化的发展,我们将页面也细分为大大小小的组件来进行拼接。在这个时候,「单一职责原则」就和前端的工作息息相关了,我们在拆分组件的时候,也应该保证每一个组件只做一件事情,只关注一个重点。这样我们的页面在后期的扩展维护上就会节省很多的功夫,这也是优秀的组件应该做到的。
「单一职责原则」的有点也是显而易见的:
- 代码复杂度变低
- 可维护性提高
- 需求变更引起的影响减小
开放-封闭原则
开放-封闭原则,是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。[ASD]
我们在开发系统的时候,经常会有这样的感受,在产品最开始设计的时候,我们就尽可能把准备工作多做一些,希望最开始的框架能够满足未来的一些需求。但是随着开发工作的逐渐深入,我们最头疼的情况就发生了:不管我们开始的时候想的有多么完善,都无法满足未来的需求,总是需要去扩展我们的框架。这种时候,「开放-封闭原则」就是我们的指导。
其实我们需要面对的问题就是:但原框架无法适应新需求的时候,我们是修改原来的代码,还是扩展原来的代码?我们经常会发现一个需求只要修改一小段代码就可以完成,然后我们就用了很快的时间完成了任务,之后却发现引起了许多的问题。所以我们在处理新需求的时候,应该更多地在源代码的基础之上去加东西,而不要去改动原有的代码,以防止影响原来的功能。
「开放封闭原则」的特征就是:
- 对于扩展是开放的
- 对于更改是封闭的
它的优点也显而易见:
- 提高了系统的稳定性
开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、复用性好、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。
里式代换原则
里氏代换原则:子类型必须能够替换掉他们的父类型。
我的理解是「里式代换原则」保留了父类的可扩展性,即我们设计的时候在父类的基础上添加特性,在具体实现的时候利用子类来实现。好处就是:
- 可以随时随地地扩展父类,因为父类的扩展不影响具体功能
- 所有具体功能的实现都由子类去做,系统是感知不到父类的存在的
- 父类可以真正的被复用
依赖倒转原则
依赖倒转原则
A. 高层模块不应该依赖低层模块。两个都应该依赖抽象。
B. 抽象不应该依赖细节。细节应该依赖抽象。
说的清楚一些应该就是我们要「针对接口编程,而不是针对实现编程」。
假如当下我们要做一个关于考试分数的柱状图标,首先我们就会想到用echarts来做这个图表。
// echarts类
class Echarts {
constructor() {}
// 绘制柱状图
renderBar(options) {}
// 绘制折线图图
renderLine(options) {}
// 绘制饼图
renderPie(options) {}
}
// 绘制图表类
class Render {
tool = new Echarts();
constructor() {}
// 绘制柱状图
renderBar(options) {
this.tool.renderBar(options);
}
// 绘制折线图图
renderLine(options) {
this.tool.renderLine(options);
}
// 绘制饼图
renderPie(options) {
this.tool.renderLine(options);
}
}
const render = new Render();
render.renderBar();
render.renderLine();
render.renderLine();
但是这时客户觉得更喜欢阿里的G2图表,这时我们修改代码就发现很难去修改,因为我们的业务绘制代码直接依赖了Echarts,如果我们需要改图表依赖,我们就要改很多东西,那怎么做才好一些呢?
// 依赖倒置原则
// echarts类
class Echarts {
constructor() {}
// 绘制柱状图
renderBar(options) {}
// 绘制折线图图
renderLine(options) {}
// 绘制饼图
renderPie(options) {}
}
// g2
class G2 {
constructor() {}
// 绘制柱状图
renderBar(options) {}
// 绘制折线图图
renderLine(options) {}
// 绘制饼图
renderPie(options) {}
}
function getTool(type) {
switch (type) {
case 'echarts':
return new Echarts();
break;
case 'g2':
return new G2();
break;
}
}
// 绘制图表类
class Render {
constructor(tool) {
this.tool = tool;
}
// 绘制柱状图
renderBar(options) {
this.tool.renderBar(options);
}
// 绘制折线图图
renderLine(options) {
this.tool.renderLine(options);
}
// 绘制饼图
renderPie(options) {
this.tool.renderLine(options);
}
}
const render = new Render(getTool('g2'));
render.renderBar();
render.renderLine();
render.renderLine();
由上可见,我们新增了一个getTool
的方法,如果我们需要调整或者扩展图表类,我们只需要扩展底层方法即可,代价和难度都会大大降低,也减少了高层代码(绘制图表类)对底层代码(图表类)的依赖。
迪米特法则
迪米特法则(LoD):如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
举个例子,现在我们打车都用上了高德地图,但是还是有些地方地图定位的不是特别准确,这些时候就需要我们口头告诉司机应该怎么走。这种场景下我们就有了3个类:乘客、司机和汽车,其实我们最终的目的是希望指挥车子的运动(向左转、向右转等等),但是其实我们和车子的控制没有关系,我们就需要通过第三者(司机)来达成目的。
// 迪米特法则
class Car {
constructor() {}
// 左转
turnLeft() {}
// 右转
turnRight() {}
// 掉头
turnRound() {}
// 直行
goStraight() {}
}
class Driver {
constructor(car) {
this.car = car;
}
// 左转
turnLeft() {
this.car.turnLeft();
}
// 右转
turnRight() {
this.car.turnRight();
}
// 掉头
turnRound() {
this.car.turnRound();
}
// 直行
goStraight() {
this.car.goStraight();
}
}
class Customer {
constructor() {}
inform(driver, direction) {
switch (direction) {
case 'left':
driver.turnLeft();
break;
case 'right':
driver.turnRight();
break;
case 'round':
driver.turnRound();
break;
case 'straight':
driver.goStraight();
break;
}
}
}
const car = new Car();
const driver = new Driver(car);
const customer = new Customer();
// 乘客告诉司机下个路口左转
customer.inform(driver, 'left');
// 乘客告诉司机下个路口右转
customer.inform(driver, 'right');
- 在类的结构设计上,每一个类都应当尽量降低成员的访问权限;
- 迪米特法则其根本思想,是强调了类之间的松耦合;
- 类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。
接口隔离原则
一个类对另一个类的依赖应该建立在最小的接口上。
对于这个原则,我的理解是这样的:在当下我们写页面的时候,常用的UI框架有ElementUI和Ant Design UI,在这两个UI框架中,有三个组件:Input
、AutoComplete
和InputNumber
,我们会发现他们有很多共同的地方:
- 都是输入框
- 支持相同的事件(Focus、Blur等等)
- 支持相同的属性(readOnly,autofocus等等)
除此之外他们也都有自己的特性:
AutoComplete
支持自动搜索InputNumber
对数字有特殊的处理
其实我们能不能把这些特性全部集中在Input
上呢?其实应该也是可以的,那样的话Input
组件就会很臃肿,也提供了很多我们平时用不到的特性。
那最好的方式就是将「自动搜索」和「数字特殊处理」当做独立接口处理,以形成我们今天的3个组件,让我们在需要的地方用最适合的组件。
我想这是前端工程师对「接口隔离原则」的一个很棒的应用。
参考
大话设计模式 -- 程杰
Element-UI
Ant Design