# 08 代理（结构型）

> 在访问一个对象前后插一脚——懒加载、权限、缓存、远程调用——却不改这个对象、也不让调用方察觉。讲清代理与装饰器这对“双胞胎”的本质区别，以及虚拟/保护/远程/缓存四种代理。

- 作者：David（道雾轩）
- 专栏：经典设计模式浅析（https://daiw.org/manual/design-patterns.md）
- 最后更新：2026-07-12
- 原文：https://daiw.org/manual/design-patterns/proxy
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 08 代理 · Proxy（结构型）

上一篇结尾埋了个扣：代理和装饰器结构几乎一样，凭什么算两个模式？这一篇解开它。

## 一、这是什么：你找明星，得先过经纪人

你想请某位明星站台。你能直接打他电话吗？不能。你得先联系他的**经纪人**。经纪人和明星“接口一致”(对外都代表这位明星)，但他站在明星前面，替明星**控制访问**：帮你排期、挡掉不靠谱的邀约、代收合同、明星忙时先应付着……你以为在跟明星打交道，其实一直在跟经纪人打交道。

代理模式就是这位经纪人：**给目标对象配一个“替身”，替身和本尊实现同一个接口，调用方对着替身说话，替身在中间控制对本尊的访问。**

配套代码是图片懒加载：`ImageProxy` 是替身，`RealImage`(加载很慢)是本尊。你拿到一堆代理时，一张图都还没真加载；直到某张图**第一次**被 `Display()`，它的代理才偷偷把真图 load 进来。

## 二、解决什么痛：我想在“访问”这件事上做文章

有时候，问题不在对象本身的功能，而在**“访问它”这个动作**上，你想加点控制：

- **它创建太贵，想用到才创建**(虚拟代理/懒加载)——配套代码这种。
- **不是谁都能访问，想加权限检查**(保护代理)。
- **它在另一台机器上，想让远程调用看起来像本地调用**(远程代理)。
- **它每次算都很慢，想把结果缓存起来**(缓存代理)。

这些需求的共同点：**你不想改目标对象**(它可能是第三方，或者职责单一不该掺和这些)，**也不想让调用方感知**(调用方就想正常用，不管你懒不懒加载)。于是在中间塞个替身。

<Callout type="info">
  **代理隔离的“会变的东西”，是“访问目标对象的方式与时机”。** 目标对象只管做好它本职的事，而“何时创建、能不能访问、要不要走网络、结果缓不缓存”这些访问控制策略，全被收进代理里，两边都干净。
</Callout>

## 三、怎么实现：同接口替身 + 按需转调本尊

```go title="go/proxy/main.go（核心）"
type Image interface { Display() string }

type RealImage struct { filename string }   // 本尊:创建代价高
func NewRealImage(f string) *RealImage {
	img := &RealImage{filename: f}
	img.loadFromDisk()                       // ← 构造即"加载",很慢
	return img
}

// 代理:和本尊同接口,持有本尊引用,延迟到首次访问才创建它
type ImageProxy struct {
	filename  string
	realImage *RealImage
}
func (p *ImageProxy) Display() string {
	if p.realImage == nil {                  // ← 访问控制:第一次才真加载
		p.realImage = NewRealImage(p.filename)
	}
	return p.realImage.Display()             // 之后复用,不再重复加载
}
```

结构上，它和装饰器几乎一字不差：都实现同接口、都持有一个同接口对象、都在方法里转调。**区别只在意图**——见下一节。

## 四、有哪些坑：和装饰器到底差在哪

这是本篇的重点。代理和装饰器是结构型里最像的一对，面试常问，记牢这张对照：

| | 装饰器 | 代理 |
| --- | --- | --- |
| **意图** | **增强功能**(结果变了) | **控制访问**(结果不变) |
| **典型标志** | 层层叠加，常多层套娃 | 通常一层，替身管着一个本尊 |
| **谁决定包不包** | 客户端主动一层层包 | 代理自己决定何时/是否接触本尊 |
| **例子** | 咖啡加料、`bufio` 包 `Reader` | 懒加载、权限校验、RPC stub |

一句话：**装饰器让对象“变强”，代理让对象“变得可控”。** 装饰器改的是功能本身(咖啡变贵了、流被压缩了)；代理不碰功能，管的是“能不能用、什么时候用、用起来多贵”。

其它坑：

- **代理链路要透明。** 调用方不该因为多了个代理就得改代码。一旦透明性破了(比如代理悄悄改了返回值语义)，就成了 bug 温床。
- **懒加载的并发安全。** 配套代码的 `if realImage == nil` 在多 goroutine 下有竞态——真实项目要用 `sync.Once` 或加锁保护“只创建一次”。
- **远程代理的“本地假象”是把双刃剑。** 让 RPC 调用看起来像本地调用很爽，但它掩盖了网络的延迟和失败。调用方以为在调本地方法，忘了处理超时/重试——这是分布式系统的经典陷阱。**好用不等于没代价。**

## 五、开源里怎么用

- **RPC 桩代码(gRPC / Thrift stub)——远程代理的头号应用。** 你调 `client.GetUser(ctx, req)` 像调本地方法，底层 stub(代理)把它序列化、走网络、等响应、反序列化。本尊在另一台机器上，代理制造了“本地”的假象。
- **Java 动态代理(`java.lang.reflect.Proxy`)——整个 Java 企业生态的基石。** Spring 的声明式事务(`@Transactional`)、AOP 切面、MyBatis 的 Mapper 接口，全靠运行时生成代理：你调接口方法，代理在前后插入“开启事务/提交回滚”“日志”"SQL 执行“。没有代理，就没有半个 Spring。
- **ORM 懒加载**：Hibernate 查出一个对象，它的关联集合往往是**代理**，你不访问就不发 SQL，一访问才去查库——虚拟代理的教科书用法。
- **Go `net/http/httputil.ReverseProxy`**：反向代理，接收请求、转发给后端、把响应带回来，中间可插改写/负载均衡/缓存。Nginx 反向代理是同一思想的基础设施级实现。

---

到这里，四个“包一层”的模式(适配器改接口、装饰器加功能、代理控访问)集齐三个，还差一个**外观**——它包的不是一个对象，而是一整个子系统。下一篇[外观](https://daiw.org/manual/design-patterns/facade)。
