从送外卖到设计模式中的代理模式

77 阅读4分钟

从送外卖到设计模式中的代理模式

🚚 现实场景类比

外卖场景:

  • 商家 → 准备餐食并交给骑手
  • 骑手 → 作为代理,负责运输和交付
  • 干饭人 → 最终接收餐食

类比设计模式:

  • 真实对象(Real Subject) → 商家(准备餐食)
  • 代理(Proxy) → 骑手(控制交付流程)
  • 客户端(Client) → 干饭人(最终接收者)

✅ 代码实现(基于 JavaScript)

// 1️⃣ 抽象接口(抽象主题)
class Deliverer {
    deliver(target) {
        throw new Error("Method 'deliver' must be implemented.");
    }
}
​
// 2️⃣ 真实主题(商家)
class Restaurant extends Deliverer {
    constructor(name) {
        super();
        this.name = name;
    }
​
    deliver(target) {
        console.log(`${this.name} 准备餐食并交给 ${target.name}`);
    }
}
​
// 3️⃣ 代理(骑手)
class Rider extends Deliverer {
    constructor(name, restaurant) {
        super();
        this.name = name;
        this.restaurant = restaurant;
    }
​
    deliver(target) {
        if (this.validateAddress(target)) {
            console.log(`${this.name} 核对地址:${target.name}`);
            this.restaurant.deliver(target); // 调用真实对象
            console.log(`${this.name} 送达 ${target.name}`);
        } else {
            console.log(`⚠️ 地址错误,无法送达 ${target.name}`);
        }
    }
​
    validateAddress(target) {
        // 模拟地址校验
        return target.name !== "未知地址";
    }
}
​
// 4️⃣ 客户端(干饭人)
class Customer {
    constructor(name) {
        this.name = name;
    }
​
    receive() {
        console.log(`${this.name} 收到餐食`);
    }
}

🧩 使用示例

// 创建角色实例
const restaurant = new Restaurant("美味餐厅");
const rider = new Rider("闪电骑手", restaurant);
const customer = new Customer("干饭人");
​
// 通过代理(骑手)完成送餐
rider.deliver(customer); // 正常流程
customer.receive();      // 干饭人接收
​
// 测试异常情况
const invalidCustomer = new Customer("未知地址");
rider.deliver(invalidCustomer); // 地址错误,无法送达

📌 输出结果

闪电骑手 核对地址:干饭人
美味餐厅 准备餐食并交给 干饭人
闪电骑手 送达 干饭人
干饭人 收到餐食
⚠️ 地址错误,无法送达 未知地址

🧠 代码解析与设计模式映射

角色对应类说明
真实对象Restaurant负责核心业务逻辑(准备餐食)
代理Rider控制对真实对象的访问,添加额外功能(地址校验、日志记录)
客户端Customer最终使用者,通过代理获取服务
抽象接口Deliverer统一接口,确保代理和真实对象行为一致

✅ 代理模式的核心价值

  1. 职责分离

    • 商家只需专注准备餐食,骑手处理交付细节(地址校验、日志记录)
    • 干饭人无需关心配送过程,只需接收结果
  2. 增强功能

    • 骑手可扩展功能:如记录送餐时间、计算配送费、异常处理等
  3. 访问控制

    • 通过 validateAddress 方法限制非法地址的配送请求
  4. 解耦与灵活性

    • 可替换不同骑手或商家,不影响客户端调用逻辑

🚀 扩展思考

  • 虚拟代理:骑手可延迟加载餐食(如用户下单后才通知商家准备)
  • 缓存代理:记录已送餐地址,避免重复配送
  • 远程代理:骑手可对接多个城市商家,隐藏跨区域配送的复杂性

📌 总结

通过外卖场景的类比,我们清晰地看到了代理模式如何通过一个中间角色(骑手)控制对真实对象(商家)的访问。这种设计模式不仅提升了系统的灵活性和可维护性,还能在不修改原始逻辑的前提下扩展功能(如地址校验、日志记录),体现了设计模式“开闭原则”的精髓。

实际开发中的作用


✅ 示例 1:静态代理 —— 权限控制

🧩 场景

我们有一个 UserService 接口,包含 save 方法。我们希望在保存用户前进行权限校验,并在保存后记录日志。

✅ 实现

// 抽象主题(Subject)
class UserService {
    save(username) {
        throw new Error("Method 'save' must be implemented.");
    }
}
​
// 真实主题(Real Subject)
class UserServiceImpl extends UserService {
    save(username) {
        console.log(`保存用户: ${username}`);
    }
}
​
// 静态代理(Proxy)
class UserServiceProxy extends UserService {
    constructor() {
        super();
        this.realService = new UserServiceImpl();
    }
​
    save(username) {
        if (this.checkPermission()) {
            console.log('权限校验通过');
            this.realService.save(username);
            console.log('操作日志已记录');
        } else {
            console.log('权限不足,无法保存');
        }
    }
​
    checkPermission() {
        // 模拟权限检查
        return true;
    }
}
​
// 使用代理
const proxy = new UserServiceProxy();
proxy.save('Alice');

📌 输出结果:

权限校验通过
保存用户: Alice
操作日志已记录

✅ 示例 2:动态代理 —— 日志记录(使用 ES6 Proxy)

🧩 场景

我们希望对任意对象的方法调用进行日志记录,而不需要为每个类手动编写代理类。

✅ 实现

// 目标对象
const userService = {
    save: function(username) {
        console.log(`保存用户: ${username}`);
    },
    delete: function(userId) {
        console.log(`删除用户: ${userId}`);
    }
};
​
// 创建代理对象
const proxy = new Proxy(userService, {
    get(target, prop, receiver) {
        const originalMethod = Reflect.get(target, prop, receiver);
​
        if (typeof originalMethod === 'function') {
            return function(...args) {
                console.log(`调用方法: ${String(prop)}`);
                console.log('参数:', args);
                const result = originalMethod.apply(this, args);
                console.log('方法执行完毕');
                return result;
            };
        }
​
        return originalMethod;
    }
});
​
// 使用代理
proxy.save('Bob');
proxy.delete(1001);

📌 输出结果:

调用方法: save
参数: [ 'Bob' ]
保存用户: Bob
方法执行完毕
调用方法: delete
参数: [ 1001 ]
删除用户: 1001
方法执行完毕

✅ 示例 3:缓存代理 —— 函数结果缓存

🧩 场景

我们希望对某些计算成本较高的函数进行缓存,避免重复计算。

✅ 实现

// 原始函数:计算阶乘
function factorial(n) {
    console.log(`计算阶乘: ${n}`);
    let result = 1;
    for (let i = 1; i <= n; i++) {
        result *= i;
    }
    return result;
}
​
// 创建缓存代理
const cacheProxy = new Proxy(factorial, {
    apply(target, thisArg, args) {
        const key = JSON.stringify(args);
        if (!cacheProxy.cache) cacheProxy.cache = {};
        if (cacheProxy.cache[key] !== undefined) {
            console.log('命中缓存');
            return cacheProxy.cache[key];
        }
        const result = Reflect.apply(target, thisArg, args);
        cacheProxy.cache[key] = result;
        return result;
    }
});
​
// 使用代理
console.log(cacheProxy(5)); // 计算
console.log(cacheProxy(5)); // 命中缓存

📌 输出结果:

计算阶乘: 5
120
命中缓存
120

✅ 示例 4:虚拟代理 —— 图片懒加载

🧩 场景

我们希望图片在滚动到可视区域时才加载。

✅ 实现

// 模拟图片加载函数
function loadImage(src) {
    console.log(`加载图片: ${src}`);
}
​
// 创建虚拟代理
function createImageProxy(src) {
    const proxy = {
        showImage: function() {
            if (!proxy.loaded) {
                loadImage(src);
                proxy.loaded = true;
            }
        }
    };
    return proxy;
}
​
// 使用代理
const imageProxy = createImageProxy('big-image.jpg');
​
// 模拟用户滚动到图片区域
imageProxy.showImage(); // 第一次调用,加载图片
imageProxy.showImage(); // 第二次调用,已加载,不重复加载

📌 输出结果:

加载图片: big-image.jpg

📚 总结

代理类型特点
静态代理手动编写代理类,适合功能简单、数量少的场景
动态代理(Proxy)使用 Proxy 对象拦截方法调用,适合通用性强、可扩展性高的场景
缓存代理缓存函数结果,减少重复计算
虚拟代理延迟初始化资源,如图片懒加载

🚀 应用场景建议

  • 日志/权限控制:使用 Proxy 或静态代理增强方法行为
  • 缓存优化:用于高成本函数或 API 调用
  • 资源管理:如图片、数据库连接等资源的延迟加载