#外观模式与组合模式
#一个用户注册的 20 个调用
我们团队有个注册流程,最初很简单:
public void register(User user) {
userDao.save(user);
}后来加了一堆功能:
public void register(User user) {
// 1. 保存用户
userDao.save(user);
// 2. 发送欢迎邮件
emailService.sendWelcome(user.getEmail());
// 3. 发送欢迎短信
smsService.send(user.getPhone(), "Welcome!");
// 4. 初始化积分账户
pointsAccountService.create(user.getId());
// 5. 初始化优惠券
couponService.grantNewUserCoupon(user.getId());
// 6. 记录审计日志
auditService.log("USER_REGISTER", user.getId());
// 7. 发送大数据埋点
dataService.track("user_register", user.getId());
// ... 一共 20 个调用
}业务代码里满是细节,任何一个新人都要花一周才能搞懂注册流程到底做了什么。
外观模式解决的就是这个问题:为一组复杂的子系统调用提供一个统一的高层接口,让客户端不需要关心内部细节。
#二、外观模式🔴
#2.1 标准写法
// 子系统A:邮件服务
class EmailService {
void sendWelcome(String email) { /* ... */ }
void sendResetPassword(String email, String token) { /* ... */ }
}
// 子系统B:短信服务
class SmsService {
void send(String phone, String message) { /* ... */ }
}
// 子系统C:积分服务
class PointsService {
void createAccount(Long userId) { /* ... */ }
void addPoints(Long userId, int points) { /* ... */ }
}
// 子系统D:优惠券服务
class CouponService {
void grantNewUserCoupon(Long userId) { /* ... */ }
}
// 外观类
class UserFacade {
private final EmailService emailService;
private final SmsService smsService;
private final PointsService pointsService;
private final CouponService couponService;
// 一个方法封装所有注册相关操作
public void register(User user) {
// 1. 核心:保存用户
userDao.save(user);
// 2. 通知:邮件
emailService.sendWelcome(user.getEmail());
// 3. 通知:短信
smsService.send(user.getPhone(), "Welcome to our platform!");
// 4. 初始化:积分账户
pointsService.createAccount(user.getId());
pointsService.addPoints(user.getId(), 100); // 新用户100积分
// 5. 初始化:优惠券
couponService.grantNewUserCoupon(user.getId());
// 6. 审计日志
auditService.log("USER_REGISTER", user.getId());
}
// 解封:只发邮件
public void resendWelcomeEmail(User user) {
emailService.sendWelcome(user.getEmail());
}
// 解封:只发短信
public void sendSms(String phone, String message) {
smsService.send(phone, message);
}
}#2.2 客户端使用
// 客户端代码:以前需要知道 20 个子系统
class ClientCode {
void registerUser() {
// 需要了解所有子系统的 API
emailService.sendWelcome(...);
smsService.send(...);
pointsService.createAccount(...);
// ...
}
}
// 现在:只需要调用外观
class ClientCode {
private final UserFacade userFacade;
void registerUser() {
userFacade.register(user); // 一行搞定
}
}#2.3 外观模式 vs 直接调用
| 维度 | 直接调用 | 外观模式 |
|---|---|---|
| 客户端复杂度 | 高(需要了解所有子系统) | 低(只需了解外观类) |
| 子系统耦合 | 客户端直接依赖所有子系统 | 客户端只依赖外观类 |
| 调用顺序 | 客户端负责 | 外观类负责 |
| 灵活度 | 高(可以精确控制) | 中(通过外观类暴露) |
| 适用场景 | 简单场景 | 复杂子系统 |
【架构权衡】 外观模式不限制客户端直接访问子系统——它只是提供一个"默认的方便入口"。高级用户仍然可以绕过外观,直接调用具体子系统。这是一种可选的简化,而不是强制封装。
#三、组合模式🟡
#3.1 问题的引入
我们的菜单系统需要支持:
菜单
├── 菜品A
├── 菜品B
└── 套餐(包含多个子菜品)
├── 菜品C
└── 菜品D文件和文件夹的树形结构也是类似:
根文件夹
├── 文件1.txt
├── 文件2.txt
└── 子文件夹
├── 文件3.txt
└── 文件4.txt核心问题:如何用统一的接口操作单个对象和组合对象?
#3.2 组合模式结构
// 组件接口:所有对象的统一接口
interface FileSystemComponent {
String getName();
long getSize();
void print(String indent); // 打印结构
}
// 叶子节点:文件
class File implements FileSystemComponent {
private final String name;
private final long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public String getName() { return name; }
@Override
public long getSize() { return size; }
@Override
public void print(String indent) {
System.out.println(indent + "- " + name + " (" + size + "KB)");
}
}
// 组合节点:文件夹
class Folder implements FileSystemComponent {
private final String name;
private final List<FileSystemComponent> children = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
@Override
public String getName() { return name; }
// 文件夹的大小 = 所有子项的大小之和
@Override
public long getSize() {
return children.stream().mapToLong(FileSystemComponent::getSize).sum();
}
@Override
public void print(String indent) {
System.out.println(indent + "+ " + name + " (" + getSize() + "KB)");
for (FileSystemComponent child : children) {
child.print(indent + " ");
}
}
// 组合操作
public void add(FileSystemComponent component) {
children.add(component);
}
public void remove(FileSystemComponent component) {
children.remove(component);
}
}#3.3 使用示例
// 构建文件树
Folder root = new Folder("root");
Folder docs = new Folder("documents");
Folder images = new Folder("images");
docs.add(new File("resume.pdf", 100));
docs.add(new File("report.docx", 500));
docs.add(new File("notes.txt", 10));
images.add(new File("photo1.jpg", 2000));
images.add(new File("photo2.jpg", 1500));
root.add(docs);
root.add(images);
root.add(new File("readme.md", 5));
// 统一接口操作
root.print("");
// 输出:
// + root (4105KB)
// + documents (610KB)
// - resume.pdf (100KB)
// - report.docx (500KB)
// - notes.txt (10KB)
// + images (3500KB)
// - photo1.jpg (2000KB)
// - photo2.jpg (1500KB)
// - readme.md (5KB)
// 计算总大小
System.out.println("Total: " + root.getSize() + "KB"); // 4105KB#3.4 组合模式的透明性与安全性
// 透明性方法:客户端不需要知道是叶子还是组合
interface FileSystemComponent {
void add(FileSystemComponent c); // 叶子也有这些方法
void remove(FileSystemComponent c);
}
// 安全性方法:接口不暴露组合操作
interface FileComponent {
long getSize();
void print();
}
interface CompositeComponent extends FileComponent {
void add(FileComponent c);
void remove(FileComponent c);
}| 方法 | 透明性 | 安全性 |
|---|---|---|
| 接口暴露 add/remove | ✅ 透明性好,客户端无需区分 | ❌ 安全性差,File 对象也可以调用 add(空实现) |
| 接口不暴露 add/remove | ❌ 透明性差,需要类型判断 | ✅ 安全性好,叶子没有 add 方法 |
Java 的 java.awt.Component 用的是透明性方法(Component 都有 add/remove),而 JDK 的 Collections 用的是安全性方法(List 和 Map 分开)。
#四、外观与组合的结合🟡
#4.1 文件系统 + 外观模式
class FileSystemFacade {
// 隐藏内部结构,提供高层 API
public void copyFile(String source, String dest) {
FileSystemComponent src = findComponent(source);
// 复制实现
}
public void deleteFolder(String path) {
FileSystemComponent component = findComponent(path);
if (component instanceof Folder) {
// 删除文件夹(递归删除子项)
}
}
public long calculateSize(String path) {
FileSystemComponent component = findComponent(path);
return component.getSize(); // 组合模式自动计算
}
}#4.2 菜单系统
// 菜单组件
interface MenuComponent {
String getName();
void print();
}
// 菜单项
class MenuItem implements MenuComponent {
private final String name;
private final double price;
private final String description;
@Override
public void print() {
System.out.println(" " + name + " - $" + price);
System.out.println(" " + description);
}
}
// 菜单组
class MenuGroup implements MenuComponent {
private final String name;
private final List<MenuComponent> items = new ArrayList<>();
@Override
public void print() {
System.out.println("\n== " + name + " ==");
for (MenuComponent item : items) {
item.print(); // 递归打印
}
}
}#五、生产避坑清单
#5.1 外观模式滥用
// ❌ 错误:把外观类变成万能类
class GodFacade {
// 什么都有,完全违背单一职责
void doA();
void doB();
void doC();
// ...
void doZ();
}
// ✅ 正确:按领域划分外观
class UserFacade { /* 用户相关 */ }
class OrderFacade { /* 订单相关 */ }
class PaymentFacade { /* 支付相关 */ }#5.2 组合模式的递归陷阱
// ❌ 错误:递归调用导致栈溢出
class Folder {
@Override
public long getSize() {
long total = 0;
for (FileSystemComponent child : children) {
total += child.getSize(); // 如果有循环引用,这里会无限递归
}
return total;
}
}⚠️
组合模式要注意循环引用的问题。如果目录 A 包含目录 B,目录 B 又包含目录 A,递归操作会栈溢出。解决方案:使用迭代器模式或限制递归深度。
#六、面试总结
#6.1 核心追问
- "外观模式和适配器模式的区别?" —— 外观简化调用,适配器转换接口
- "组合模式的透明性和安全性怎么选?" —— 透明性好但安全性差,Java AWT 选透明性
- "什么场景下用外观模式?" —— 子系统复杂、客户端需要简化
#6.2 级别差异
| 级别 | 期望回答 |
|---|---|
| P5 | 能写出两种模式的基本结构 |
| P6 | 能说出透明性/安全性权衡,能结合 Spring 分析 |
| P7 | 能识别循环引用陷阱,能在复杂系统中正确选型 |