1. 引言
设计模式在构建健壮、可维护且可扩展的代码中起着至关重要的作用。其中,代理模式(Proxy Pattern) 因其灵活性和实用性脱颖而出。
本文将深入探讨代理模式的定义、典型应用场景以及在 Kotlin 中的具体实现方式。对于有经验的开发者而言,掌握这一模式有助于解耦核心逻辑与横切关注点(如权限控制、日志、延迟加载等),是日常开发中的“利器”。
2. 理解代理模式
✅ 代理模式是一种结构型设计模式,它为某个对象提供一个代理(或占位符),以控制对该对象的访问。
这个代理可以在真实对象的方法调用前后插入额外行为,比如:
- 延迟初始化(Lazy Initialization)
- 权限校验
- 日志记录
- 远程通信封装
关键在于:代理和真实对象实现同一接口,客户端无需感知它们之间的差异,从而实现了透明访问。
💡 核心思想:用代理层隔离直接依赖,增强控制力而不侵入原逻辑。
3. 代理模式的常见变体
以下是几种典型的代理模式应用场景,每种都对应不同的业务需求。
3.1. 虚拟代理(Virtual Proxy)
用于延迟创建开销较大的对象,直到真正需要时才实例化,常用于资源密集型操作。
interface RealObject {
fun performOperation()
}
class RealObjectImpl : RealObject {
override fun performOperation() {
println("RealObject performing operation")
}
}
class VirtualProxy : RealObject {
private val realObject by lazy { RealObjectImpl() }
override fun performOperation() {
realObject.performOperation()
}
}
📌 踩坑提醒:lazy { }
默认是线程安全的(LazyThreadSafetyMode.SYNCHRONIZED
),如果性能敏感且确定单线程使用,可显式指定 lazy(LazyThreadSafetyMode.NONE) { }
提升效率。
✅ 优势:避免启动阶段不必要的资源消耗。
3.2. 保护代理(Protection Proxy)
控制对敏感对象的访问权限,确保只有授权用户才能执行操作,适用于基于角色的权限系统。
interface SensitiveObject {
fun access()
}
class SensitiveObjectImpl : SensitiveObject {
override fun access() {
println("SensitiveObject accessed")
}
}
class ProtectionProxy(private val userRole: String) : SensitiveObject {
private val realObject: SensitiveObjectImpl = SensitiveObjectImpl()
override fun access() {
if (userRole == "admin") {
realObject.access()
} else {
println("Access denied. Insufficient privileges.")
}
}
}
✅ 使用场景示例:
- 后台管理接口仅允许
admin
角色调用 - 敏感数据导出功能限制普通用户
⚠️ 注意:生产环境应结合 Spring Security 或自定义注解 + AOP 实现更完善的权限体系,此处仅为演示原理。
3.3. 日志代理(Logging Proxy)
拦截方法调用并记录日志信息,适用于调试、审计或性能监控。
interface ObjectToLog {
fun operation()
}
class RealObjectToLog : ObjectToLog {
override fun operation() {
println("RealObjectToLog performing operation")
}
}
class LoggingProxy(private val realObject: RealObjectToLog) : ObjectToLog {
override fun operation() {
println("Logging: Before operation")
realObject.operation()
println("Logging: After operation")
}
}
📌 可扩展方向:
- 记录耗时(
measureTimeMillis
) - 输出参数/返回值(注意脱敏)
- 集成 SLF4J 写入文件或发送到 ELK
❌ 不推荐在高频调用路径上做同步打印,会影响性能。
3.4. 远程代理(Remote Proxy)
代表位于不同地址空间(如远程服务器)的对象,使本地调用看起来像本地方法一样简单。
interface RemoteObject {
fun performRemoteOperation()
}
class RemoteObjectImpl : RemoteObject {
override fun performRemoteOperation() {
println("RemoteObject performing remote operation on the server")
}
}
class RemoteProxy(private val serverAddress: String) : RemoteObject {
override fun performRemoteOperation() {
println("Proxy: Initiating remote communication with server at $serverAddress")
val remoteObject = RemoteObjectImpl()
remoteObject.performRemoteOperation()
println("Proxy: Remote communication complete")
}
}
class Client(private val remoteObject: RemoteObject) {
fun executeRemoteOperation() {
println("Client: Performing operation through remote proxy")
remoteObject.performRemoteOperation()
}
}
class Server(private val remoteObject: RemoteObject) {
fun startServer() {
println("Server: Server started")
}
}
📌 关键点总结:
- ✅ 客户端通过
RemoteProxy
调用,完全 unaware 实际是远程调用 - ✅ 代理封装了网络通信细节(序列化、连接、异常处理等)
- 🔗 典型应用:RMI、gRPC Stub、Feign Client、Dubbo Invoker
📚 拓展阅读:Design patterns series
4. Kotlin 中代理模式的实际实现
我们通过一个图像加载的例子,直观对比是否使用代理带来的差异。
4.1. 未使用代理的情况
interface Image {
fun display(): Unit
}
class RealImage(private val filename: String) : Image {
init {
loadFromDisk()
}
private fun loadFromDisk() {
println("Loading image: $filename")
}
override fun display() {
println("Displaying image: $filename")
}
}
🔴 问题所在:
- 构造即加载,即使后续不显示也会浪费 I/O 资源
- 图像大时会造成初始化卡顿
- 无法复用加载状态
4.2. 使用代理优化后的实现
引入 ProxyImage
,实现懒加载和缓存。
interface Image {
fun display(): Unit
}
class RealImage(private val filename: String) : Image {
init {
loadFromDisk()
}
private fun loadFromDisk() {
println("Loading image: $filename")
}
override fun display() {
println("Displaying image: $filename")
}
}
class ProxyImage(private val filename: String) : Image {
private var realImage: RealImage? = null
override fun display() {
if (realImage == null) {
realImage = RealImage(filename)
}
realImage?.display()
}
}
✅ 改进效果:
- 第一次调用
display()
才触发加载 —— 懒加载 - 后续调用直接使用已创建实例 —— 缓存复用
- 对客户端透明,调用方式不变
📌 测试示例:
fun main() {
val image: Image = ProxyImage("photo.jpg")
image.display() // 加载 + 显示
image.display() // 直接显示
}
输出:
Loading image: photo.jpg
Displaying image: photo.jpg
Displaying image: photo.jpg
5. 使用代理模式的优势
优势 | 说明 |
---|---|
✅ 控制访问 | 可添加权限检查、频率限制、熔断机制等 |
✅ 功能增强 | 在不修改原类的前提下增加日志、缓存、监控等行为 |
✅ 资源管理 | 实现延迟加载、连接池管理、对象生命周期控制 |
✅ 解耦清晰 | 将横切逻辑从核心业务中剥离,提升模块化程度 |
一句话总结:让代理干脏活累活,本体专心做事。
6. 使用代理模式的潜在问题
缺陷 | 风险说明 | 应对建议 |
---|---|---|
❌ 增加复杂度 | 多一层封装,理解成本上升 | 合理命名类(如 XxxProxy )、写好文档 |
⚠️ 线程安全问题 | 多线程下可能重复创建或状态不一致 | 使用 synchronized 、@Volatile 或 AtomicReference |
❌ 耦合风险 | 客户端依赖代理可能导致重构困难 | 优先面向接口编程,配合 DI 框架降低耦合 |
📌 特别提醒:
若多个线程同时调用 ProxyImage.display()
,存在 realImage
被多次初始化的风险。改进方案如下:
class ThreadSafeProxyImage(private val filename: String) : Image {
@Volatile
private var realImage: RealImage? = null
override fun display() {
if (realImage == null) {
synchronized(this) {
if (realImage == null) {
realImage = RealImage(filename)
}
}
}
realImage?.display()
}
}
双重检查锁(Double-Checked Locking)+ @Volatile
是 Kotlin 中常见的线程安全延迟初始化写法。
7. 总结
代理模式在 Kotlin 开发中是一个非常实用的结构型模式,尤其适合以下场景:
- ✅ 图片、大数据集的懒加载
- ✅ 接口调用的权限控制
- ✅ 方法级别的日志/监控埋点
- ✅ 分布式系统的本地桩(Stub)
虽然会带来一定的复杂度,但只要合理设计、命名规范,并注意线程安全问题,就能充分发挥其“四两拨千斤”的威力。
最佳实践建议:
结合 Kotlin 的by lazy
、高阶函数、委托属性等特性,可以写出更简洁优雅的代理逻辑。但在大型项目中,也可考虑使用 AOP(如 Spring AOP 或 Koin AOP)来统一管理通用代理行为,避免重复编码。