事实上,门面模式在传统 Dao-Service-Controller 分层架构中极为常见。试着回想一下,你定义的 Controller 是不是一般只有一些 Service 的调用,只涉及一些逻辑判断,不会有具体的业务逻辑在 Controller 代码块中?如果有,那可能是你分层分的不够彻底吧。
像上述暴露的每一个 Controller 接口就是一个门面模式的最佳实践,下面就来详细介绍一下门面模式。
1. 定义
门面模式是一种结构型设计模式,它的定义是:“要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。”
2. 类图
在门面模式中,只有两种角色:
- 门面角色(Facade):暴露给外部系统的高层次接口,客户端可以调用这个角色的方法。此角色知晓子系统的所有功能和责任。一般情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去,也就说该角色没有实际的业务逻辑,只是一个委托类
- 子系统角色(Sub Class):多个子系统角色一起组成了门面角色,同时子系统角色不会直接暴露给外部系统使用。每一个子系统都不是一个单独的类,而是一个类的集合。子系统并不知道门面的存在。对于子系统而言,门面仅仅是另外一个客户端而已。
门面模式的类图如下:
3. 示例
在年初我们项目基于 DDD 的思想进行领域重构时,我就用到了门面模式,下面就作为门面模式的最佳实践来举例。
假设现在需要重构的是用户注册的功能,它可能需要校验字段必填、校验用户唯一性、填充默认字段、持久化、手动抛 MQ 消息等操作,视图层需要调用服务端暴露的 Controller 接口进行用户注册。
先来看未使用门面模式重构前的代码,Service 层代码如下:
public class OldCreateUserService {
public void validateRequired(User user) {
System.out.println("OldCreateUserService-校验必填");
}
public void validateUniqueness(User user) {
System.out.println("OldCreateUserService-校验唯一性");
}
public void preCreate(User user) {
System.out.println("OldCreateUserService-持久化前置操作");
}
public int create(User user) {
System.out.println("OldCreateUserService-持久化操作");
return 0;
}
public void postCreate(User user) {
System.out.println("OldCreateUserService-持久化后置操作");
}
}
暴露给视图层的 Controller 层代码如下:
public class OldCreateUserController {
// 依赖注入
private OldCreateUserService oldCreateUserService;
// 用户注册
public int register(User user) {
oldCreateUserService.validateRequired(user);
oldCreateUserService.validateUniqueness(user);
oldCreateUserService.preCreate(user);
int id = oldCreateUserService.create(user);
oldCreateUserService.postCreate(user);
return id;
}
}
或许这样看来并没有什么特别大的问题,那么如果现在来了一个新需求:现在需要新增一个注册用户的入口,我们就叫他 AnotherRegisterController 吧,代码该怎么写?
这十分简单,把原来注册的代码拷贝一份不就好了(我们一般不在其他地方注入 Controller 层的 Bean),其代码如下:
public class AnotherRegisterController {
// 依赖注入
private OldCreateUserService oldCreateUserService;
// 用户注册
public int register(User user) {
oldCreateUserService.validateRequired(user);
oldCreateUserService.validateUniqueness(user);
oldCreateUserService.preCreate(user);
int id = oldCreateUserService.create(user);
oldCreateUserService.postCreate(user);
return id;
}
}
确实,这能够实现这个需求,但是这就带来了一个新的问题:有两处一模一样的代码。有人可能会说了,这能有啥问题?两处一模一样的代码意味着当用户注册业务发生改变时,这两处都需要做变动,如果此时又新增了若干个注册用户的入口呢?这样代码的可维护性就大大降低了。
这时候如果使用门面模式,无论有多少处注册用户的入口,我们最多只需要改一处就可以了。
使用门面模式重构注册用户的代码如下:
public class CreateUserFacade {
private void validateRequired(User user) {
System.out.println("校验必填");
}
private void validateUniqueness(User user) {
System.out.println("校验唯一性");
}
private void preCreate(User user) {
System.out.println("持久化前置操作,如填充默认字段");
}
private int create(User user) {
System.out.println("持久化操作");
return 0;
}
private void postCreate(User user) {
System.out.println("持久化后置操作,如手动抛 MQ 消息");
}
/**
* public 类型的可被外部访问的方法
*/
public int doCreate(User user) {
this.validateRequired(user);
this.validateUniqueness(user);
this.preCreate(user);
int id = this.create(user);
this.postCreate(user);
return id;
}
}
可以看到,原来在 OldCreateUserController#register 方法被迁移到了 CreateUserFacade 中,同时 register 方法中的调用的方法访问类型全部变成了 private 类型,并且只暴露了一个 public 类型的 doCreate 方法,这就是门面模式。
无论会新增多少个注册用户的入口,都只需要调用 CreateUserFacade#doCreate 方法就可以,后期如果注册用户业务发生变动,也只需要修改这一处。
4. 使用场景
门面模式的使用场景是:
- 为复杂的模块提供一个供外界访问的接口
- 当客户端与子系统间依赖性很强,需要接耦时
5. 小结
本文讲述了门面模式,它的定义是——要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
门面模式的优点是:
- 减少系统的相互依赖
- 提高系统灵活性:不管子系统内部如何变化,只要不影响到门面对象,就没有关系
- 提高系统安全性:只需要访问门面方法,内部方法不允许客户端访问
门面模式的缺点是:
- 不符合开闭原则,对修改关闭,对扩展开放
6. 参考资料
- 《设计模式之禅》(第2版)
最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。
文档信息
- 本文作者:Planeswalker23
- 本文链接:https://planeswalker23.github.io/2022/02/03/%E6%88%91%E6%89%80%E7%90%86%E8%A7%A3%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E7%B3%BB%E5%88%97-%E7%AC%AC9%E7%AF%87-%E4%BD%A0%E4%B8%80%E5%AE%9A%E5%BE%88%E7%9C%BC%E7%86%9F%E7%9A%84%E9%97%A8%E9%9D%A2%E6%A8%A1%E5%BC%8F/
- 版权声明:本作品系原创,作者保留所有权利,未经作者允许,禁止转载和演绎。