python设计模式–代理模式(Proxy Pattern)

Python 设计模式——代理模式(Proxy Pattern)详解

本文面向零基础新手,从概念到多种代理类型(虚拟、保护、缓存、日志),从简单示例到完整用法,全方位讲解代理模式。


一、什么是代理模式?

1.1 通俗理解

代理模式是一种结构型设计模式,核心思想是:

不直接访问“真实对象”,而是通过一个“代理对象”去访问;代理和真实对象实现同一接口,客户只和代理打交道,代理在转交给真实对象前后可以加一层逻辑(如权限检查、日志、缓存、延迟加载),从而在不改真实对象的前提下控制访问、增强或简化使用。

可以这样类比:

  • 现实中的代理:你要找明星签字,不会直接冲进他家,而是通过“经纪人”(代理):经纪人先判断你能不能见、记一笔“谁在什么时候请求”,再决定是否转达给明星。明星(真实对象)没变,但访问被经纪人控制了。
  • 代码里:你要访问“大图片”,不直接 new 大图片(可能很慢、占内存),而是先拿一个“图片代理”:代理在第一次真正显示时才去加载大图(延迟加载);或代理先查缓存,有则直接返回,没有再调真实对象。客户只和代理交互,代理再决定何时、如何访问真实对象。

所以,代理模式解决的是:在“访问真实对象”前后加一层控制或逻辑(权限、日志、缓存、延迟加载),而不修改真实对象本身。

1.2 为什么需要代理?

场景一:延迟加载(虚拟代理)

大对象创建或加载很慢(如大图、大文件),希望“用到时才加载”。客户拿到的是代理,代理在第一次被调用时才创建/加载真实对象,之后转调真实对象。

场景二:访问控制(保护代理)

只有满足条件(如已登录、有权限)才能调用真实对象。客户调的是代理,代理先检查权限,通过再转给真实对象,否则拒绝。

场景三:增强与记录(日志代理、缓存代理)

在调用前后打日志、计时、统计;或先查缓存,命中则直接返回,未命中再调真实对象并写入缓存。真实对象不关心这些,由代理完成。

场景四:远程代理

真实对象在另一台机器上,代理在本地,客户调代理时,代理通过网络把请求转给远程对象,把结果返回。客户无感知“本地还是远程”。

1.3 适用场景(什么时候用?)

场景 说明
延迟加载 大对象用到时才创建/加载,用代理占位。
访问控制 权限、登录检查,由代理统一做。
日志、统计、缓存 在调用前后加逻辑,不改真实对象。
远程访问 本地代理代表远程对象,隐藏网络细节。

简单记忆:要在不改真实对象的前提下控制或增强对它的访问时,在中间加一个代理。


二、代理模式的结构(三个角色)

角色 说明
主题(Subject) 接口,定义客户和真实对象都要遵守的约定(如 request、load)。
真实主题(RealSubject) 真正干活的类,实现主题接口。
代理(Proxy) 实现主题接口,内部持有一个真实主题(或能创建/获取真实主题);客户调用代理时,代理先/后做自己的事(检查、日志、缓存、延迟创建等),再转调真实主题。

关系可以理解为:

  • 客户只依赖 Subject 接口,拿到的是 Proxy 实例(或 RealSubject,但通常通过代理访问)。
  • Proxy 实现 Subject,在方法里:可选地先做逻辑(权限、缓存查找)→ 若需要则创建或获取 RealSubject → 调用 RealSubject 的同一方法 → 可选地后做逻辑(日志、写缓存)→ 返回结果。

三、示例一:延迟加载(虚拟代理)——大图

需求:大图加载很慢,希望“显示时才加载”。客户拿到的先是一个“图片代理”,第一次调用 display() 时代理才去加载真实大图,再显示。

3.1 主题接口 + 真实主题

from abc import ABC, abstractmethod

class Image(ABC):
    @abstractmethod
    def display(self):
        pass

class RealImage(Image):
    def __init__(self, path: str):
        self._path = path
        print(f"  [RealImage] 从磁盘加载大图: {path}")

    def display(self):
        print(f"  [RealImage] 显示: {self._path}")

3.2 代理:第一次 display 时才加载

class ImageProxy(Image):
    def __init__(self, path: str):
        self._path = path
        self._real = None  # 先不创建真实对象

    def display(self):
        if self._real is None:
            self._real = RealImage(self._path)  # 第一次调用时才加载
        self._real.display()

3.3 使用

proxy = ImageProxy("/big/photo.jpg")
print("代理已创建,尚未加载图片")
proxy.display()  # 这时才加载并显示
proxy.display()  # 直接显示,不再加载

要点:创建 ImageProxy 很快,不占大内存;真正用到时才加载 RealImage,这就是虚拟代理(延迟加载)


四、示例二:访问控制(保护代理)

需求:敏感操作只有“已登录用户”才能执行。客户调的是代理,代理先检查是否登录,通过再转给真实对象。

4.1 主题 + 真实主题

class Database(ABC):
    @abstractmethod
    def query(self, sql: str) -> list:
        pass

class RealDatabase(Database):
    def query(self, sql: str) -> list:
        print(f"  [RealDatabase] 执行: {sql}")
        return [{"id": 1, "name": "test"}]

4.2 代理:先检查再转发

class DatabaseProxy(Database):
    def __init__(self, real: Database):
        self._real = real
        self._user = None

    def login(self, user: str):
        self._user = user
        print(f"  用户 {user} 已登录")

    def query(self, sql: str) -> list:
        if self._user is None:
            raise PermissionError("请先登录")
        return self._real.query(sql)

4.3 使用

real = RealDatabase()
proxy = DatabaseProxy(real)
# proxy.query("SELECT * FROM users")  # 会抛 PermissionError
proxy.login("admin")
proxy.query("SELECT * FROM users")  # 正常执行

要点:真实对象不关心“谁在调”;权限逻辑都在代理里,这就是保护代理


五、示例三:日志代理

需求:在每次调用真实对象前后打日志,不改真实对象代码。

class LoggingProxy(Database):
    def __init__(self, real: Database):
        self._real = real

    def query(self, sql: str) -> list:
        print("[Log] 请求 query:", sql)
        result = self._real.query(sql)
        print("[Log] 返回行数:", len(result))
        return result

客户使用 LoggingProxy(real),所有对 query 的调用都会先打日志、再调真实对象、再打日志。真实对象无感知。


六、示例四:缓存代理

需求:某操作耗时(如复杂查询),希望相同参数只算一次,结果缓存起来;下次相同参数直接返回缓存。

class CachingProxy(Database):
    def __init__(self, real: Database):
        self._real = real
        self._cache = {}

    def query(self, sql: str) -> list:
        if sql in self._cache:
            print("  [Cache] 命中")
            return self._cache[sql]
        result = self._real.query(sql)
        self._cache[sql] = result
        return result

客户用 CachingProxy(real),第一次 query(“SELECT …”) 走真实对象并缓存;第二次相同 SQL 直接返回缓存。这就是缓存代理


七、代理与装饰器的区别

对比 代理 装饰器
目的 控制/管理对对象的访问(延迟、权限、缓存、远程) 动态给对象增加职责(加料、加日志)
关注点 “谁能在什么时候访问”“何时创建”“是否走缓存” “在原有行为前后加什么逻辑”
典型 虚拟代理、保护代理、缓存代理 咖啡加奶、带日志的发送器

简单记忆:代理侧重访问控制(能不能用、何时加载、用不用缓存);装饰器侧重行为增强(同一接口上多加一层逻辑)。两者在实现上很像(都实现同一接口、都持有一个对象并转发),但意图不同。


八、常见问题与注意点

8.1 代理和真实对象接口一致

客户只依赖主题接口,所以代理必须实现同一接口,才能“无缝替换”;否则客户就要区分代理和真实对象,失去代理的意义。

8.2 谁创建真实对象?

可以是代理在需要时自己 new(如虚拟代理);也可以由外部传入(如保护代理、日志代理通常接收一个已存在的真实对象)。按场景选择。

8.3 不要滥用

若没有“延迟加载、访问控制、缓存、日志、远程”等需求,直接使用真实对象即可;代理会多一层调用,只在有明确需求时使用。


九、小结

  • 代理模式:通过代理对象代替对真实对象的直接访问;代理与真实对象实现同一接口,在转调前后可加访问控制、延迟加载、缓存、日志等逻辑,而不修改真实对象。
  • 三个角色:主题(接口)、真实主题、代理(实现主题并持有/创建真实主题,在方法里加逻辑再转发)。
  • 常见类型:虚拟代理(延迟加载)、保护代理(权限)、缓存代理、日志代理、远程代理。
  • 实现要点:代理实现主题接口;内部持有或能创建真实主题;在调用真实主题前后执行自己的逻辑。

建议先写“大图延迟加载”和“数据库保护代理”,再试“缓存代理”或“日志代理”,这样对代理模式会掌握得比较扎实。

发表评论