js设计模式(四)-结构型模式(代理模式/组合模式/装饰器模式)

115 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第19天,点击查看活动详情

代理模式(委托模式)

正向代理和反向代理

正向代理: 一般的访问流程是客户端直接向目标服务器发送请求并获取内容,使用正向代理后,客户端改为向代理服务器发送请求,并指定目标服务器(原始服务器),然后由代理服务器和原始服务器通信,转交请求并获得的内容,再返回给客户端。正向代理隐藏了真实的客户端,为客户端收发请求,使真实客户端对服务器不可见。例如:vue项目里面里面的 proxy 和 axios 守卫中的 request 拦截。

反向代理: 与一般访问流程相比,使用反向代理后,直接收到请求的服务器是代理服务器,然后将请求转发给内部网络上真正进行处理的服务器,得到的结果返回给客户端。反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。常用于处理跨域请求。

虚拟代理

虚拟代理就是把一些开销很大的对象,延迟到真正需要它的时候才去创建执行。

// 图片懒加载
const myImage = (() {
    const imgNode = document.createElement('img');
    document.body.appendChild( imgNode );
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();
 
const proxyImage = (() {
    const img = new Image();
    img.onload = () => {
        myImage.setSrc( this.src );
    }
    return {
        setSrc: src => {
            myImage.setSrc('http://seopic.699pic.com/photo/40167/3716.jpg_wh1200.jpg');
            img.src = src;
        }
    }
})();
 
proxyImage.setSrc('http://seopic.699pic.com/photo/40167/7823.jpg_wh1200.jpg');

缓存代理

缓存代理就是可以为一些开销大的运算结果提供暂时的存储,下次运算时,如果传递进来堵塞参数跟之前一致,则可以直接返回前面存储的运算结果。

const getFib = (number) => {
    if (number <= 2) {
        return 1;
    } else {
        return getFib(number - 1) + getFib(number - 2);
    }
}
 
const getCacheProxy = (fn, cache = new Map()) => {
    return new Proxy(fn, {
        apply(target, context, args) {
        const argsString = args.join(' ');
        if (cache.has(argsString)) {
            // 如果有缓存,直接返回缓存数据 
            console.log(`输出${args}的缓存结果: ${cache.get(argsString)}`);
            return cache.get(argsString);
        }
        const result = fn(...args);
        cache.set(argsString, result);
        return result;
        }
    })
}
const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); 

用 ES6 的 Proxy 构造函数实现代理

ES6 所提供 Proxy 构造函数能够让我们轻松的使用代理模式:

const proxy = new Proxy(target, handler);

Proxy 构造函数传入两个参数:要代理的对象 和 用来定制代理行为的对象。(如果想知道 Proxy 的具体使用方法,可参考阮一峰的《 ECMAScript入门 - Proxy 》)

组合模式

组合模式允许你将对象组合成树形结构来表现整体和部分的层次结构,让使用者可以以一致的方式处理组合对象以及部分对象。

组合模式的适用场景:如果对象组织呈树形结构就可以考虑使用组合模式,特别是如果操作树中对象的方法比较类似时。

优点

  • 忽略组合对象和单个对象的差别,对外一致接口使用;
  • 解耦调用者与复杂元素之间的联系,处理方式变得简单。

缺点

  • 树叶对象接口一致,无法区分,只有在运行时方可辨别;
  • 包裹对象创建太多,额外增加内存负担。
// 文件夹类
class Folder {
    constructor(name, children) {
        this.name = name
        this.children = children
    }
    
    // 在文件夹下增加文件或文件夹
    add(...fileOrFolder) {
        this.children.push(...fileOrFolder)
        return this
    }
    
    // 扫描方法
    scan(cb) {
        this.children.forEach(child => child.scan(cb))
    }
}
 
// 文件类
class File {
    constructor(name, size) {
        this.name = name
        this.size = size
    }
    
    // 在文件下增加文件,应报错
    add(...fileOrFolder) {
        throw new Error('文件下面不能再添加文件')
    }
    
    // 执行扫描方法
    scan(cb) {
        cb(this)
    }
}
 
const foldMovies = new Folder('电影', [
    new Folder('漫威英雄电影', [
        new File('钢铁侠.mp4', 1.9),
        new File('蜘蛛侠.mp4', 2.1),
        new File('金刚狼.mp4', 2.3),
        new File('黑寡妇.mp4', 1.9),
        new File('美国队长.mp4', 1.4)]),
    new Folder('DC英雄电影', [
        new File('蝙蝠侠.mp4', 2.4),
        new File('超人.mp4', 1.6)])
])
 
console.log('size 大于2G的文件有:')
 
foldMovies.scan(item => {
    if (item.size > 2) {
        console.log(`name:${ item.name } size:${ item.size }GB`)
    }
})
 
// size 大于2G的文件有:
// name:蜘蛛侠.mp4 size:2.1GB
// name:金刚狼.mp4 size:2.3GB
// name:蝙蝠侠.mp4 size:2.4GB

我看了几个文档,大家都把上面这个例子作为demo去理解组合模式。

说下我的理解:首先组合模式无愧于结构型模式这个标签,这种模式里面 核心是 foldMovies 这个数据结构,试想一下,我们经常被面试的一道题:树形结构拍平或者扁平结构转换树状结构。

如果我们提前封装一个工具,可以把树状结构或者扁平化结构使用组合模式+适配器模式的方式管理起来,尤其是需要频繁操作的场景下,是不是会很方便的进行遍历和CURD?

装饰器模式

可以将装饰器理解为游戏人物购买的装备,例如LOL中的英雄刚开始游戏时只有基础的攻击力和法强。但是在购买的装备后,在触发攻击和技能时,能够享受到装备带来的输出加成。我们可以理解为购买的装备给英雄的攻击和技能的相关方法进行了装饰。

参考:JavaScript设计模式(五)-装饰器模式

// 把原来的老逻辑代码放在一个类里
class HorribleCode {
  control() {
    console.log("我是一堆老逻辑");
  }
}

// 老代码对应的装饰器
class Decorator {
  // 将老代码实例传入
  constructor(oldHC) {
    this.oldHC = oldHC;
  }
  control() {
    this.oldHC.control();
    // “包装”了一层新逻辑
    this.newHC();
  }
  newHC() {
    console.log("我是新的逻辑");
  }
}

const horribleCode = new HorribleCode();

//这里我们把老代码实例传给了 Decorator,以便于后续 Decorator 可以进行逻辑的拓展。
const decorator = new Decorator(horribleCode);

decorator.control();

// 我是一堆老逻辑
// 我是新的逻辑

image.png

个人感觉 类的继承 似乎就是在实现这个模式,但是看了其他大佬分享的内容以后又感觉还是有点区别的:

  1. 类的继承是原生的写法,而装饰器模式则是使用一种结构来将两个类进行关联
  2. 类的继承耦合度要高的多,而装饰器模式就灵活多了,正如模式介绍中所说的,LOL 中英雄的装备,可以买/卖/换。

粘一下大佬的原分析:

image.png

参考文档