Published on

代理模式详解

Authors
  • avatar
    Name
    青雲
    Twitter

在软件设计中,代理模式(Proxy Pattern)是一种结构型设计模式。它为其他对象提供一种代理,并由代理对象控制对原对象的访问。代理模式常用于延迟对象的创建、控制对对象的访问以及提高系统的安全性。

为什么需要代理模式?

直接购买商品的流程可能涉及到找到卖家、沟通、支付、确认收货等复杂的步骤。这里,如果我们把每个买家和卖家看成是一个个对象,那么直接的交易就意味着对象间直接的交互。

现实生活中,我们现在很少会直接和卖家进行沟通和交易,而是通过购物平台(淘宝、拼多多、京东、抖音等)来完成这一系列操作。在这个场景中,购物平台就扮演了一个"代理"的角色。我们作为买家,通过代理(购物平台)来间接与卖家进行交云。

代理的作用

  1. 隐藏真实交易双方的细节:作为买家,我们不需要知道卖家的具体信息,也不需要与卖家直接沟通。所有这些都通过购物平台这个代理来完成。
  2. 增加安全性:购物平台会对交易双方进行验证,确保交易的安全性。
  3. 提供附加服务:除了基本的买卖功能,购物平台还会提供物流跟踪、售后服务、评价系统等额外的服务,这些是直接交易很难提供的。
  4. 减少直接交互带来的复杂性:通过代理,复杂的交易流程被简化,用户体验因此得到提升。

在软件开发中,我们有时需要在不改变现有代码的前提下,增加某些功能或逻辑。代理模式通过引入代理对象,巧妙地控制对原始对象的访问,使我们可以在不影响原始对象设计的情况下,轻松扩展其功能。

例如,在一个庞大的系统中,我们可能需要延迟某些非常耗时的对象的初始化,或者需要对某些关键操作进行日志记录和权限控制。通过代理模式,我们可以有效地解决这些问题。

基本概念

代理模式包括以下几个主要部分:

  1. 代理接口(Proxy Interface): 定义了代理类和原始类共有的方法,使得代理类可以用于替代原始类。
  2. 原始类(Real Subject): 实现了代理接口的具体类,是被代理的对象。
  3. 代理类(Proxy Class): 实现了代理接口,包含了对原始类对象的引用,并在调用方法时对其进行一些控制。

实现示例

我们将通过一个具体示例来展示代理模式的实现过程。假设我们有一个视频播放系统,在系统中我们可以播放大量的视频文件。由于初始化视频对象可能非常耗时,我们可以通过代理模式来实现视频对象的延迟加载。

定义代理接口和原始类

// 代理接口,定义统一的方法
interface Video {
  play(): void;
}

// 具体原始类,表示实际的视频对象
class RealVideo implements Video {
  private fileName: string;

  constructor(fileName: string) {
    this.fileName = fileName;
    this.loadVideoFromDisk();
  }

  private loadVideoFromDisk(): void {
    console.log(`Loading video: ${this.fileName}`);
  }

  play(): void {
    console.log(`Playing video: ${this.fileName}`);
  }
}

定义代理类

// 代理类,用于控制对实际视频对象的访问
class ProxyVideo implements Video {
  private realVideo: RealVideo;
  private fileName: string;

  constructor(fileName: string) {
    this.fileName = fileName;
  }

  play(): void {
    if (!this.realVideo) {
      this.realVideo = new RealVideo(this.fileName); // 延迟初始化
    }
    this.realVideo.play();
  }
}

使用代理类播放视频

// 使用代理类来播放视频
const video = new ProxyVideo('example_video.mp4');

// 第一次播放,进行初始化加载
video.play();

// 第二次播放,不需要初始化
video.play();
Loading video: example_video.mp4
Playing video: example_video.mp4
Playing video: example_video.mp4

应用场景

代理模式适用于以下场景:

  1. 延迟加载:当某些对象初始化非常耗时时,可以通过代理模式实现延迟加载,只有在需要时才进行初始化。
  2. 访问控制:通过代理模式控制对原始对象的访问,添加权限验证等逻辑。
  3. 日志记录:在代理类中添加日志记录功能,记录对原始对象的访问情况。
  4. 远程代理:在分布式系统中,代理对象充当实际远程对象的本地代表。

前端开发中的应用

事件代理(Event Delegation)

在处理大量类似的事件监听器(比如列表中的每个项的点击事件)时,为每个元素单独添加事件监听器会影响性能和资源消耗。事件代理利用了事件冒泡的机制,只在父级元素上设置一个监听器来管理所有子元素的事件。这样,不仅减少了监听器的数量,还提高了程序的效率。

document.getElementById('list-container').addEventListener('click', function(e) {
  if(e.target && e.target.nodeName == 'LI') {
    console.log(e.target.id + ' was clicked');
  }
});

虚拟代理(Virtual Proxy)

虚拟代理可以用来控制对一些开销较大的操作的访问,例如延迟加载(Lazy Loading)图片或其他资源。直到这些资源真正需要显示在屏幕上时才加载它们,减少初始加载时间,提升用户体验。

class ImageProxy {
  private imageElement: HTMLImageElement;
  private realImage: HTMLImageElement;

  constructor(imageElement: HTMLImageElement, url: string) {
    this.imageElement = imageElement;
    this.realImage = new Image();
    this.realImage.onload = () => {
      this.imageElement.src = this.realImage.src;
    };
    this.realImage.src = url;
  }
}

// 使用虚拟代理懒加载图片
const imgElement = document.getElementById('myImage') as HTMLImageElement;
new ImageProxy(imgElement, 'https://example.com/image.jpg');

保护代理(Protection Proxy)

在前端开发中,使用保护代理可以控制对某些敏感信息的访问。例如,只有在用户完成身份验证后才允许访问某个 API 或获取某些数据。

function fetchData(api) {
  // 简化示例:检查是否有访问权限
  if (!sessionStorage.getItem('authenticated')) {
    console.log('Access denied. User not authenticated.');
    return;
  }
  // 假装发起API请求
  console.log('Fetching data from', api);
}

const proxyFetchData = new Proxy(fetchData, {
  apply(target, thisArg, argumentsList) {
    if (sessionStorage.getItem('userRole') !== 'admin') {
      console.log('Access denied. Admin role required.');
      return;
    }
    return Reflect.apply(...arguments);
  }
});

缓存代理(Caching Proxy)

缓存代理可以用于缓存重复请求的结果,以避免不必要的计算或网络请求,优化性能。特别是在处理大量数据和高频访问的场景中非常有用。

const expensiveFunctionProxy = new Proxy(expensiveFunction, {
  cache: new Map(),
  apply(target, thisArg, argumentsList) {
    const cacheKey = argumentsList.toString();
    if (!this.cache.has(cacheKey)) {
      const result = Reflect.apply(target, thisArg, argumentsList);
      this.cache.set(cacheKey, result);
    }
    return this.cache.get(cacheKey);
  }
});

开源库中的典型应用

Vue.js 数据响应机制

Vue.js 数据响应机制中广泛使用代理模式。在 Vue 3 中,使用了 ES6 的 Proxy 来监听数据对象的变化,从而实现响应式。当数据变化时,Proxy 能够拦截这些变化,并通知视图进行更新。

const data = { title: 'Hello, Vue 3' };

const proxyData = new Proxy(data, {
  get(target, property) {
    console.log(`Getting ${property}: ${target[property]}`);
    return Reflect.get(...arguments);
  },
  set(target, property, value) {
    console.log(`Setting ${property}: ${value}`);
    return Reflect.set(...arguments);
  }
});

proxyData.title; // 访问属性时,`get`捕获器被触发
proxyData.title = 'Hello, Proxy'; // 修改属性时,`set`捕获器被触发

Webpack 的 HMR

Webpack 的 HMR 功能通过代理模式实现模块热替换。当开发者对代码进行更改时,HMR 代理能够拦截这些更改并通知浏览器替换旧模块,从而无需完全刷新页面就可以更新应用。这大大提高了开发效率,特别是对于大型应用,避免了频繁的全页面刷新。

// Webpack 配置文件中的 HMR 配置
module.exports = {
  // 其他配置项...
  devServer: {
    hot: true,  // 开启 HMR
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),  // 注册 HMR 插件
  ],
};

// 示例模块代码
if (module.hot) {
  // 模块自身更新
  module.hot.accept('./module-to-watch.js', () => {
    console.log('Module updated!');
  });

  // 模块销毁时清理
  module.hot.dispose(() => {
    console.log('Module disposed!');
  });
}
  1. 热替换插件:HotModuleReplacementPlugin 插件使得 Webpack 能够支持 HMR。
  2. 模块更新与替换:通过 module.hot.acceptmodule.hot.dispose,模块能够在更新时执行相关逻辑,避免整个页面刷新。

Redux 中间件

Redux 库在处理异步操作和日志记录等功能时广泛使用代理模式,尤其是通过中间件机制。中间件本质上就是一个代理,它可以拦截、修改甚至拒绝 Redux 动作,实现了对数据流的高度控制。

const loggerMiddleware = store => next => action => {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
};

Fetch API 拦截器

虽然 Fetch API 原生并不支持拦截器,但可以通过代理模式实现。这样的应用允许开发者拦截和修改 HTTP 请求与响应,是实现例如添加通用请求头、响应处理和错误处理的一个有效手段。

const fetchProxy = new Proxy(fetch, {
  apply(target, thisArg, argumentsList) {
    console.log(`Fetching: ${argumentsList[0]}`);
    return Reflect.apply(...arguments).then(response => {
      // Do something with the response
      return response;
    });
  }
});

MobX 中的状态管理

MobX 是另一个利用代理模式实现响应式编程的库。通过使用 @observable 装饰器或 makeObservable 方法,MobX 能够将普通的 JavaScript 对象转换为可观察的对象。当可观察对象的状态发生变化时,依赖这些状态的计算值或反应会自动更新。

import { makeAutoObservable } from "mobx";

class Todo {
  id = Math.random();
  title = "";
  finished = false;

  constructor(title) {
    makeAutoObservable(this);
    this.title = title;
  }

  toggle() {
    this.finished = !this.finished;
  }
}

优缺点

优点

  1. 控制对象访问:通过代理对象,可以控制对原始对象的访问,实现权限控制、延迟加载等功能。
  2. 增强对象功能:在不改变原始对象的前提下,通过代理对象增强其功能,如添加日志记录、性能监控等。
  3. 优化性能:通过延迟加载和减少对象创建,提高系统性能。

缺点

  1. 增加代码复杂性:引入代理对象后,增加了类的数量和代码的复杂性。
  2. 潜在的性能开销:代理对象通过引用原始对象进行操作,可能导致一些额外的性能开销。

总结

代理模式是一种功能强大的设计模式,能够提供对对象访问的精细控制并附加额外的操作,适用于多种场景,如性能优化、安全控制及系统资源管理等方面。在实现时,应当权衡其带来的好处与潜在的复杂性,以确保设计的高效与可维护。