设计模式之模板和委派
模板模式
什么是模板模式?
想象一下,你和朋友决定一起做一道菜,比如 “炒青菜”。你们都知道做这道菜的基本流程是固定的:买菜 -> 洗菜 -> 切菜 -> 炒菜 -> 装盘。这个流程就是 “算法的骨架”。但是,每个人炒菜的“细节”可以不一样:你可能喜欢放点蒜末提香、朋友可能喜欢加一勺醋、你们可能用的锅、火候也不同。模板设计模式,就是为了解决这种 流程固定,细节可变 的情况而设计的。它把“炒菜”这个固定流程写在一个“父类”(可以理解为一个标准菜谱)里,这个流程是不能改的。而 “炒菜” 这个具体步骤则被定义成一个 “抽象方法”,交给具体的 “子类” 去实现。
它解决了什么问题?
模板设计模式主要解决了以下两个核心痛点:
- 避免代码重复:如果每个子类都自己写一遍 “买菜->洗菜->切菜->装盘” 这些完全相同的步骤,代码就会非常冗余。模板模式把这部分公共代码提取到父类中,实现了
代码复用。 - 保证流程一致,同时允许灵活扩展:它确保了所有子类都遵循同一个核心流程,保证了系统行为的一致性。同时,当需要增加一种新的“菜”(比如“辣炒青菜”)时,只需要新建一个子类,去实现 “炒菜” 这个具体步骤即可,无需修改已有的父类和其他子类代码,这符合
开闭原则(对扩展开放,对修改关闭)。
模板的一个小例子
比如我们这里以一个生活中的小例子进行说明。我们知道泡茶和冲咖啡的步骤很像,都是:
烧开水 -> 冲泡 -> 倒进杯子 -> 加调料。
第一步:定义抽象基类(模板)
1 | public abstract class Beverage { |
第二步:实现具体的子类
1 | class Coffee extends Beverage { |
第三步:客户端调用
1 | public class Client { |
Shiro 如何通过模板方法简化开发?
在 Shiro 中,最核心的 Realm 体系就是模板方法模式的教科书级应用。如果你要自定义一个 Realm,你通常不会直接实现 Realm 接口,而是继承 AuthorizingRealm。
实现原理拆解:
Shiro 的父类已经帮你处理好了复杂的逻辑(如:缓存检查、数据转换、异常捕获),你只需要关心最核心的 “怎么从数据库查数据”。以认证为例,我们看看shiro的实现。
在 AuthenticatingRealm(AuthorizingRealm 的父类)中,定义了认证的 “模板”:
1 | public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) { |
你作为开发者:
- 只需要实现
doGetAuthenticationInfo(查用户)和doGetAuthorizationInfo(查权限)。你不需要关心 Session 怎么存、缓存怎么清理、怎么比对密码,父类全帮你写好了。 - 认证的流程(先查缓存、异常捕获、结果校验)是极其严谨的。通过模板方法,Shiro 保证了无论你写什么样的子类,基本的安全防御流程都不会乱。
模板另一个经典例子 HttpServlet
在 Web 处理中,无论你处理的是 “登录” 还是 “查询订单”,有一些流程是固定不变的:
- 接收 Request 和 Response 对象。
- 解析 HTTP 请求的方法(是 GET、POST 还是 PUT。。)。
- 根据方法分发逻辑。
- 异常处理。
而变化的部分只有:针对特定请求的具体业务逻辑。我们来看 HttpServlet 是如何定义这个模板的。
第一步:顶层骨架 —— service 方法
这是模板的核心。它定义了处理请求的标准流程:先获取方法名,再分发。
1 | // 这是 HttpServlet 里的模板方法 |
第二步:默认实现(空的填空题)
HttpServlet 为这些 doXXX 方法提供了默认实现,但默认逻辑通常是返回一个 405 错误(表示不支持该方法),强制要求子类去“填空”。
1 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) |
第三部:开发者如何填空?
当你写一个自己的 Servlet 时,你不需要去管怎么解析 HTTP 协议,你只需要继承 HttpServlet 并覆盖你需要的方法:
1 | public class MyServlet extends HttpServlet { |
由此我们看到,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 | protected void doDispatch(HttpServletRequest request, HttpServletResponse response) { |
由此带来的 “三大奇效”:
① 彻底解耦:在普通 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。
这就是很多现代框架的设计精髓:利用委派模式做调度,利用模板模式做基础,利用代理模式做拦截。