隐藏委托关系 / 移除中间人
隐藏委托关系(Hide Delegate)在一个对象上加一个方法,让调用方不必穿过它去访问它内部的另一个对象;移除中间人(Remove Middle Man)反过来,删掉只做转发的方法,让调用方直接访问内部对象。
它们解决的痛是:调用方写着 a.b.c,b 的结构一变,所有这样写的地方都坏;或者反过来,一个类堆满了转发方法,每加一个功能都要先加一个转发。
一个公司内部系统里,每个员工属于一个部门,部门有一个经理。审批流、请假单、周报提醒三个模块都要找员工的经理,于是到处写着 person.department.manager。
"""员工、部门与经理:重构前。"""
class Department: def __init__(self, name: str, manager: str) -> None: self.name = name self.manager = manager
class Person: def __init__(self, name: str, department: Department) -> None: self.name = name self.department = department
def approval_route(person: Person) -> str: return f"{person.name} 的报销 → {person.department.manager}"
def leave_notice(person: Person) -> str: return f"请假通知抄送 {person.department.manager}"
if __name__ == "__main__": tech = Department("技术部", "赵强") p = Person("小林", tech) print(approval_route(p)) print(leave_notice(p))
# 输出:# 小林 的报销 → 赵强# 请假通知抄送 赵强// 员工、部门与经理:重构前。class Department { constructor( readonly name: string, readonly manager: string, ) {}}
class Person { constructor( readonly name: string, readonly department: Department, ) {}}
function approvalRoute(person: Person): string { return `${person.name} 的报销 → ${person.department.manager}`;}
function leaveNotice(person: Person): string { return `请假通知抄送 ${person.department.manager}`;}
const p = new Person("小林", new Department("技术部", "赵强"));console.log(approvalRoute(p));console.log(leaveNotice(p));
// 输出:// 小林 的报销 → 赵强// 请假通知抄送 赵强如果现在公司改了组织结构:经理不再挂在部门上,而是挂在“汇报线”上,Department 要拆成 Department 和 ReportingLine 两个类。所有写着 person.department.manager 的地方都得改,三个模块、十几处。可是这三个模块关心的只是“这个人的经理是谁”,它们根本不该知道经理是从部门上找到的。
这是过长的消息链:调用方依赖了一串对象的内部结构,链上任何一环变了它都受影响。
"""员工、部门与经理:重构后。"""
class Department: def __init__(self, name: str, manager: str) -> None: self.name = name self.manager = manager
class Person: def __init__(self, name: str, department: Department) -> None: self.name = name self._department = department
@property def manager(self) -> str: return self._department.manager
def approval_route(person: Person) -> str: return f"{person.name} 的报销 → {person.manager}"
def leave_notice(person: Person) -> str: return f"请假通知抄送 {person.manager}"
if __name__ == "__main__": tech = Department("技术部", "赵强") p = Person("小林", tech) print(approval_route(p)) print(leave_notice(p))
# 输出:# 小林 的报销 → 赵强# 请假通知抄送 赵强// 员工、部门与经理:重构后。class Department { constructor( readonly name: string, readonly manager: string, ) {}}
class Person { constructor( readonly name: string, private readonly department: Department, ) {}
get manager(): string { return this.department.manager; }}
function approvalRoute(person: Person): string { return `${person.name} 的报销 → ${person.manager}`;}
function leaveNotice(person: Person): string { return `请假通知抄送 ${person.manager}`;}
const p = new Person("小林", new Department("技术部", "赵强"));console.log(approvalRoute(p));console.log(leaveNotice(p));
// 输出:// 小林 的报销 → 赵强// 请假通知抄送 赵强组织结构调整时,只改 Person.manager 一处的实现,三个模块一行不动。department 变成了私有字段,调用方想穿透也穿不过去了。
怎么一步步做
Section titled “怎么一步步做”隐藏委托关系:
- 在服务对象(
Person)上为委托对象(Department)的每个被用到的方法或字段建一个转发方法。这里只用到了manager。 - 把调用方的
person.department.manager逐个改成person.manager,每改一个跑一次测试。 - 如果再没有调用方需要直接访问
department,把它变成私有。跑测试。
移除中间人正好反过来:在服务对象上提供一个返回委托对象的方法,把调用方的 person.manager 改成 person.department.manager,最后删掉转发方法。
定义与适用场景
Section titled “定义与适用场景”隐藏委托关系:在服务对象上建立转发方法,把委托对象藏起来,调用方只和服务对象打交道。它是“最少知识原则”(迪米特法则)的具体做法:一个对象只和它的直接朋友说话,不和朋友的朋友说话。
它的反向手法是移除中间人。隐藏委托关系做多了,Person 上会长出 manager、budget、location、headcount……十几个只做一行转发的方法,每次 Department 加一个功能,Person 就得跟着加一个转发。这时 Person 成了中间人:它的大部分方法都只是把调用转给别人。该让调用方直接拿 department 去用。
两个手法之间没有绝对的对错,要找的是平衡点:调用方关心的、稳定的少数几件事,用转发方法隐藏;调用方需要大量使用的委托对象,直接交出去。系统演化时这个平衡点会移动,所以两个手法会来回用。
相关的设计模式
Section titled “相关的设计模式”把隐藏委托关系做到一整个子系统,就是外观(Facade)模式:子系统里有很多类、很多复杂的协作关系,外观类提供几个简单的方法,调用方只和外观打交道,不知道背后有哪些类。Person.manager 是只隐藏了一个委托对象的迷你外观。外观模式见《设计模式》书第 11 章。
真实系统里的例子
Section titled “真实系统里的例子”Django 里的长链
Section titled “Django 里的长链”Django 项目里常见 request.user.profile.avatar.url 这样的链:模板、视图、序列化器都这样写,Profile 模型一改字段,全站多处报错。在 User 上加一个属性把链藏起来,顺便处理“没有头像”的情况。
"""模拟 Django 的长链:在 User 上加属性隐藏 profile.avatar。"""
DEFAULT_AVATAR = "/static/default.png"
class Avatar: def __init__(self, url: str) -> None: self.url = url
class Profile: def __init__(self, avatar: Avatar | None) -> None: self.avatar = avatar
class User: def __init__(self, username: str, profile: Profile) -> None: self.username = username self.profile = profile
@property def avatar_url(self) -> str: avatar = self.profile.avatar return avatar.url if avatar else DEFAULT_AVATAR
if __name__ == "__main__": users = [User("lin", Profile(Avatar("/media/lin.jpg"))), User("wang", Profile(None))] for u in users: print(u.username, u.avatar_url)
# 输出:# lin /media/lin.jpg# wang /static/default.png原来每个写 user.profile.avatar.url 的地方都要自己判断 avatar 是不是 None,有的地方忘了判断就报错。现在模板里写 {{ user.avatar_url }} 就够了。
什么时候别用
Section titled “什么时候别用”一个委托关系只在一两处用到,而且很稳定,a.b.c 写着就写着,加转发方法只是多一层。别看到点号就想隐藏,链式调用本身不是问题,依赖不稳定的内部结构才是。
流式接口(query.filter(...).order_by(...).limit(...))和数组方法链(list.filter().map())不是消息链,每一步返回的都是同一种东西,不暴露内部结构。
- 调用方穿过一个对象去用另一个对象,内部结构一变就全坏:在中间对象上加转发方法,把内部对象藏起来。
- 转发方法加多了,中间对象就成了中间人:删掉转发,让调用方直接用内部对象。
- 两者之间找平衡:稳定的少数几件事隐藏,大量使用的直接交出去。
- 把一整个子系统藏在几个简单方法后面,就是外观模式。