前端设计模式
设计模式
设计模式是解决方案,是对软件设计方案中普遍存在的问题提出的解决方案。
算法是不是设计模式?
算法不是设计模式。
算法解决的是计算问题,不是解决设计上的问题。设计模式通常讨论的是对象间的关系、程序的组织形式等设计问题。
面向对象是不是设计模式?
面向对象是设计模式。
函数式编程是不是设计模式?
函数式编程是设计模式。
面向对象和函数式概念包含的范围很大,不大适合做太具体的设计模式探讨。
或者说,OOP 和 FP 是两类设计模式的集合,是编程范式。
前端设计模式
对前端普遍问题的解法。
前端中会用到传统的设计模式:
- 工厂(Factory)
- 单例(Signleton)
- 观察者(Observer)
- 构造器(Builder)
- 代理模式(Proxy)
- 外观模式(Facade)
- 适配器(Adapter)
- 装饰器(Decorator)
- 迭代器(Generator)
还有一些偏前端的:
- 组件化(Component)
- Restful
- 单项数据流
- Immutable
- 插件
- DSL(元数据)
单例(singleton)
确保一个类只有一个实例。例如 document.window
。
常见用法:
class ComponentsLoader {
private static inst: ComponentsLoader = new ComponentsLoader()
static get() {
return ComponentsLoader.inst
}
}
class IDGen {
private constructor() {}
static inst = new IDGen()
get() { return inst }
}
tsx
隐含单例的逻辑:
const editor = useContext(RednerContext)
typescript
设计模式关注的是设计目标,并不是对设计实现的强制约束。闭包也可以实现单例,例如:
const singleton = () => {
const obj = new ...
return () => {
...
}
}
typescript
理解设计模式,灵活使用设计模式。
总结:
- 可以用于配置类、组件上下文中共用的类等;
- 用于对繁重资源的管理(例如数据库连接池)。
工厂(Factory)
将类型的构造函数隐藏在创建类型的方法之下。
export class Rect {
static of(left: number, top: number, width: number, height: number) {
return new Rect(left, top, width, height)
}
}
typescript
例如 React.crateElement
,它也相当于一个工厂方法。
export default class Project {
public static async create() {
const crateor = new ProjectCreator()
return await crateor.create()
}
}
typescript
例如 ORM 框架 Sequelize 对于不同 dialect 的实现,也是工厂模式的一种。
适用场景:
- 隐藏被创建的类型;
- 构造函数较复杂;
- 构造函数较多。
观察者(Observer)
对象状态改变时通知其他对象。
Vue.use(Vuex)
const store = new Vuex.store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})
new Vue({
el: '#app',
store
})
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}
typescript
场景:
- 实现发布、订阅之间 1 对多的消息通知;
- 实现 Reactive Programming。
主动的、响应的:Proactive vs Reactive
命令式的程序有什么缺点?
- 组件间依赖比较强;
- 需要借助很多第三方代码去实现功能。
每个组件都应该知道自己应该做什么。
构造器(Builder)
将类型的创建构成抽象成各个部分。
例如,造车:
造车() {
造发动机()
造轮子()
造内饰()
...
}
例如 JSX 编写组件:
<Page>
<TitleBar />
<Tabs>
<Tab title="首页" icon="">...</Tab>
<Tab title="发现" icon="">...</Tab>
<Tab title="个人中心" icon="">...</Tab>
</Tabs>
</Page>
tsx
代理模式(Proxy)
将代理类作为原类的接口。通常代理类会在原类型的基础上做一些特别的事情。
例如 vue reactivity
实现。
function createReactiveObject(
target: Target,
// ...
) {
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
}
// get track
// set trigger
typescript
什么时候适合用代理模式?
如果你想在原有的类型上增加一些功能和变通处理,但是又不希望用户意识到,这时就可以使用代理模式。
适配器模式(Adapter)
通过一层包装,让接口不同的类型拥有相同的用法。因此也称为包装模式(wrapper)。
让不同的组件拥有相同的设计接口,抹平差异,简化用户操作。用户不需要去理解多个概念,减少心智负担。
例如 ant-design
中的:
- onChange:
- defaultValue
例如 React SyntheticEvent。
https://reactjs.org/docs/events.html#gatsby-focus-wrapper
外观模式(Facade)
将多个复杂的功能隐藏在统一的调用接口中。
例如 vite dev
、vite build
。
内部功能实现很复杂,我们可以将内部功能按照用户的需要分类,做成门面,让用户使用,不需要关心内部实现逻辑。
例如 react 的 useState
、useEffect
、useRef
、useContext
也算是外观模式的实现,开箱即用。
外观模式优点:
- 整合资源;
- 降低使用复杂度(开箱即用)。
状态机(StateMachine)
将行为绑定在对象内部状态变化之上。
例如 redux。
场景:
- 组件/管理交互设计;
- 在 DOM 之上抽象用户交互。
装饰器(Decorator)
在不改变对象、函数结构的情况下为它添加功能或说明。
例如 @deprecated
:
interface UIInfo {
/** @deprecated use box instead */
width: number
/** @deprecated use box instead */
height: number
box: BoxDescriptor
}
typescript
例如之前的 React 代码:
@fetchProductList()
class List extends ReactComponent {
render() {
const productList = this.props.productList
return <...></,,,>
}
}
function fetchProductList(Target) {
return () => {
class ProxyClass extends React.Component {
fetch() {
...fetch logic
}
render () {
const list = this.state.list
return <Target productList={list} />
}
}
return ProxyClass
}
}
typescript
目前写法:
const List = () => {
const productList = useFetchProductList()
return <...></...>
}
typescript
前端还需要使用装饰器嘛?
作为高阶组件的装饰器暂时不使用了,但是它只有其他用途,例如 typescript 官网的一个例子。
class Point {
private _x: number
private _y: number
constructor(x: number, y: number) {
this._x = x
this._y = y
}
@configurable(false)
get x() {
return this._x
}
@configurable(false)
get y() {
return this._y
}
}
function configurable(value: boolean) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value
}
}
typescript
装饰器不是切面(aop),但是切面是装饰器。
主要作用:
- 替换原有实现;
- 修改元数据。
迭代器(Iterator)
用 Iterator 来遍历容器内的元素(隐藏容器内部数据结构)。
例如 JavaScript 的 Set
、Array
、HashMap
等。
例如使用 Generator,用来简化 Iterator 的构造。