計(jì)算機(jī)辦公軟件培訓(xùn)班seo工程師招聘
文章目錄
- 模板方法模式
- 簡(jiǎn)介
- 作用
- 模板方法模式的缺點(diǎn)
- 模板方法模式的應(yīng)用場(chǎng)景
- 業(yè)務(wù)場(chǎng)景
- 開(kāi)源框架中的應(yīng)用
- 對(duì)比回調(diào)和Hook模式
- 關(guān)于組合優(yōu)先于繼承
- 關(guān)于設(shè)計(jì)模式亂用的現(xiàn)象
模板方法模式
簡(jiǎn)介
模板方法模式是一種行為型設(shè)計(jì)模式,該設(shè)計(jì)模式的核心在于通過(guò)抽象出一套相對(duì)標(biāo)準(zhǔn)的處理步驟,并可靈活的將任意步驟交給子類(lèi)去進(jìn)行擴(kuò)展,使得可以在不改變整體業(yè)務(wù)處理流程的前提下,通過(guò)定義不同的子類(lèi)實(shí)現(xiàn)即可完成業(yè)務(wù)處理的擴(kuò)展。
我們可以舉個(gè)簡(jiǎn)單的例子,比如對(duì)于下面定義的method
方法中調(diào)用的a、b、c
三個(gè)子方法,可以通過(guò)不同的子類(lèi)實(shí)現(xiàn)來(lái)完成不同業(yè)務(wù)邏輯的處理。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();protected abstract void b();protected abstract void a();}
還可以這樣定義,此時(shí)相當(dāng)于b
方法在父類(lèi)中有一套默認(rèn)的處理,子類(lèi)可以根據(jù)需要選擇重寫(xiě)或者不重寫(xiě)。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();protected void b() {// 默認(rèn)處理邏輯。。。}protected abstract void a();}
當(dāng)然,還可以將b
方法聲明為private
或者加上final
關(guān)鍵字從而禁止子類(lèi)重寫(xiě),此時(shí)b
方法的邏輯就完全由父類(lèi)統(tǒng)一管理。
public abstract class Temp {public final void method() {a();b();c();}protected abstract void c();private void b() {// 固定處理邏輯。。。}protected abstract void a();}
作用
模板方法模式主要有兩大作用:復(fù)用和擴(kuò)展。
復(fù)用:復(fù)用指的是像method
這樣的方法,所有子類(lèi)都可以拿來(lái)使用,復(fù)用該方法中定義的這套處理邏輯。
擴(kuò)展:擴(kuò)展的能力就更加強(qiáng)大了,狹義上可以針對(duì)代碼進(jìn)行擴(kuò)展,子類(lèi)可以獨(dú)立增加功能邏輯,而不影響其他的子類(lèi),符合開(kāi)閉原則,廣義上可以針對(duì)整個(gè)框架進(jìn)行擴(kuò)展,比如像下面這段代碼邏輯:
public class Temp {public final void method() {a();b();c();d();}protected void c() {// 默認(rèn)處理邏輯。。。};private void b() {// 固定處理邏輯。。。}protected void a() {// 默認(rèn)處理邏輯。。。}protected void d() {// 強(qiáng)制子類(lèi)必須重寫(xiě)throw new UnsupportedOperationException();}}
框架默認(rèn)可以直接使用,但同時(shí)也預(yù)留了a
、c
、d
三個(gè)方法的擴(kuò)展能力,且d
方法還通過(guò)拋出異常的方式,強(qiáng)制要求子類(lèi)必須重寫(xiě),所以現(xiàn)在完全可以通過(guò)方法重寫(xiě)的方式實(shí)現(xiàn)框架的功能擴(kuò)展。
這種框架擴(kuò)展的方式的典型案例就是Servlet
中定義的service
方法,該方法分別預(yù)留了doGet
和doPost
等擴(kuò)展方法。
模板方法模式的缺點(diǎn)
從另一個(gè)角度來(lái)說(shuō),設(shè)計(jì)模式本身實(shí)際上并不存在什么缺點(diǎn),真正導(dǎo)致出現(xiàn)這些問(wèn)題的原因還是使用設(shè)計(jì)模式的方式,尤其是新手在剛了解到設(shè)計(jì)模式的時(shí)候,往往會(huì)試圖到處找場(chǎng)景去套用各種設(shè)計(jì)模式,甚至一個(gè)方法能用上好幾種,這就是典型的手里拿個(gè)錘子,看什么都是釘子。所以,如果按照這樣的使用方式,通常就會(huì)導(dǎo)致子類(lèi)或者實(shí)現(xiàn)類(lèi)非常多,但邏輯卻很少,或相似;方法為了兼容各種場(chǎng)景而過(guò)于抽象,導(dǎo)致代碼復(fù)雜度增加,可閱讀性也變差。
針對(duì)模板方式模式來(lái)說(shuō),因?yàn)橥ǔG闆r下是通過(guò)繼承機(jī)制來(lái)實(shí)現(xiàn)業(yè)務(wù)流程的不變部分和可變部分的分離,因此,如果可變部分的業(yè)務(wù)邏輯并不復(fù)雜,或者不變部分和可變部分的關(guān)系不清晰時(shí),就不適合用模板方法模式了。
模板方法模式的應(yīng)用場(chǎng)景
業(yè)務(wù)的整體處理流程是固定的,但其中的個(gè)別部分是易變的,或者可擴(kuò)展的,此時(shí)就可以使用模板方法模式,下面我們分別舉一些常見(jiàn)的業(yè)務(wù)場(chǎng)景和開(kāi)源框架的應(yīng)用來(lái)說(shuō)明。
業(yè)務(wù)場(chǎng)景
訂單結(jié)算場(chǎng)景
訂單結(jié)算在電商平臺(tái)是非常常見(jiàn)的功能,整個(gè)結(jié)算過(guò)程一定會(huì)包含:訂單生成、庫(kù)存校驗(yàn)、費(fèi)用計(jì)算、結(jié)果通知,但比如其中費(fèi)用計(jì)算則可能在優(yōu)惠券、折扣、運(yùn)費(fèi)等地方又有所不同,因此可以將整個(gè)結(jié)算過(guò)程抽象為一個(gè)模板類(lèi),具體的結(jié)算類(lèi)只需要繼承該模板類(lèi),并實(shí)現(xiàn)具體的計(jì)算規(guī)則即可。
任務(wù)活動(dòng)場(chǎng)景
常見(jiàn)的任務(wù)活動(dòng),主要包含三步驟:任務(wù)事件接收、任務(wù)規(guī)則匹配、任務(wù)獎(jiǎng)勵(lì)觸發(fā),而往往事件接收和獎(jiǎng)勵(lì)觸發(fā)都是比較統(tǒng)一的,規(guī)則匹配則跟具體的任務(wù)相關(guān),所以可以用模板方法模式來(lái)實(shí)現(xiàn)。
開(kāi)源框架中的應(yīng)用
Spring MVC
handleRequestInternal
由子類(lèi)實(shí)現(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)典的模板方法模式應(yīng)用,其主要是用來(lái)執(zhí)行SQL
,query
方法是模板方法的主流程,doQuery
方法是其留給子類(lèi)實(shí)現(xiàn)的。
public abstract class BaseExecutor implements Executor {// 幾個(gè)do開(kāi)頭的方法都是留給子類(lèi)實(shí)現(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方式,交由子類(lèi)實(shí)現(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抽象類(lèi)
AbstractCollection
中實(shí)現(xiàn)了Set
接口中定義的addAll
方法,該方法又是基于add
方法來(lái)實(shí)現(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
方法,而是希望子類(lèi)自己去實(shí)現(xiàn),如果調(diào)用者不小心直接調(diào)用了AbstractCollection
的add
方法,則會(huì)直接拋出異常。
public boolean add(E e) {throw new UnsupportedOperationException();
}
對(duì)比回調(diào)和Hook模式
回調(diào)和Hook
這兩種模式,在一定程度上也能起到模板方法模式的效果,他們都可以在一套流程中預(yù)留某個(gè)擴(kuò)展點(diǎn),然后將這個(gè)擴(kuò)展點(diǎn)交由請(qǐng)求方自己來(lái)實(shí)現(xiàn),最常見(jiàn)的就是支付場(chǎng)景,在請(qǐng)求支付的時(shí)候,往往是不會(huì)同步等待支付結(jié)果的,而是在請(qǐng)求的同時(shí)注冊(cè)一個(gè)回調(diào)接口,這樣三方支付系統(tǒng)完成支付之后,就會(huì)回調(diào)這個(gè)接口來(lái)完成支付結(jié)果的通知。
雖然從應(yīng)用場(chǎng)景上來(lái)回調(diào)或者Hook
模式和模板方法模式差不多,但從代碼實(shí)現(xiàn)方式來(lái)看,卻有很大差異,模板方法模式是基于繼承的方式來(lái)實(shí)現(xiàn)的,這實(shí)際上是有很大的局限性,而回調(diào)或者Hook
模式則是基于組合方式來(lái)實(shí)現(xiàn)的,我們都知道組合優(yōu)于繼承,其次,回調(diào)或者Hook
模式還可以基于匿名類(lèi)的方式來(lái)實(shí)現(xiàn),不用事先定義類(lèi),顯然更加靈活,當(dāng)然,回調(diào)也有其問(wèn)題,使用不當(dāng),容易出現(xiàn)調(diào)用關(guān)系混亂,系統(tǒng)層次混亂等現(xiàn)象。
關(guān)于組合優(yōu)先于繼承
繼承是實(shí)現(xiàn)代碼重用的重要手段之一,但并非是實(shí)現(xiàn)代碼重用的最佳方式,繼承打破了封裝性,因此很容易在使用時(shí)產(chǎn)生問(wèn)題,為了更好的說(shuō)明這一點(diǎn),我們來(lái)舉個(gè)例子,假設(shè)我們現(xiàn)在需要為HashSet``添加一個(gè)計(jì)數(shù)功能,即看看HashSet
自創(chuàng)建以來(lái),一共被添加過(guò)多少個(gè)元素,我們可以用下面這種方式來(lái)實(shí)現(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
,問(wèn)題就在于前面介紹的AbstractCollection
關(guān)于addAll
的實(shí)現(xiàn)方式,很明顯在addAll
方法中調(diào)用add
方法時(shí)被重復(fù)統(tǒng)計(jì)了,你不能因此說(shuō)是addAll
的實(shí)現(xiàn)方法有問(wèn)題。
也許你只要像下面這段代碼一樣,就能修復(fù)這個(gè)問(wèn)題,但這又依賴一個(gè)事實(shí):addAll
方法是在add
方法中實(shí)現(xiàn)的,這實(shí)際上并不是什么標(biāo)準(zhǔn),你也不能保證在之后的版本中不會(huì)發(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());}
}
看吧,這就是使用組合的威力,組合更像是裝飾者模式,他可以在不改變?cè)蓄?lèi)的功能的前提下,輕松實(shí)現(xiàn)功能的擴(kuò)展,最重要的是,他比繼承要可靠的多。
關(guān)于設(shè)計(jì)模式亂用的現(xiàn)象
最后,再來(lái)聊聊關(guān)于設(shè)計(jì)模式亂用的問(wèn)題,主要突出為以下兩個(gè)階段:
- 新手:這經(jīng)常發(fā)生在剛接觸設(shè)計(jì)模式不久的階段,急于找地方使用的情況,開(kāi)發(fā)人員不考慮實(shí)際的業(yè)務(wù)場(chǎng)景,完全是為了用設(shè)計(jì)模式而用設(shè)計(jì)模式,甚至是先想好要用什么樣的設(shè)計(jì)模式,然后讓業(yè)務(wù)邏輯盡量往這個(gè)模式上去套。
- 勝任者:過(guò)了新手階段之后,此時(shí)你對(duì)設(shè)計(jì)模式也有一定使用經(jīng)驗(yàn)了,開(kāi)始意識(shí)到胡亂使用設(shè)計(jì)模式造成的問(wèn)題了,懂得了理解業(yè)務(wù)場(chǎng)景才是關(guān)鍵,那還有什么問(wèn)題呢?此時(shí)的階段就好比術(shù)和道的區(qū)別,術(shù)是多變的,就像我們常說(shuō)的23種設(shè)計(jì)模式一樣,而道是不變的,無(wú)論哪種設(shè)計(jì)模式始終都是以幾種設(shè)計(jì)原則為依據(jù),正所謂萬(wàn)變不離其宗,設(shè)計(jì)模式的使用不應(yīng)當(dāng)局限于形式上,要能靈活變換。
- 精通者:如果跨過(guò)新手階段的關(guān)鍵在于多寫(xiě)多練的話,那么要跨過(guò)勝任者階段則要多思考了,得道的關(guān)鍵在于領(lǐng)悟。