北京网站建设方案排名,软件工程师40岁后的出路,迎访问中国建设银行网站-,wordpress设置页面加载文章目录 模板方法模式简介作用模板方法模式的缺点模板方法模式的应用场景业务场景开源框架中的应用 对比回调和Hook模式关于组合优先于继承 关于设计模式乱用的现象 模板方法模式
简介
模板方法模式是一种行为型设计模式#xff0c;该设计模式的核心在于通过抽象出一套相对… 文章目录 模板方法模式简介作用模板方法模式的缺点模板方法模式的应用场景业务场景开源框架中的应用 对比回调和Hook模式关于组合优先于继承 关于设计模式乱用的现象 模板方法模式
简介
模板方法模式是一种行为型设计模式该设计模式的核心在于通过抽象出一套相对标准的处理步骤并可灵活的将任意步骤交给子类去进行扩展使得可以在不改变整体业务处理流程的前提下通过定义不同的子类实现即可完成业务处理的扩展。
我们可以举个简单的例子比如对于下面定义的method方法中调用的a、b、c三个子方法可以通过不同的子类实现来完成不同业务逻辑的处理。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();protected abstract void b();protected abstract void a();}还可以这样定义此时相当于b方法在父类中有一套默认的处理子类可以根据需要选择重写或者不重写。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();protected void b() {// 默认处理逻辑。。。}protected abstract void a();}当然还可以将b方法声明为private或者加上final关键字从而禁止子类重写此时b方法的逻辑就完全由父类统一管理。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();private void b() {// 固定处理逻辑。。。}protected abstract void a();}作用
模板方法模式主要有两大作用复用和扩展。
复用复用指的是像method这样的方法所有子类都可以拿来使用复用该方法中定义的这套处理逻辑。
扩展扩展的能力就更加强大了狭义上可以针对代码进行扩展子类可以独立增加功能逻辑而不影响其他的子类符合开闭原则广义上可以针对整个框架进行扩展比如像下面这段代码逻辑
public class Temp {public final void method() {a();b();c();d();}protected void c() {// 默认处理逻辑。。。};private void b() {// 固定处理逻辑。。。}protected void a() {// 默认处理逻辑。。。}protected void d() {// 强制子类必须重写throw new UnsupportedOperationException();}}框架默认可以直接使用但同时也预留了a、c、d三个方法的扩展能力且d方法还通过抛出异常的方式强制要求子类必须重写所以现在完全可以通过方法重写的方式实现框架的功能扩展。 这种框架扩展的方式的典型案例就是Servlet中定义的service方法该方法分别预留了doGet和doPost等扩展方法。
模板方法模式的缺点
从另一个角度来说设计模式本身实际上并不存在什么缺点真正导致出现这些问题的原因还是使用设计模式的方式尤其是新手在刚了解到设计模式的时候往往会试图到处找场景去套用各种设计模式甚至一个方法能用上好几种这就是典型的手里拿个锤子看什么都是钉子。所以如果按照这样的使用方式通常就会导致子类或者实现类非常多但逻辑却很少或相似方法为了兼容各种场景而过于抽象导致代码复杂度增加可阅读性也变差。
针对模板方式模式来说因为通常情况下是通过继承机制来实现业务流程的不变部分和可变部分的分离因此如果可变部分的业务逻辑并不复杂或者不变部分和可变部分的关系不清晰时就不适合用模板方法模式了。
模板方法模式的应用场景
业务的整体处理流程是固定的但其中的个别部分是易变的或者可扩展的此时就可以使用模板方法模式下面我们分别举一些常见的业务场景和开源框架的应用来说明。
业务场景
订单结算场景
订单结算在电商平台是非常常见的功能整个结算过程一定会包含订单生成、库存校验、费用计算、结果通知但比如其中费用计算则可能在优惠券、折扣、运费等地方又有所不同因此可以将整个结算过程抽象为一个模板类具体的结算类只需要继承该模板类并实现具体的计算规则即可。
任务活动场景
常见的任务活动主要包含三步骤任务事件接收、任务规则匹配、任务奖励触发而往往事件接收和奖励触发都是比较统一的规则匹配则跟具体的任务相关所以可以用模板方法模式来实现。
开源框架中的应用
Spring MVC
handleRequestInternal由子类实现
public abstract class AbstractController extends WebContentGenerator implements Controller {OverrideNullablepublic ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {if (HttpMethod.OPTIONS.matches(request.getMethod())) {response.setHeader(Allow, getAllowHeader());return null;}// Delegate to WebContentGenerator for checking and preparing.checkRequest(request);prepareResponse(response);// Execute handleRequestInternal in synchronized block if required.if (this.synchronizeOnSession) {HttpSession session request.getSession(false);if (session ! null) {Object mutex WebUtils.getSessionMutex(session);synchronized (mutex) {return handleRequestInternal(request, response);}}}return handleRequestInternal(request, response);}/*** Template method. Subclasses must implement this.* The contract is the same as for {code handleRequest}.* see #handleRequest*/Nullableprotected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)throws Exception;}
MyBatis
BaseExecutor是MyBatis中经典的模板方法模式应用其主要是用来执行SQLquery方法是模板方法的主流程doQuery方法是其留给子类实现的。 public abstract class BaseExecutor implements Executor {// 几个do开头的方法都是留给子类实现的protected abstract int doUpdate(MappedStatement ms, Object parameter)throws SQLException;protected abstract ListBatchResult doFlushStatements(boolean isRollback)throws SQLException;protected abstract E ListE doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)throws SQLException;protected abstract E CursorE doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)throws SQLException; Overridepublic E ListE query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql ms.getBoundSql(parameter);CacheKey key createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}SuppressWarnings(unchecked)Overridepublic E ListE query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity(executing a query).object(ms.getId());if (closed) {throw new ExecutorException(Executor was closed.);}if (queryStack 0 ms.isFlushCacheRequired()) {clearLocalCache();}ListE list;try {queryStack;list resultHandler null ? (ListE) localCache.getObject(key) : null;if (list ! null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}
}private E ListE queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ListE list;localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 具体query方式交由子类实现list doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {localCache.removeObject(key);}localCache.putObject(key, list);if (ms.getStatementType() StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;}
JDK AbstractCollection抽象类
AbstractCollection中实现了Set接口中定义的addAll方法该方法又是基于add方法来实现的具体代码如下所示
public boolean addAll(Collection? extends E c) {boolean modified false;for (E e : c)if (add(e))modified true;return modified;
}但AbstractCollection本身并不处理add方法而是希望子类自己去实现如果调用者不小心直接调用了AbstractCollection的add方法则会直接抛出异常。
public boolean add(E e) {throw new UnsupportedOperationException();
}对比回调和Hook模式
回调和Hook这两种模式在一定程度上也能起到模板方法模式的效果他们都可以在一套流程中预留某个扩展点然后将这个扩展点交由请求方自己来实现最常见的就是支付场景在请求支付的时候往往是不会同步等待支付结果的而是在请求的同时注册一个回调接口这样三方支付系统完成支付之后就会回调这个接口来完成支付结果的通知。
虽然从应用场景上来回调或者Hook模式和模板方法模式差不多但从代码实现方式来看却有很大差异模板方法模式是基于继承的方式来实现的这实际上是有很大的局限性而回调或者Hook模式则是基于组合方式来实现的我们都知道组合优于继承其次回调或者Hook模式还可以基于匿名类的方式来实现不用事先定义类显然更加灵活当然回调也有其问题使用不当容易出现调用关系混乱系统层次混乱等现象。
关于组合优先于继承
继承是实现代码重用的重要手段之一但并非是实现代码重用的最佳方式继承打破了封装性因此很容易在使用时产生问题为了更好的说明这一点我们来举个例子假设我们现在需要为HashSet添加一个计数功能即看看HashSet自创建以来一共被添加过多少个元素我们可以用下面这种方式来实现
public class CountHashSetE extends HashSetE {private int addCount 0;public CountHashSet() {}Overridepublic boolean add(E e) {addCount;return super.add(e);}Overridepublic boolean addAll(Collection? extends E c) {addCount c.size();return super.addAll(c);}public int getAddCount() {return addCount;}
}class Main {public static void main(String[] args) {CountHashSetInteger countHashSet new CountHashSet();countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}很遗憾最终输出结果并不是3而是6问题就在于前面介绍的AbstractCollection关于addAll的实现方式很明显在addAll方法中调用add方法时被重复统计了你不能因此说是addAll的实现方法有问题。
也许你只要像下面这段代码一样就能修复这个问题但这又依赖一个事实addAll方法是在add方法中实现的这实际上并不是什么标准你也不能保证在之后的版本中不会发生变化。
public class CountHashSetE extends HashSetE {private int addCount 0;public CountHashSet() {}Overridepublic boolean add(E e) {addCount;return super.add(e);}// Override
// public boolean addAll(Collection? extends E c) {
// addCount c.size();
// return super.addAll(c);
// }public int getAddCount() {return addCount;}
}class Main {public static void main(String[] args) {CountHashSetInteger countHashSet new CountHashSet();countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}使用组合的方式
public class ForwardingSetE implements SetE {private final SetE s;public ForwardingSet(SetE s) {this.s s;}Overridepublic int size() {return s.size();}Overridepublic boolean isEmpty() {return s.isEmpty();}Overridepublic boolean contains(Object o) {return s.contains(o);}Overridepublic IteratorE iterator() {return s.iterator();}Overridepublic Object[] toArray() {return s.toArray();}Overridepublic T T[] toArray(T[] a) {return s.toArray(a);}Overridepublic boolean add(E e) {return s.add(e);}Overridepublic boolean remove(Object o) {return s.remove(o);}Overridepublic boolean containsAll(Collection? c) {return s.containsAll(c);}Overridepublic boolean addAll(Collection? extends E c) {return s.addAll(c);}Overridepublic boolean retainAll(Collection? c) {return s.retainAll(c);}Overridepublic boolean removeAll(Collection? c) {return s.removeAll(c);}Overridepublic void clear() {s.clear();}
}class CountSetE extends ForwardingSetE {private int addCount 0;public CountSet(SetE s) {super(s);}Overridepublic boolean add(E e) {addCount;return super.add(e);}Overridepublic boolean addAll(Collection? extends E c) {addCount c.size();return super.addAll(c);}public int getAddCount() {return addCount;}
}class Main {public static void main(String[] args) {CountSetInteger countHashSet new CountSet(new HashSet());countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}看吧这就是使用组合的威力组合更像是装饰者模式他可以在不改变原有类的功能的前提下轻松实现功能的扩展最重要的是他比继承要可靠的多。
关于设计模式乱用的现象
最后再来聊聊关于设计模式乱用的问题主要突出为以下两个阶段
新手这经常发生在刚接触设计模式不久的阶段急于找地方使用的情况开发人员不考虑实际的业务场景完全是为了用设计模式而用设计模式甚至是先想好要用什么样的设计模式然后让业务逻辑尽量往这个模式上去套。胜任者过了新手阶段之后此时你对设计模式也有一定使用经验了开始意识到胡乱使用设计模式造成的问题了懂得了理解业务场景才是关键那还有什么问题呢此时的阶段就好比术和道的区别术是多变的就像我们常说的23种设计模式一样而道是不变的无论哪种设计模式始终都是以几种设计原则为依据正所谓万变不离其宗设计模式的使用不应当局限于形式上要能灵活变换。精通者如果跨过新手阶段的关键在于多写多练的话那么要跨过胜任者阶段则要多思考了得道的关键在于领悟。