跳转到内容

隐藏委托关系 / 移除中间人

隐藏委托关系(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 再穿过 Department 才拿到经理;重构后只问 Person.manager,Department 藏在 Person 里面

组织结构调整时,只改 Person.manager 一处的实现,三个模块一行不动。department 变成了私有字段,调用方想穿透也穿不过去了。

隐藏委托关系:

  1. 在服务对象(Person)上为委托对象(Department)的每个被用到的方法或字段建一个转发方法。这里只用到了 manager。
  2. 把调用方的 person.department.manager 逐个改成 person.manager,每改一个跑一次测试。
  3. 如果再没有调用方需要直接访问 department,把它变成私有。跑测试。

移除中间人正好反过来:在服务对象上提供一个返回委托对象的方法,把调用方的 person.manager 改成 person.department.manager,最后删掉转发方法。

隐藏委托关系:在服务对象上建立转发方法,把委托对象藏起来,调用方只和服务对象打交道。它是“最少知识原则”(迪米特法则)的具体做法:一个对象只和它的直接朋友说话,不和朋友的朋友说话。

它的反向手法是移除中间人。隐藏委托关系做多了,Person 上会长出 manager、budget、location、headcount……十几个只做一行转发的方法,每次 Department 加一个功能,Person 就得跟着加一个转发。这时 Person 成了中间人:它的大部分方法都只是把调用转给别人。该让调用方直接拿 department 去用。

两个手法之间没有绝对的对错,要找的是平衡点:调用方关心的、稳定的少数几件事,用转发方法隐藏;调用方需要大量使用的委托对象,直接交出去。系统演化时这个平衡点会移动,所以两个手法会来回用。

把隐藏委托关系做到一整个子系统,就是外观(Facade)模式:子系统里有很多类、很多复杂的协作关系,外观类提供几个简单的方法,调用方只和外观打交道,不知道背后有哪些类。Person.manager 是只隐藏了一个委托对象的迷你外观。外观模式见《设计模式》书第 11 章。

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 }} 就够了。

一个委托关系只在一两处用到,而且很稳定,a.b.c 写着就写着,加转发方法只是多一层。别看到点号就想隐藏,链式调用本身不是问题,依赖不稳定的内部结构才是。

流式接口(query.filter(...).order_by(...).limit(...))和数组方法链(list.filter().map())不是消息链,每一步返回的都是同一种东西,不暴露内部结构。

  • 调用方穿过一个对象去用另一个对象,内部结构一变就全坏:在中间对象上加转发方法,把内部对象藏起来。
  • 转发方法加多了,中间对象就成了中间人:删掉转发,让调用方直接用内部对象。
  • 两者之间找平衡:稳定的少数几件事隐藏,大量使用的直接交出去。
  • 把一整个子系统藏在几个简单方法后面,就是外观模式。