前端设计模式

设计模式

设计模式是解决方案,是对软件设计方案中普遍存在的问题提出的解决方案。

算法是不是设计模式?

算法不是设计模式。
算法解决的是计算问题,不是解决设计上的问题。设计模式通常讨论的是对象间的关系、程序的组织形式等设计问题。

面向对象是不是设计模式?

面向对象是设计模式。

函数式编程是不是设计模式?

函数式编程是设计模式。

面向对象和函数式概念包含的范围很大,不大适合做太具体的设计模式探讨。
或者说,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 devvite build

内部功能实现很复杂,我们可以将内部功能按照用户的需要分类,做成门面,让用户使用,不需要关心内部实现逻辑。

例如 react 的 useStateuseEffectuseRefuseContext 也算是外观模式的实现,开箱即用。

外观模式优点:

  • 整合资源;
  • 降低使用复杂度(开箱即用)。

状态机(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 的 SetArrayHashMap 等。

例如使用 Generator,用来简化 Iterator 的构造。