当调用方不方便直接操作某个对象,又或者希望在访问对象之前或之后做某些操作时,可以使用代理模式。
举个例子,我们有一个存放着超多数据的对象,如果把这个对象直接在程序中初始化的话,肯定会占据很多内存;而且,程序不一定一开始就需要用到这个对象,也不一定需要这个对象的所有数据;所以,比较经济的方法是,把这个超大对象放在服务端,当程序需要的时候再发起网络请求去访问。
问题是,在程序中可能不止一处要用到这个对象,我们总不想把发起网络请求的逻辑到处复制粘贴吧。而代理模式的思想就是,提供一个代理对象(替身),把这个代理对象伪装成本体对象,然后把本体对象藏起来。调用方需要数据的时候,就去访问代理对象,其实调用方是分不清自己在访问代理对象还是本体对象的,它也根本不需要区分。
用图解释大概是这个样子:
代理模式大概有以下几种用途:
惰性初始化:在真正需要的时候才通过代理对象去初始化本体对象,一般是本体对象消耗很大的时候才需要这样做;
控制访问权限:代理对象负责甄别哪些调用方有权访问本体对象,对有权访问的调用方,代理对象把它们的请求转发给本体对象,对于无权访问的,就无视它们的请求;
打印日志:想要记录某个对象的访问日志时,可以用一个代理对象来做这件事,这样就不需要对本体对象做修改;
缓存请求结果:就像上例一样,代理对象负责发送网络请求并缓存请求结果,下次有同样的请求时可以直接返回缓存的数据;
智能引用:可以用一个代理对象来记录某个对象当前有多少个引用,如果没有调用方引用这个本体对象,这个对象的内存就可以被及时释放了;
唯一的修改是,把调用方的访问对象从本体对象改成代理对象,其他不变,因为代理对象是一个“伪本体对象”,在调用方看来,它和本体对象是一样的。
单例模式就是一个 class 永远都返回同一个实例,而且这个实例还可以在全局中访问到。
听起来是不是很像一个全局变量?
同样都是全局可访问,全局变量有被其他代码修改的风险,而单例模式提供的实例不能通过外部代码来修改替换,除非这个 class 暴露了修改实例的方法。
单例模式还能封装一些代码逻辑。
单例模式永远返回同一个实例对象,所以我们不能使用普通的构造函数来实现,因为每次 new 的时候都会创建一个新的实例。
其实如果是 JS 的普通构造函数语法的话,new 的时候如果函数中返回一个对象,那 new 出来的对象就会被丢弃,然后我们也可以把单例对象作为构造函数的静态属性缓存起来,不过这样我们也把单例对象暴露出去了。
我们得把构造函数和单例对象隐藏起来,然后另外暴露一个获取单例对象的方法。在这个方法中,我们需要实现的是:
因为 JS 的 class 还不支持私有属性,所以我们先用一个 IIFE 来实现单例模式。
let count = 0 const Singleton = (function () { // 缓存单例实例对象 let instance = null // 只有首次调用 getInstance 方法时才会用 new 调用 Constructor 方法 // 外部代码无法通过 new 调用 Constructor 创建实例对象 // Contructor 只会执行一次 function Constructor() { count++ } function getInstance() { // 首次调用,创建单例实例并缓存 if (!instance) { instance = new Constructor() } // 之后调用,直接返回缓存的实例对象 return instance } // 暴露获取单例对象的方法 return { getInstance } }()) const a = Singleton.getInstance() const b = Singleton.getInstance() console.log(a === b); // true console.log(count); // 1 复制代码
比如要压缩一张图片,可选择的压缩算法有 jpg
、png
、gif
等等。我们实现了一个 ImageCompressor
class,提供了 compress
方法来处理这个任务,代码如下。
class ImageCompressor { constructor() {} compress(img, algorithm) { let compressedImg = null switch (algorithm) { case 'jpg': // jpg 的压缩算法 // compressedImg = ... break case 'png': // png 的压缩算法 break case 'gif': // gif 的压缩算法 break default: break } return compressedImg } } const imageCompressor = new ImageCompressor() const pngImg = imageCompressor('exampleImageFile', 'png') const jpgImg = imageCompressor('exampleImageFile', 'jpg') 复制代码
可以看到在上面的代码中,所有压缩算法都是在 compress
方法中实现的,而在实际中,每种算法的逻辑都十分复杂,如果都写在同一个方法中,这个方法很快就会膨胀了。这种写法导致的问题就是,如果之后要修改或者新增某个算法,都很有可能会不小心影响到其他代码。
为了代码更容易维护,我们需要把这些算法拆分成独立的小块,并通过一个“代言人”来“管理”它们。
用“代言人”和“管理”好像不那么准确,但我又想不到什么词了,意会意会。
从以上代码中可以观察到,compress
的输入和输出是差不多的:
而不同的是:
遇到这种模式的问题我们都可以使用策略模式
来解决:
处理方法
抽离成各自独立的 class处理方法
就是一个解决问题的 策略
Context
class 来提供统一的对外接口,Context
内部再调用不同的 策略
方法假设要写一个可以同时处理两数加减乘除的函数,我们可能会实现成以下的样子:
doMath
函数接收两个操作数 a
和 b
,以及一个算术类型 operation
作为参数,然后根据不同的算术类型返回不同的数学计算结果。
const doMath = (a, b, operation) => { switch (operation) { case 'ADD': return a + b case 'MINUS': return a - b case 'MULTIPLY': return a * b case 'DIVIDE': return a / b default: return } } doMath(1, 2, 'ADD') // 3 doMath(1, 2, 'MINUS') // -1 doMath(1, 2, 'MULTIPLY') // 2 doMath(1, 2, 'DIVIDE') // 0.5 复制代码
可以观察到,多次调用 doMath
函数的共同点在于:
而不同点就在于:
这个问题模式就很适合使用策略模式来解决。
用 OOP 的形式来实现的话,我们先把上面的例子改写成 class 的形式吧,改写方式之一:
class SimpleMath { constructor() { this.operations = { ['ADD']: (a, b) => a + b, ['MINUS']: (a, b) => a - b, ['MULTIPLY']: (a, b) => a * b, ['DIVIDE']: (a, b) => a / b, } } calculate(a, b, operation) { return this.operations[operation](a, b) } } 复制代码
目前所有算法都是在 SimpleMath
中实现的,接下来我们尝试把每个算法抽离成独立的 class:
// 不同算法被抽离成不同的 class // 实现的效果是这些 class 的都应该有相同的实例方法,但这些实例方法做的事情不一样 // p.s. 按理说这些 class 都应该实现同样的 interface,但 JS 中并没有 interface class Add { calculate(a, b) { return a + b } } class Minus { calculate(a, b) { return a - b } } class Multiply { calculate(a, b) { return a * b } } class Divide { calculate(a, b) { return a / b } } // 原本的 SimpleMath 就只保留一个对具体算法实例的引用 operation // SimpleMath 并不关心具体使用什么算法,调用方在调用 SimpleMath 的时候把具体的算法对象传过来,SimpleMath 负责调用这个算法对象的 calculate 方法 class SimpleMath { constructor(operation) { // operation 保存着具体算法实例 this.operation = operation } calculate(a, b) { // SimpleMath 的 calculate 方法只是负责调用具体算法实例的 calculate 方法 return this.operation.calculate(a, b) } } // 调用方代码: // 调用方必须知道自己需要的是哪种算法,并把对应的算法实例传给 SimpleMath const add = new SimpleMath(new Add()) add.calculate(1, 2) // 3 const multiply = new SimpleMath(new Multiply()) multiply(1, 2) // 2 复制代码
问题
回答
策略模式中有 3 个角色:
SimpleMath
,是负责完成某项任务的角色,但只知道需要完成一项任务,并不知道任务具体应该如何完成,只有一个;Client 通过调用 Context 并指定某个 Strategy 来间接调用相应 Strategy 的某个方法。
其实不用 class 来实现,只要是符合这种思想的都是策略模式吧,把 strategy 拆分成函数也行吧,个人看法个人看法。
🌟github仓库地址