08 代理(结构型)
在访问一个对象前后插一脚——懒加载、权限、缓存、远程调用——却不改这个对象、也不让调用方察觉。讲清代理与装饰器这对“双胞胎”的本质区别,以及虚拟/保护/远程/缓存四种代理。
08 代理 · Proxy(结构型)
上一篇结尾埋了个扣:代理和装饰器结构几乎一样,凭什么算两个模式?这一篇解开它。
一、这是什么:你找明星,得先过经纪人
你想请某位明星站台。你能直接打他电话吗?不能。你得先联系他的经纪人。经纪人和明星“接口一致”(对外都代表这位明星),但他站在明星前面,替明星控制访问:帮你排期、挡掉不靠谱的邀约、代收合同、明星忙时先应付着……你以为在跟明星打交道,其实一直在跟经纪人打交道。
代理模式就是这位经纪人:给目标对象配一个“替身”,替身和本尊实现同一个接口,调用方对着替身说话,替身在中间控制对本尊的访问。
配套代码是图片懒加载:ImageProxy 是替身,RealImage(加载很慢)是本尊。你拿到一堆代理时,一张图都还没真加载;直到某张图第一次被 Display(),它的代理才偷偷把真图 load 进来。
二、解决什么痛:我想在“访问”这件事上做文章
有时候,问题不在对象本身的功能,而在**“访问它”这个动作**上,你想加点控制:
- 它创建太贵,想用到才创建(虚拟代理/懒加载)——配套代码这种。
- 不是谁都能访问,想加权限检查(保护代理)。
- 它在另一台机器上,想让远程调用看起来像本地调用(远程代理)。
- 它每次算都很慢,想把结果缓存起来(缓存代理)。
这些需求的共同点:你不想改目标对象(它可能是第三方,或者职责单一不该掺和这些),也不想让调用方感知(调用方就想正常用,不管你懒不懒加载)。于是在中间塞个替身。
代理隔离的“会变的东西”,是“访问目标对象的方式与时机”。 目标对象只管做好它本职的事,而“何时创建、能不能访问、要不要走网络、结果缓不缓存”这些访问控制策略,全被收进代理里,两边都干净。
三、怎么实现:同接口替身 + 按需转调本尊
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 反向代理是同一思想的基础设施级实现。
到这里,四个“包一层”的模式(适配器改接口、装饰器加功能、代理控访问)集齐三个,还差一个外观——它包的不是一个对象,而是一整个子系统。下一篇外观。