持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情
设计和模式两个词应该分开读,先有设计,后有模式。
- 设计:设计原则,设计思想
- 模式:前辈总结出来的固定的套路
为何需要设计?
因为软件规模变大,甚至是一个系统集群,需要先设计,后开发,否则就乱掉。
为何需要模式?
可套用前人经验,降低设计和沟通的成本。
通俗来说,设计(仅指编程设计)就是按照哪一种思路或者标准来实现功能。同样的功能,不同的设计思想都能用不同的方式来实现,前期效果可能一样,但是随着产品功能的增加和扩展,设计的作用才会慢慢的显示出来。
五大设计原则
S O L I D 五大设计原则
- S 单一职责原则
- O 开放封闭原则
- L 李氏置换原则
- I 接口独立原则
- D 依赖导致原则
相对于前端来说,重点关注单一职责原则和开放封闭原则就可以了。
-
单一职责原则: 一个程序只做好一件事,如果功能过于复杂就拆分开,每个部分保持独立。
-
开放封闭原则: 对修改封闭,对扩展开放,这是软件设计的终极目标。即要设计一种机制,当需求发生变化时,根据这种机制扩展代码,而不是修改原有的代码。
以常见的 Promise 来解释一下前两个原则。
// 加载图片
function loadImg(src: string) {
const promise = new Promise<HTMLImageElement>((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject('图片加载失败')
}
img.src = src
})
return promise
}
const src = 'https://www.imooc.com/static/img/index/logo_new.png'
const result = loadImg(src)
result.then((img: HTMLImageElement) => {
console.log('img.width', img.width)
return img
}).then((img: HTMLImageElement) => {
console.log('img.height', img.height)
}).catch((err) => {
console.log(err)
})
- 单一职责原则:每个
then
中的逻辑只做好一件事,如果要做多个就用多个then
- 开放封闭原则:如果这个需求要修改,那去扩展
then
即可,现有的逻辑不用修改,即对扩展开放、对修改封闭
其实 S 和 O 是相符现成的,相互依赖。开放封闭原则的好处不止于此,从整个软件开发流程看,减少现有逻辑的更改,也会减少测试的成本。比如,你如果更改了其中的一个模块,那么测试的时候只需要测试更改的模块即可,如果没有遵循S/O原则,你更改的代码会影响全部,那么测试的时候就需要测试所有的功能。
前端常用的设计模式
前端最常用的设计模式有如下几种,本文主要讲解工厂模式。
- 工厂模式
- 单例模式
- 原型模式
- 装饰器模式
- 代理模式
- 观察者模式
- 迭代器模式
工厂模式
简单版本的工厂模式
class Product {
name: string
constructor(name: string) {
this.name = name
}
fn1() {
console.log('product fn1')
}
fn2() {
console.log('product fn2')
}
}
class Creator {
create(name: string): Product {
return new Product(name)
}
}
const creator = new Creator()
const p1 = creator.create('p1')
const p2 = creator.create('p2')
标准版本的工厂函数
interface IProduct {
name: string
fn1: () => void
fn2: () => void
}
class Product1 implements IProduct {
name: string
constructor(name: string) {
this.name = name
}
fn1() {
console.log('product fn1')
}
fn2() {
console.log('product fn2')
}
}
class Product2 implements IProduct {
name: string
constructor(name: string) {
this.name = name
}
fn1() {
console.log('product fn1')
}
fn2() {
console.log('product fn2')
}
}
class Creator {
create(type: string, name: string): IProduct {
if (type === 'p1') {
return new Product1(name)
}
if (type === 'p2') {
return new Product2(name)
}
throw new Error('valid type')
}
}
这样写的工厂模式有什么好处呢?
-
符合依赖倒置原则,即依赖于抽象的接口,而不是依赖于具体的类。
create
方法返回的值类型是接口IProduct
,而不是具体的类Product1
或者Product2
,这样的好处是当我们进行扩展的时候,比如扩展了Product3
或者Product4
,那就不需要修改create
函数的返回值了。 -
处理 new 的逻辑放在
create
函数里面,如果写在外面,每次创建的时候都要进行if else
判断,代码非常的冗余。
5 大设计原则中,最重要的就是:开放封闭原则,对扩展开放,对修改封闭。工厂模式是如何满足开发封闭原则的呢?
工厂和类分离解耦
现在你要得到一个汉堡,你是跟服务员要(买)一个,还是自己动手做一个?这个问题,服务员就是工厂方法,而动手做一个其实就是new A()
。
可以扩展多个类
我们定义好了IProduct
接口,这样就可以扩展出很多的类。
工厂的创建逻辑也可以自由扩展
在工厂函数里面,我们根据用户传入的参数可以创建不同的类。
遇到
new class
时,考虑工厂模式
工厂模式的使用场景
jQuery $('div')
// 注意是大写的Window
declare interface Window {
$: (selector: string) => JQuery
}
class JQuery {
selector: string
length: number
constructor(selector: string) {
const domArr = Array.prototype.slice.call(
document.querySelectorAll(selector)
)
const length = selector.length
for (let i = 0; i < length; i++) {
// @ts-ignore
this[i] = domArr[i]
}
this.selector = selector
this.length = length
}
append(ele: HTMLElement): JQuery {
// ...
return this
}
addClass(key: string, value: string): JQuery {
return this
}
}
window.$ = (selector: string) => {
return new JQuery(selector)
}
- window下面是没有
$
,那么需要我们给Window
这个接口添加一个$
的声明
declare interface Window {
$: (selector: string) => JQuery
}
- 如果开放给用户的不是
$
,然后让用户自己去new JQuery(selector)
,带来的问题:- 不方便链式操作,如
$('div').append($('#p1'))
- 不宜将构造函数暴露给用户,尽量高内聚、低耦合
- 不方便链式操作,如
Vue _createElementVNode
我们把下面这段代码复制到在线编译 vue-next-template-explorer.netlify.app/这个网站里面
<div>
<span>静态文字</span>
<span :id="hello" class="bar">{{ msg }}</span>
</div>
编译后的代码:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "静态文字"),
_createElementVNode("span", {
id: _ctx.hello,
class: "bar"
}, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"])
]))
}
可以看到,会编译出很多 _createElementVNode
JS 代码,这些函数就是工厂函数,用于创建 vnode
,里面主要逻辑就是return new VNode()
。
React createElement
在 React 中使用 JSX 语法,我们把下面代码复制到在线编译 www.babeljs.cn/repl
const profile = <div>
<img src="avatar.png" className="profile" />
<h3>{[user.firstName, user.lastName].join(' ')}</h3>
</div>
这是一种语法糖,编译之后就会是
// 返回 vnode
const profile = React.createElement("div", null,
React.createElement("img", { src: "avatar.png", className: "profile" }),
React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);
其实React.createElement
也是一个工厂,模拟代码
class Vnode(tag, attrs, children) {
// ...省略内部代码...
}
React.createElement = function (tag, attrs, children) {
return new Vnode(tag, attrs, children)
}