做視頻播放網(wǎng)站百度問答庫
文章目錄
- 模板方法模式
- 簡介
- 作用
- 模板方法模式的缺點
- 模板方法模式的應用場景
- 業(yè)務場景
- 開源框架中的應用
- 對比回調(diào)和Hook模式
- 關于組合優(yōu)先于繼承
- 關于設計模式亂用的現(xiàn)象
模板方法模式
簡介
模板方法模式是一種行為型設計模式,該設計模式的核心在于通過抽象出一套相對標準的處理步驟,并可靈活的將任意步驟交給子類去進行擴展,使得可以在不改變整體業(yè)務處理流程的前提下,通過定義不同的子類實現(xiàn)即可完成業(yè)務處理的擴展。
我們可以舉個簡單的例子,比如對于下面定義的method
方法中調(diào)用的a、b、c
三個子方法,可以通過不同的子類實現(xiàn)來完成不同業(yè)務邏輯的處理。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();protected abstract void b();protected abstract void a();}
還可以這樣定義,此時相當于b
方法在父類中有一套默認的處理,子類可以根據(jù)需要選擇重寫或者不重寫。
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
方法的邏輯就完全由父類統(tǒng)一管理。
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
方法還通過拋出異常的方式,強制要求子類必須重寫,所以現(xiàn)在完全可以通過方法重寫的方式實現(xiàn)框架的功能擴展。
這種框架擴展的方式的典型案例就是Servlet
中定義的service
方法,該方法分別預留了doGet
和doPost
等擴展方法。
模板方法模式的缺點
從另一個角度來說,設計模式本身實際上并不存在什么缺點,真正導致出現(xiàn)這些問題的原因還是使用設計模式的方式,尤其是新手在剛了解到設計模式的時候,往往會試圖到處找場景去套用各種設計模式,甚至一個方法能用上好幾種,這就是典型的手里拿個錘子,看什么都是釘子。所以,如果按照這樣的使用方式,通常就會導致子類或者實現(xiàn)類非常多,但邏輯卻很少,或相似;方法為了兼容各種場景而過于抽象,導致代碼復雜度增加,可閱讀性也變差。
針對模板方式模式來說,因為通常情況下是通過繼承機制來實現(xiàn)業(yè)務流程的不變部分和可變部分的分離,因此,如果可變部分的業(yè)務邏輯并不復雜,或者不變部分和可變部分的關系不清晰時,就不適合用模板方法模式了。
模板方法模式的應用場景
業(yè)務的整體處理流程是固定的,但其中的個別部分是易變的,或者可擴展的,此時就可以使用模板方法模式,下面我們分別舉一些常見的業(yè)務場景和開源框架的應用來說明。
業(yè)務場景
訂單結(jié)算場景
訂單結(jié)算在電商平臺是非常常見的功能,整個結(jié)算過程一定會包含:訂單生成、庫存校驗、費用計算、結(jié)果通知,但比如其中費用計算則可能在優(yōu)惠券、折扣、運費等地方又有所不同,因此可以將整個結(jié)算過程抽象為一個模板類,具體的結(jié)算類只需要繼承該模板類,并實現(xiàn)具體的計算規(guī)則即可。
任務活動場景
常見的任務活動,主要包含三步驟:任務事件接收、任務規(guī)則匹配、任務獎勵觸發(fā),而往往事件接收和獎勵觸發(fā)都是比較統(tǒng)一的,規(guī)則匹配則跟具體的任務相關,所以可以用模板方法模式來實現(xiàn)。
開源框架中的應用
Spring MVC
handleRequestInternal
由子類實現(xiàn)
public abstract class AbstractController extends WebContentGenerator implements Controller {@Override@Nullablepublic 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
中經(jīng)典的模板方法模式應用,其主要是用來執(zhí)行SQL
,query
方法是模板方法的主流程,doQuery
方法是其留給子類實現(xiàn)的。
public abstract class BaseExecutor implements Executor {// 幾個do開頭的方法都是留給子類實現(xiàn)的protected abstract int doUpdate(MappedStatement ms, Object parameter)throws SQLException;protected abstract List<BatchResult> doFlushStatements(boolean isRollback)throws SQLException;protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)throws SQLException;protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)throws SQLException; @Overridepublic <E> List<E> 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> List<E> 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();}List<E> list;try {queryStack++;list = resultHandler == null ? (List<E>) 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> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 具體query方式,交由子類實現(xiàn)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
中實現(xiàn)了Set
接口中定義的addAll
方法,該方法又是基于add
方法來實現(xiàn)的,具體代碼如下所示:
public boolean addAll(Collection<? extends E> c) {boolean modified = false;for (E e : c)if (add(e))modified = true;return modified;
}
但AbstractCollection
本身并不處理add
方法,而是希望子類自己去實現(xiàn),如果調(diào)用者不小心直接調(diào)用了AbstractCollection
的add
方法,則會直接拋出異常。
public boolean add(E e) {throw new UnsupportedOperationException();
}
對比回調(diào)和Hook模式
回調(diào)和Hook
這兩種模式,在一定程度上也能起到模板方法模式的效果,他們都可以在一套流程中預留某個擴展點,然后將這個擴展點交由請求方自己來實現(xiàn),最常見的就是支付場景,在請求支付的時候,往往是不會同步等待支付結(jié)果的,而是在請求的同時注冊一個回調(diào)接口,這樣三方支付系統(tǒng)完成支付之后,就會回調(diào)這個接口來完成支付結(jié)果的通知。
雖然從應用場景上來回調(diào)或者Hook
模式和模板方法模式差不多,但從代碼實現(xiàn)方式來看,卻有很大差異,模板方法模式是基于繼承的方式來實現(xiàn)的,這實際上是有很大的局限性,而回調(diào)或者Hook
模式則是基于組合方式來實現(xiàn)的,我們都知道組合優(yōu)于繼承,其次,回調(diào)或者Hook
模式還可以基于匿名類的方式來實現(xiàn),不用事先定義類,顯然更加靈活,當然,回調(diào)也有其問題,使用不當,容易出現(xiàn)調(diào)用關系混亂,系統(tǒng)層次混亂等現(xiàn)象。
關于組合優(yōu)先于繼承
繼承是實現(xiàn)代碼重用的重要手段之一,但并非是實現(xiàn)代碼重用的最佳方式,繼承打破了封裝性,因此很容易在使用時產(chǎn)生問題,為了更好的說明這一點,我們來舉個例子,假設我們現(xiàn)在需要為HashSet``添加一個計數(shù)功能,即看看HashSet
自創(chuàng)建以來,一共被添加過多少個元素,我們可以用下面這種方式來實現(xiàn):
public class CountHashSet<E> extends HashSet<E> {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) {CountHashSet<Integer> countHashSet = new CountHashSet<>();countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}
很遺憾最終輸出結(jié)果并不是3
,而是6
,問題就在于前面介紹的AbstractCollection
關于addAll
的實現(xiàn)方式,很明顯在addAll
方法中調(diào)用add
方法時被重復統(tǒng)計了,你不能因此說是addAll
的實現(xiàn)方法有問題。
也許你只要像下面這段代碼一樣,就能修復這個問題,但這又依賴一個事實:addAll
方法是在add
方法中實現(xiàn)的,這實際上并不是什么標準,你也不能保證在之后的版本中不會發(fā)生變化。
public class CountHashSet<E> extends HashSet<E> {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) {CountHashSet<Integer> countHashSet = new CountHashSet<>();countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}
使用組合的方式
public class ForwardingSet<E> implements Set<E> {private final Set<E> s;public ForwardingSet(Set<E> 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 Iterator<E> 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 CountSet<E> extends ForwardingSet<E> {private int addCount = 0;public CountSet(Set<E> 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) {CountSet<Integer> countHashSet = new CountSet<>(new HashSet<>());countHashSet.addAll(Arrays.asList(1, 2, 3));System.out.println(countHashSet.getAddCount());}
}
看吧,這就是使用組合的威力,組合更像是裝飾者模式,他可以在不改變原有類的功能的前提下,輕松實現(xiàn)功能的擴展,最重要的是,他比繼承要可靠的多。
關于設計模式亂用的現(xiàn)象
最后,再來聊聊關于設計模式亂用的問題,主要突出為以下兩個階段:
- 新手:這經(jīng)常發(fā)生在剛接觸設計模式不久的階段,急于找地方使用的情況,開發(fā)人員不考慮實際的業(yè)務場景,完全是為了用設計模式而用設計模式,甚至是先想好要用什么樣的設計模式,然后讓業(yè)務邏輯盡量往這個模式上去套。
- 勝任者:過了新手階段之后,此時你對設計模式也有一定使用經(jīng)驗了,開始意識到胡亂使用設計模式造成的問題了,懂得了理解業(yè)務場景才是關鍵,那還有什么問題呢?此時的階段就好比術和道的區(qū)別,術是多變的,就像我們常說的23種設計模式一樣,而道是不變的,無論哪種設計模式始終都是以幾種設計原則為依據(jù),正所謂萬變不離其宗,設計模式的使用不應當局限于形式上,要能靈活變換。
- 精通者:如果跨過新手階段的關鍵在于多寫多練的話,那么要跨過勝任者階段則要多思考了,得道的關鍵在于領悟。