设计模式之模板和委派

模板模式

什么是模板模式?

想象一下,你和朋友决定一起做一道菜,比如 “炒青菜”。你们都知道做这道菜的基本流程是固定的:‌买菜 -> 洗菜 -> 切菜 -> 炒菜 -> 装盘。这个流程就是 “算法的骨架”。但是,每个人炒菜的“细节”可以不一样:你可能喜欢放点蒜末提香、朋友可能喜欢加一勺醋、你们可能用的锅、火候也不同。‌模板设计模式,就是为了解决这种 流程固定,细节可变 的情况而设计的。它把“炒菜”这个固定流程写在一个“父类”(可以理解为一个标准菜谱)里,这个流程是不能改的。而 “炒菜” 这个具体步骤则被定义成一个 “抽象方法”,交给具体的 “子类” 去实现。


它解决了什么问题?

模板设计模式主要解决了以下两个核心痛点:

  • ‌避免代码重复‌:如果每个子类都自己写一遍 “买菜->洗菜->切菜->装盘” 这些完全相同的步骤,代码就会非常冗余。模板模式把这部分公共代码提取到父类中,实现了‌代码复用‌。
  • ‌保证流程一致,同时允许灵活扩展‌:它确保了所有子类都遵循同一个核心流程,保证了系统行为的一致性。同时,当需要增加一种新的“菜”(比如“辣炒青菜”)时,只需要新建一个子类,去实现 “炒菜” 这个具体步骤即可,‌无需修改已有的父类和其他子类代码‌,这符合 开闭原则(对扩展开放,对修改关闭)。


模板的一个小例子

比如我们这里以一个生活中的小例子进行说明。我们知道泡茶和冲咖啡的步骤很像,都是:

烧开水 -> 冲泡 -> 倒进杯子 -> 加调料。


第一步:定义抽象基类(模板)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class Beverage {
// 这就是“模板方法”,定义了固定的顶层算法结构
// 设置为 final,防止子类破坏流程
public final void prepareRecipe() {
boilWater();
brew(); // 抽象步骤:交给子类
pourInCup();
addCondiments(); // 抽象步骤:交给子类
}

private void boilWater() { System.out.println("将水煮沸"); }
private void pourInCup() { System.out.println("倒进杯子"); }

// 下面是需要子类“填空”的抽象方法
protected abstract void brew();
protected abstract void addCondiments();
}

第二步:实现具体的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Coffee extends Beverage {
@Override
protected void brew() { System.out.println("用沸水冲泡咖啡粉"); }
@Override
protected void addCondiments() { System.out.println("加入糖和牛奶"); }
}

class Tea extends Beverage {
@Override
protected void brew() { System.out.println("浸泡茶叶"); }
@Override
protected void addCondiments() { System.out.println("加入柠檬"); }
}

第三步:客户端调用

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
Beverage myCoffee = new Coffee();
myCoffee.prepareRecipe(); // 执行父类定义的标准流程
}
}


Shiro 如何通过模板方法简化开发?

在 Shiro 中,最核心的 Realm 体系就是模板方法模式的教科书级应用。如果你要自定义一个 Realm,你通常不会直接实现 Realm 接口,而是继承 AuthorizingRealm


实现原理拆解:

Shiro 的父类已经帮你处理好了复杂的逻辑(如:缓存检查、数据转换、异常捕获),你只需要关心最核心的 “怎么从数据库查数据”。以认证为例,我们看看shiro的实现。

在 AuthenticatingRealm(AuthorizingRealm 的父类)中,定义了认证的 “模板”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) {
// 1. 检查缓存中是否已经有了
AuthenticationInfo info = getCachedAuthenticationInfo(token);

if (info == null) {
// 2. 核心:调用抽象方法,让子类去实现真正的查询逻辑!
info = doGetAuthenticationInfo(token);

// 3. 将结果放入缓存
if (token != null && info != null) {
cacheAuthenticationInfoIfNecessary(token, info);
}
}
return info;
}

// 这就是留给开发者的“填空题”
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token);

你作为开发者:

  • 只需要实现 doGetAuthenticationInfo(查用户)和 doGetAuthorizationInfo(查权限)。你不需要关心 Session 怎么存、缓存怎么清理、怎么比对密码,父类全帮你写好了。
  • 认证的流程(先查缓存、异常捕获、结果校验)是极其严谨的。通过模板方法,Shiro 保证了无论你写什么样的子类,基本的安全防御流程都不会乱。


模板另一个经典例子 HttpServlet

在 Web 处理中,无论你处理的是 “登录” 还是 “查询订单”,有一些流程是固定不变的:

  1. 接收 Request 和 Response 对象。
  2. 解析 HTTP 请求的方法(是 GET、POST 还是 PUT。。)。
  3. 根据方法分发逻辑。
  4. 异常处理。

而变化的部分只有:针对特定请求的具体业务逻辑。我们来看 HttpServlet 是如何定义这个模板的。

第一步:顶层骨架 —— service 方法

这是模板的核心。它定义了处理请求的标准流程:先获取方法名,再分发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这是 HttpServlet 里的模板方法
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

// 1. 获取请求方式(固定逻辑)
String method = req.getMethod();

// 2. 根据方式进行分发(算法骨架)
if (method.equals("GET")) {
doGet(req, resp); // 调用“填空”方法
} else if (method.equals("POST")) {
doPost(req, resp); // 调用“填空”方法
}
// ... 其他方法如 PUT, DELETE
}

第二步:默认实现(空的填空题)

HttpServlet 为这些 doXXX 方法提供了默认实现,但默认逻辑通常是返回一个 405 错误(表示不支持该方法),强制要求子类去“填空”。

1
2
3
4
5
protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
// 默认报错,等你重写
sendError(resp, 405);
}

第三部:开发者如何填空?

当你写一个自己的 Servlet 时,你不需要去管怎么解析 HTTP 协议,你只需要继承 HttpServlet 并覆盖你需要的方法:

1
2
3
4
5
6
7
public class MyServlet extends HttpServlet {
// 开发者只需要填充这个“坑”
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
System.out.println("处理业务逻辑...");
}
}

由此我们看到,shiro 和 servlet 都遵循了同一套设计原则,它们的设计哲学是一致的:都是把复杂的、重复的、容易出错的流程收口在父类,把简单的、多变的业务留给开发者。


委派模式

啥是委派模式呢?

委派模式的核心理念就是“这件事我不亲自做,我交给专业的人去做,但我对结果负责。”

通俗比喻就是:

  • 普通 Servlet(模板模式):你是小老板,凡事亲力亲为。虽然有一张流程表(模板),但扫地、接电话、写代码还是得你亲自在 doGet 里填代码实现。
  • DispatcherServlet(委派模式):你是大集团 CEO。你不再亲自扫地或写代码。
    • 有人要见你,你交给大秘(HandlerMapping)查查该去哪个部门。
    • 查到了部门,你交给主管(HandlerAdapter)去下达具体命令。
    • 主管执行完拿到报表,你交给翻译(ViewResolver)把报表画成 PPT 给客户看。
    • 你(DispatcherServlet)全程只负责调度,不写一行业务逻辑。


从 DispatcherServlet 说委派

上面我们说到了 Servlet 通过模板模式极大地简化了开发。实际上,Spring MVC 在此基础上又封装了一层,这就是 SpringMVC 的核心—— DispatcherServlet。这玩意儿就是一个 “超级调度中心”,它并没有像普通 Servlet 那样让你去每个类里“填坑”,而是通过委派模式(Delegate Pattern),把活儿全都分给了更专业的组件。DispatcherServlet 也不是一开始就用委派这种设计的,他也有一个进化的历史:


第一阶段:继承父类的模板

DispatcherServlet 的祖先依然是 HttpServlet。它重写了父类的 doService 方法(这是 Spring 对 service 的扩展),将其指向了一个核心逻辑:doDispatch。


第二阶段:由 “填空” 变为 “分发”

在 DispatcherServlet 内部,最重要的代码不是 doGet,而是 doDispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
// 1. 委派给 HandlerMapping:去帮我找找,这个 URL 归哪个 Controller 管?
HandlerExecutionChain mappedHandler = getHandler(request);

// 2. 委派给 HandlerAdapter:去帮我看看,怎么调用这个 Controller 的方法?
//(因为有的是注解式的,有的是实现接口式的,适配器能统一调用)
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 3. 真正干活:主管(适配器)去执行 Controller 里的业务
ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());

// 4. 委派给 ViewResolver:去帮我把结果渲染成 HTML 或 JSON
processDispatchResult(request, response, mappedHandler, mv, dispatchException);
}

由此带来的 “三大奇效”:

  • ① 彻底解耦:在普通 Servlet 中,你的 URL 路径和类是死死绑定的。在 Spring MVC 中,DispatcherServlet 根本不知道你会写什么 Controller,它只通过 HandlerMapping 动态查找。

  • ② 强大的扩展性(策略模式的变种)

    • 因为是委派给不同的组件,你可以非常方便地更换这些“打工仔”:
    • 想支持 JSON,给委派的对象加个 HttpMessageConverter。
    • 想支持新的 URL 规则,换一个 HandlerMapping 委派对象。。。等等。
    • 你不需要修改 DispatcherServlet 的源码,只需要配置不同的委派组件。
  • ③ 统一的横切面:就像 Shiro 的 SecurityManager 一样,由于所有的请求都要经过 DispatcherServlet 这个 CEO,它可以在分发任务之前,统一处理国际化、主题、文件上传等杂事。


发现了吗?这又回到了我们开始聊的 Shiro SecurityManager。

  • SecurityManager 也是一个 CEO,它把认证委派给 Authenticator,把授权委派给 Authorizer。

  • DispatcherServlet 也是一个 CEO,它把找路委派给 Mapping,把干活委派给 Adapter。

这就是很多现代框架的设计精髓:利用委派模式做调度,利用模板模式做基础,利用代理模式做拦截。