2023-11-23·55 min read

重读设计模式阅读笔记

设计模式详细阅读笔记:工厂模式、适配器、代理、责任链等

设计模式

date: 2024-01-01 status: Public type: Post updatedAt: 2024-12-28T15:33 Public: false category: Daily

前述

如果说前两年还有web component, wasm,基于es module快速打包,前端还在狂卷一条街。那么从今年开始,前端似乎已经进入了完全内卷的阶段,看不到什么新的东西了。最近也就看到一个前司新出rspack,然后就是各种新API的提案。当然可能也是我个人的注意力都在AI上了。

最近要准备面试,正好不知道从哪开始,只能把我认为的前端知识体系,做一个系统的归类和总结。刚好也作为简历能力的一种归类。

如果说用面试的知识体系来评价一个人的话,在我心中大概是下面几个方向。不熟的就瞎说说,不对就不对吧

语言熟练度

  • 熟练度是量变引起质变的东西
  • 虽然在类型提示各种插件遍布的今天,甚至ChatGpt能帮你纠正各种错误的语法,熟练的语言不再能带来太多优势。
  • 但是我觉得当你多写这门语言的代码,肯定能对本身一些设计的初衷,存在的问题,产生更多的思考。
  • 可以说这里属于很硬实的基本功,虽然八股文被人人喊打,认为不懂这一道题又如何,查查谷歌、gpt,答案立马就有,但是如果超过一定的题目都不懂,就会引起所说的质变了。

前端语言部分我觉得包括

  1. Javascript 包括es5 es6各种语法的熟练度
  2. React框架下的各种语法,这里其实内容很多。我不熟悉Vue就不单聊了,其实React的官网最近做了更新,我从零重新看了一遍,从教程的思路出发,理解了很多设计上的思路。
  3. Typescript 包括其本身基础语法的各种Utils,类型推导语法,以及上述各种语言关于ts的基础使用方式
  4. css主要指css、css3的一些语法、sass或者less的基础语法、tailwindcss的基础语法
    1. 其实css内容真细究,之前看张鑫旭书里的一些内容,还是非常非常细的,虽然日常开发大都用不到。现在说不定可能把书喂给gpt当知识库用更合理(
  5. nextjs、webpack社区更多使用的框架:经常有人戏谑说前端是配置config文件的书写工程师,各种属性定义都可以通过官网查到,这里把他归为一类“语言语法”也毫不为过
  6. 算法和设计模式

工程化流程

  • 工程化本身的内容很多,但也离不开一些基础知识,可以说前端社区最重、最劝退、最多解决方案的部分,就在这里。
  • 工程化其实是在做技术产品。产品经理需要知道用户想要什么,用户也会不断提出自己的需求。这里又会回到乔布斯说的,是需求驱动技术,还是技术决定了需求的问题。
  • 都有什么需求?这样罗列下来就是工程化的部分了。

时间更短、体积更小的编译

  • 为啥前端需要编译,可能是尝试进入前端学习门槛的第一个重要问题
  • webpack 提供的各种loader、loader的原理
  • 体积更小,除了简单的混淆压缩之外,更为重要的是两个方面,treeshaking和lazy
  • 前者是去除代码中不需要的部分,又涉及到webpack是怎么标记变量巴拉巴拉
  • 后者广义上就是拆包,也是把给用户展现不需要的部分,放到其他时机加载。这里的不需要的部分和其他时机又有很多。
  • Split chunk使用到的浏览器缓存
  • 近来最卷的以vite为首提供的es module打包dev环境,确实很快,浏览器支持果然才是最终方案,但可惜原来包袱太重,esm支持的包还是需要进一步迭代,现阶段还是只能dev使用吧
  • 另一方面是各种底层编译器的竞争,这块我实在不懂。esbuild为什么这么快。让我研究一下rspack干了啥
  1. 更方便的定制中间件处理

当你提供了上面一套基准方案后,你的产品被大伙使用,自然会出现社区生态。原来的中间件设计方案就更为关键。

  1. 方便的共享复用
  • 这里涉及到包定义一大堆cjs,esm的部分
  • 然后是各个包之间是怎么互相引用,循环引用是什么

比较常见的共享

  • 发布npm包
  • monorepo下的workspace的npm包
  • 基于nextjs transpilePackages的类似合并打包的思路
  • 基于webpack5 模块联邦的复用
  • 基于微前端沙箱,直接bundle级别的复用
  1. 最少的配置
  • 最少的配置本身是一个伪命题,基于某某名言“复杂度不会凭空消失”,不管是各种上层框架,多半是基于“约定”,或者更”直觉”的想法,定义一些属性,当你要修改的时候,又得爬到它们的官网,搜索对应的开关配置是什么
  • 因此这里的点其实是等于上面熟练度提到的语法及语法糖部分

工作流

页面渲染流程

这本身也是react vue这部分框架做的事情

  • 数据驱动dom操作方便已经成为必然
  • 不得不说虚拟dom的设计

数据管理

跨端版本

基于虚拟dom的概念,上面的前端在其他端可以说重新来了一遍。

小程序特有的同步机制,以及原生移动端组件,事件。打包发版,确实是又有一套生态。

然而我基本都没做过。

Nodejs也是我不太熟悉的领域。虽然写过bff层,但是严格说这块属于后端。但是卷就是要把别人干的活颁给自己干。

说起来sass这类服务应该也是归于这里吧,服务我理解的重点应该是,稳定性,可扩容性,sass应该还包括冷启动调用的问题种种

https://github.com/ascoders/weekly

https://github.com/ascoders/weekly/blob/master/设计模式/167.精读《设计模式 - Abstract Factory 抽象工厂》.md

因为精读那里就很全了,这边也不做分享级别的产出,这边可以以一种

温故而知新的思路来看。

当然我只会瞎写自己的理解。

工厂&接口

  1. 抽象工厂

工厂生产需要各种零部件。

但是工厂只需要零部件满足某些条件而已。

  • 生产车子需要轮子,但是轮子只需要能滚就行了。
  • 甚至不关心是不是圆的。这样的好处是后续换别的牌子的轮子,也不需要过多的改动。

抽象工厂的思路其实是接口开发的标准例子。也是现代生产的”标准化流程”的体现

问题:显然接口开发的好处在于你需要有不同的轮子换着装。但是如果你要改变工厂内容,比如要加个自动驾驶,加个后视镜。新的部件的增加确实可以在工厂修改。但是显然也违背了开闭。

  1. Builder 生成器

这个很有意思。其实是上面抽象工厂包一层。其实不用过多理解这个事。

在工厂中,你还需要把每个零件,按你想要的零件传给工厂。

在Builder里面,你更像是甩手掌柜。我今天就把工厂设定为只要这几种类型的零件,组成流水线去取就可以了。

也就是不会在通过 new Car()

而是CarBuilder.setxxx().run()

  1. Factory Method(工厂方法)

我家的灯坏了,我把灯泡拧下来,随便去五金店找个就换了,因为灯泡是满足接口设计规范的。也就是用抽象工厂的思路设计的。

其实1的本质是简单工厂,这里的意思是接口标准工厂。

问题很明显,就是所有的子类产品都需要满足接口规范。

生产的所有灯泡,都需要满足接口。

  • 所有设计模式的优势同样都是劣势。
  1. Prototype(原型模式)

这个其实很简单,javascript的叫做prototype。java的叫做static

其实就叫做,所有实例都共享的静态方法。

这里特指共享的”复制”方法。

比如生产钥匙new Key()的实例

每一把钥匙除了自己私有的开锁方法,也可以有公共都一样的复制方法。毕竟复制应该是一样的。

(这例子举得不太好。

Untitled 15.png

原型的语义是,复制会比新建更快。因为新建需要找工厂配零件。

复制一般是类似继承。同时复制在可以用”原型链”的设计,节约很多资源。

  1. 单例模式
  • 我感觉单例有且只有这一种用法和解释,当然可以加逻辑
  • 单例的目的就是为了单例。当然也分饿汉式懒汉式
class Ball {
  private _instance = undefined

  // 构造函数申明为 private,就可以阻止 new Ball() 行为
  private constructor() {}

  public static getInstance = () => {
    if (this._instance === undefined) {
      this._instance = new Ball()
    }

    return this._instance
  }
}

// 使用
const ball = Ball.getInstance()

Adapter适配器模式

适配器模式的本质是包一层。

  • 不同国家🔌插头不通用,我可以插到一个转接器上面
  • 220v的插座,可以用变压器转出来需要使用的电源。

数据库ORM

  • ORM屏蔽了SQL语法的区别,对于通用功能,ORM会自己做转换。

适配器本质也是一个:面向接口编程的思路

  • 但是插座Adapter有一个很明显的区别,他是一个兼容的一层。也就是像一根typec = typea的数据线。只做兼容转换的事情。把本来不能用的接口,适配兼容成可以用的一层。
  • 我们一般说没有什么事情是兼容不了的,只要包一层。指的就是这个

问题:

需要用适配器本身本身就是问题。如果都是你设计的代码,你的代码不应该会有不同类型的接口。

  • 除非你是在兼容旧代码,基于各种成本的原因。不希望改造旧代码。而采用包一层的方式来伪装成可以用的新代码接口。
  • 当然除了旧代码,还有三方库,都是适用的。

桥接模式 Bridge

说到桥接我第一反应是,家里宽带的光猫改成桥接。将本来在光猫的拨号行为,让路由器来进行拨号。

但其实跟这个毫无关系。

我列一下文中写的这个意图:将抽象部分与它的实现部分分离,使它们可以独立地变化。

简直是天书。。。根本看不懂

这个例子很好:

窗口(Window)类的派生

假设存在一个 Window 窗口类,其底层实现在不同操作系统是不一样的,假设对于操作系统 A 与 B,分别有 AWindow 与 BWindow 继承自 Window,现在要做一个新功能 ManageWindow(管理器窗口),就要针对操作系统 A 与 B 分别生成 AManageWindow 与 BManageWindow,这样显然不容易拓展。

无论我们新增支持 C 操作系统,还是新增支持一个 IconWindow,类的数量都会成倍提升,因为我们所做的 AMangeWindow 与 BMangeWindow 同时存在两个即以上的独立维度,这使得增加维度时,代码变得很冗余。

class Window {
  private windowImp: WindowImp

  public drawBox() {
    // 通过画线生成 box
    this.windowImp.drawLine(0, 1)
    this.windowImp.drawLine(1, 1)
    this.windowImp.drawLine(1, 0)
    this.windowImp.drawLine(0, 0)
  }
}

// 拓展 window 就非常容易
class SuperWindow extends Window {
  public drawIcon {
    // 通过自定义画线
    this.windowImp.drawLine(0, 5)
    this.windowImp.drawLine(3, 9)
  }
}

桥接模式的精髓,通过上面的例子可以这么理解:

Window 的能力是 drawBox,那继承 Window 容易拓展 drawIcon 吗?默认是不行的,因为 Window 并没有提供这个能力。经分析可以看出,划线是一种基础能力,不应该与 Window 代码耦合,因此我们将基础能力放到 windowImp 中,这样 drawIcon 也可以利用其基础能力画线了。

问题和评价

  • 其实是一种更深层的抽象。
  • 理论上这里通过继承的方式,让superwindow获得了drawLine的底层能力。
    • 这是刚好也是window,所以实现起来看起来比较自然
    • 如果你判断drawLine属于更公共的能力,这种做法就不一定合适了
    • 反而可能让不属于window的人,为了使用这个能力,强行extends了window

组合模式 Composite

  • 其实就是所有嵌套的树状结构
    • 存在部分-整体的关系。
    • 组合模式需要抽象出来一个对象作为这个部分-整体的统一操作模型。

操作系统的文件夹与文件

操作系统的文件夹与文件也是典型的树状结构,为了方便递归出文件夹内文件数量或者文件总大小,我们最好设计的时候就将文件夹与文件抽象为文件,这样每个节点都拥有相同的方法添加、删除、查找子元素,而不需要关心当前节点是文件夹或是文件。

问题和评价:

本质是为了抹平树状结构带来的复杂度。

但是如果你树和节点的差异很大。就没必要这样做了。

Decorator 装饰器模式

这个和adapter很像,也叫包一层。

但是adapter包一层是为了兼容,装饰器包一层,是为了增强功能。

例如照片想包一层相框,变成带相框的照片。

那么他就要找能包相框的工具了。

这里有一个很明显的对应关系:照片的多种多样的,但是相框是固定接口设计的(例如就只是木框能挂的相框)

为什么不继承生成子类?

因为带相框的照片不属于照片的子类。

这等于是一种更简易灵活的实现。

Facade外观模式

意图:为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

其实就是服务员模式。

因为消费者不需要了解厨房做菜各种问题,甚至不想知道有什么菜,甚至菜单也不想看。

我直接找你服务员。

吃什么?

图书管理员

图书馆是一个非常复杂的系统,虽然图书按照一定规则摆放,但也只有内部人员比较清楚,作为一位初次来的访客,想要快速找到一本书,最好的办法是直接问图书管理员,而不是先了解这个图书馆的设计,因为你可能要来回在各个楼宇间奔走,借书的流程可能也比较长。

图书管理员就起到了简化图书馆子系统复杂度的作用,我们只要凡事询问图书管理员即可,而不需要关心他是如何与图书馆内部系统打交道的。

最多跑一次便民服务

浙江省推出的最多跑一次服务非常方便,很多办事流程都简化了,无论是证件办理还是业务受理,几乎只要跑一次,而必须要持续几天的流程也会通过手机短信或者 App 操作完成后续流程。

这就相当于外观模式,因为政府系统内部的办事流程可能没有太大变化,但通过抽象出 Facade(外观),让普通市民可以直接与便民办事处连接,而不需要在车管所与驾校之间来回奔波,背后的事情没有少,只是便民办事处帮你做了。

如果内部操作足够复杂,添加一个服务员是非常好的。

但是如果你内部只是一些小操作,甚至我可能需要帮你搞点别的。我找服务员可能就画蛇添足,效率低下了。

Flyweight 享元模式

  • 这个单词指(拳击或其他比赛中的)特轻量级选手,次最轻量级选手(体重48至51公斤) ;
  • 很奇怪的一个翻译

网盘存储

当我们上传一部电影时,有时候几十 GB 的内容不到一秒就上传完了,这是网盘提示你,“已采用极速技术秒传”,你会不会心生疑惑,这么厉害的技术为什么不能每次都生效?

另外,网盘存储时,同一部电影可能都会存放在不同用户的不同文件夹中,而且电影文件又特别巨大,和富文本类似,电影文件也只有存放位置是不同的,而其余内容都特别巨大且只读,有什么办法能优化存储呢?

感觉这个也没啥好说的:

意图:运用共享技术有效地支持大量细粒度的对象。

共享技术可以理解为缓存,当一个对象创建后,再次访问相同对象时,就不再创建新的对象了,而只有在访问没有被缓存过的对象时,才创建新对象,并立即缓存起来。

Proxy代理模式

意图:为其他对象提供一种代理以控制这个对象的访问。

这个在中文语义下,很好理解。

这里有两个比较重要的关键词:控制访问

我找中介找房子,像是我代理了中介帮忙,但是事实上中介并没有我钱包的访问权,我也未提前支付中介费用。

我找私募基金管理资产,就有这味道了,他可以控制我资产的分配。我把钱的访问权、控制权封装成一个接口,通过合同约束,给他进行访问。

对于业务层的我,这种调用是无感知的。

代理模式十分好理解,但是这个模式的难点在于:为什么要使用代理,什么时候应该使用代理

  1. 基于保护原则,做权限控制

代理的显然诉求是,不希望侵入,也就是基于开闭原则。

  1. 对于开销大的对象使用代理,以按需引用。

这跟需要保护是一样的,保护是因为重要,那我们理解开销大也是一种重要就可以了。

  1. 在对象访问与修改时要执行一些其他逻辑,适合在代理层做。

这就是纯属的逻辑解耦,刚好代理模式对代码侵入比较小。

责任链模式 Chain of Responsibility

意图:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

这个模式很有意思,我在学习编程的很早就看到了这个,但是当时怎么也理解不通。

当时我的问题是:为什么要按链传递,为什么不能一开始解决。传递不会很乱吗?

原因是在设计的时候:这条链本身就是基于结构,或者说基于设计,是为了处理各类问题的。

就像是你定义了一层汇报关系。

每个汇报关系的职能,只会处理他感兴趣的。

其实这个用例子举还不太好。

还是KOA的洋葱中间件,直接就能理解了。

JS 事件冒泡机制

其实 JS 事件冒泡机制就是个典型的职责链模式,因为任何 DOM 元素都可以监听比如 onClick,不仅可以自己响应事件,还可以使用 event.stopPropagation() 阻止继续冒泡。

在中间件机制的例子中,后端 Web 框架对 Http 请求的处理就是个运用职责链模式的典型案例,因为后端框架要处理的请求是平行关系,任何请求都可能要求被响应,但对请求的处理是通过插件机制拓展的,且对每个请求的处理都是一个链条,存在处理、加工、再处理的逻辑关系。

Command(命令模式)

意图:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

这个跟前面有个模式比较重复。

外观模式。

点菜是命令模式

为什么顾客会找服务员点菜,而不是直接冲到后厨盯着厨师做菜?因为做菜比较慢,肯定会出现排队的现象,而且有些菜可能是一起做效率更高,所以将点菜和做菜分离比较容易控制整体效率。

其实这个社会现象就对应编程领域的命令模式:点菜就是一个个请求,点菜员记录的菜单就是将请求生成的对象,点菜员不需要关心怎么做菜、谁来做,他只要把菜单传到后厨即可,由后厨统一调度。

这里的点菜员,你可以理解为暴露的外观。

也可以认为是这个暴露的命令。

但是这里更强调的是,产生了排队队列和日志、然后后厨统一对这些队列进行分配。

—有点累了,不想写这玩意了。

解释器模式

他用了语言的解释器来做比喻。

整个感受是:对于更底层不可控的内容,通过多加一层,减少上层的细节处理。

其实是属于细节的抽象处理,多加一层。多加一层即可以做统一结构,也可以减少上层对不必要信息的处理。事实上属于信息复杂度转换,但是没有损失信息内容。也就是解释本身的语义:你听我给你解释,不是这样的官人。

迭代器模式

这是一种特指,设计模式这个归类比较奇怪。

迭代器也是一种封装细节的逻辑,可以认为是命令模式的一种。

同时迭代器在一定程度上可以屏蔽数组和其他类型的聚合细节,因为迭代器只需要对外暴露下一个next()

我只想要下一个,你就是那个传送带上的夹子

中介者模式

只是一种组织,一种管理方式。

通过提供管理者,来降低每个部分之间的耦合程度。

备忘录模式

意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

其实就是快照存档。

但是是有目的的,为什么将这个称为一种设计模式。

因为这种拍照需要你对原始数据做一些配合处理,例如序列化之类的。

拍照自然就包括最重要的恢复。

怎么拍,什么时机拍,取决于需求以及照片的大小。

最经典的两个应用就是:redo、undo各种编辑器内的。以及游戏中的保存。

如果往另外一个方向思考,前端的store也类似属于这种模式。

观察者模式 Observer

意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

其实我更喜欢订阅模式这个说法,但是它们又希望这两者有一定的区别。

观察者模式应用在生活的各个地方。

集权方,由于缺少用户的信息,需要向用户发生信息通知,所以就需要每个用户来订阅他。

rss就是典型。

当然你订阅报纸,这种上古时代的事情我就不说了。

addlistener, removeListener 在dom在已经得到比较多的应用。

我要说的是,这个模式不适合做过多的逻辑处理。

他的语义在生活中听起来像是:一…就的一个逻辑。

比如:你一听到火警报警声,你就跑下楼。

这个按钮一有点击,你就搞啥。

听起来很合理,但是当listener增加之后,整个逻辑从阅读上没有什么问题。

从理解上,却变成了各种类型各异的订阅。

最致命的是:在排查问题的时候,这些成千上百的订阅全部会化为迷宫的开关陷阱,一旦某个开关触发,这个陷阱就会打开/关闭。

State状态模式

意图:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

这个是一个相对的概念

也就是有一个对外封装的完善接口。

内部状态变成可以典型的分成两种:

  • 一种是通过if else去区分逻辑,大部分情况下都非常有效
  • 第二种是拆分成各个不同的类,可能继承同一个接口类去做分别实现。
    • 在团队接口人的例子里面,团队中的每个成员负责不同的事情,就类似于这个概念
    • 在台灯的例子里,我们将每个台灯的灯的状态,就拆分成了不同个类,那么每种灯的状态只需要知道按钮点击后,自己应该找哪种下一个灯的人去变化就行了。
  • 状态模式的设计意在 减少每一种状态自己理解范围,只需要知道局限的知识就可以了。但是对于全局的接口人,可以通过增加各种状态,以及指导各种流程。对外显现出的是,这个全局接口全知全能,各种状态都能干的情况。

在实际开发中,很容易滥用状态模式(当然很多人根本不知道)

事实上if else在90%的情况下,对于前端日常开发来说都非常有效。

反而难以维护的代码,多是过度引入了状态模式

设计模式的本质是降低复杂度、耦合度,便于人们阅读。

if else的使用本身是完美满足这几个方面的。

只是在长度和限制上,if else很难横向做扩展。

Strategy(策略模式)

意图:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。

这是一个非常直观名字的模式。

事实上就是提供多种选择。每个选择都能到达结果。

他们之间的切换没有障碍,新增和减少,也不会根本上引起这个结果的变化。

最典型就是:手机地图导航,你想从A地到B地,可以选择步行、驾车、地铁。

这个策略,和我们日常理解的策略是一致的。

策略模式很明显是为了:方便横向扩展更多的策略,以及对比各个策略。

排序算法也是另外一个比较典型的例子。

Template Method(模版模式)

意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

就是模板。

template将所有都说完了。

这里需要注意我们在使用的时候:要明确设计好模板的范围,和模板变量。那些是我们这个骨架应该关注的,那些是不需要的,那些是用户输入的关键,哪些是用户灵活处理的

Visitor(访问者模式)

基本可以等同于代理模式

本质也是包了一层,减少对原始的修改。

这边更关注的是visit这个能力。


杀青啦~