為什么總是用不好設(shè)計(jì)模式?
先點(diǎn)贊再看,養(yǎng)成好習(xí)慣
前言
經(jīng)??吹揭恍┰O(shè)計(jì)模式的文章,寫了很多內(nèi)容,也舉了一些很“生動(dòng)形象”的例子。
但是可能和《Head First 設(shè)計(jì)模式》會(huì)有一樣的問題:看完了,我會(huì)了,但是好像用不上?或者是硬套設(shè)計(jì)模式。
舉幾個(gè)我見過的極端的例子:
倆字段,也要來個(gè) Builder
3個(gè) if,提個(gè)策略模式
5行代碼還很簡單的初始化,也要弄個(gè) Factory
……
至于為什么會(huì)出現(xiàn)這種問題……我聊聊我的看法
原因
大多數(shù)的研發(fā)人員,做的工作都是業(yè)務(wù)功能開發(fā),也就是常說的 CRUD。只是不同的業(yè)務(wù)場景,CRUD的復(fù)雜度不同而已。
可是對于業(yè)務(wù)代碼來說,很多情況下不太好套用設(shè)計(jì)模式,或者說沒法很好的應(yīng)用設(shè)計(jì)模式。
平時(shí)看到的最多的是策略模式的文章吧,為什么呢?
我猜是因?yàn)檫@個(gè)最好寫,應(yīng)用在業(yè)務(wù)代碼里比較簡單;隨便一個(gè)稍微復(fù)雜點(diǎn)的場景,就可以套用一下策略模式,把多個(gè) if 拆分到多個(gè)類里。
的確,業(yè)務(wù)代碼里適當(dāng)?shù)氖褂貌呗阅J娇梢越档蛷?fù)雜度;但就算用也得住一個(gè)度,不要把各種業(yè)務(wù)里的 if 都換成策略模式了,不然代碼會(huì)炸的……
之前見過一個(gè)項(xiàng)目,雖然是內(nèi)部xx系統(tǒng),但那個(gè)研發(fā)小哥可能是走火入魔了。抽了 80 多個(gè)策略類出來,這八十多個(gè)類里又分為十幾個(gè)組,每組七八個(gè)策略類,但每個(gè)類里的代碼,也不過十幾二十行,而且還有重復(fù)代碼。
我當(dāng)時(shí)問了他一句:你在養(yǎng)蠱嗎?
像這個(gè)研發(fā)小哥就是一個(gè)反例,濫用設(shè)計(jì)模式,過分的將各種分支代碼全部套進(jìn)設(shè)計(jì)模式了。不過我猜想他可能是為了學(xué)習(xí)吧,學(xué)以致用……
其他的委托代理狀態(tài)之類的模式,想應(yīng)用在業(yè)務(wù)代碼里,就比較費(fèi)勁了,因?yàn)闆]有那么多合適的場景。但策略模式則不同,有 if 的地方都可以嘗試套一下……
可是設(shè)計(jì)模式是用來解決問題,降低/轉(zhuǎn)移復(fù)雜度的,而不是增加復(fù)雜度。
非業(yè)務(wù)代碼里的設(shè)計(jì)模式
跳出業(yè)務(wù)代碼來,甚至說跳出純業(yè)務(wù)代碼來之后,想應(yīng)用設(shè)計(jì)模式就比較簡單了,甚至不需要你硬套,遇到問題時(shí)就自然的會(huì)想到用設(shè)計(jì)模式來解決。
舉個(gè)栗子
系統(tǒng)里一般需要一個(gè) traceId/requestId 來將整個(gè)鏈路串起來,配合日志打印或者集中式的APM抽取。
就拿單體應(yīng)用來說,一般用日志框架的 MDC 來綁定這個(gè) traceId。在 Filter 或者 一些 AOP 里,給 MDC 一個(gè) traceID,那么整個(gè)調(diào)用鏈路都可以用這一個(gè) ID,打印日志時(shí)就可以根據(jù) traceId 區(qū)分不同請求了,就像這樣:
2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - 請求第0步
2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - 請求第1步
2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - 請求第2步
2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - 請求第0步
2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - 請求第1步
2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - 請求第2步
...
復(fù)制代碼通過 000/111 這個(gè) traceId 就可以區(qū)分是哪個(gè)請求。
可 MDC 是通過 ThreadLocal 進(jìn)行存儲(chǔ)數(shù)據(jù)的,ThreadLocal 畢竟是和線程綁定的。如果鏈路中使用了線程池處理,那可怎么辦?線程池里子線程打印日志的時(shí)候,MDC 可獲取不到主線程的 traceId,但對于這個(gè)請求來說,主子線程都是一個(gè)鏈路……
還記得這句話嗎?
計(jì)算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過增加一個(gè)間接的中間層來解決”
這里借助委托模式,來增加一個(gè)中間層,問題就很好解決了。
既然是主子線程的數(shù)據(jù)傳遞問題,那么只需要在創(chuàng)建子線程的時(shí)候,從主線程里將 MDC 里的 traceId 拿出來,傳遞給新建的子線程就可以了,就像這樣:
public class MDCDelegateRunnable implements Runnable{
private Runnable target;
private String traceId;
public MDCDelegateRunnable(Runnable target, String traceId) {
this.target = target;
this.traceId = traceId;
}
@Override
public void run() {
MDC.put("traceId",traceId);
target.run();
MDC.remove("traceId");
}
}
復(fù)制代碼然后再來一個(gè)委托模式的線程池,將 execute方法重寫。把線程池中原本的 Runnable 對象包裝為剛才的 MDCDelegateRunnable,在創(chuàng)建時(shí),將 traceId 通過構(gòu)造參數(shù)傳遞
public class MDCDelegateExecutorService extends AbstractExecutorService {
public MDCDelegateExecutorService(AbstractExecutorService target) {
this.target = target;
}
private AbstractExecutorService target;
@Override
public void shutdown() {
target.shutdown();
}
//...
@Override
public void execute(@NotNull Runnable command) {
target.execute(new MDCDelegateRunnable(command, MDC.get("traceId")));
}
}
復(fù)制代碼搞定,來測試一下:
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
MDC.put("traceId","111");
new MDCDelegateExecutorService((AbstractExecutorService) Executors.newFixedThreadPool(5)).execute(new Runnable() {
@Override
public void run() {
System.out.println("runnable: "+MDC.get("traceId"));
}
});
Future<String> future = new MDCDelegateExecutorService((AbstractExecutorService) Executors.newFixedThreadPool(5)).submit(new Callable<String>() {
@Override
public String call() throws Exception {
return MDC.get("traceId");
}
});
System.out.println("callable: "+future.get());
System.in.read();
}
//output
runnable: 111
callable: 111
復(fù)制代碼完美,本來麻煩的 traceId 傳遞問題,現(xiàn)在通過一個(gè)簡單的委托模式就解決了。不用修改調(diào)用方代碼,也沒有破壞線程池的代碼。
JDK 里的委托模式
還記得Executors#newSingleThreadExecutor這個(gè)單線程線程池的創(chuàng)建方法吧,那什么情況下需要單線程的線程池呢?
比如我只是需要一個(gè)異步并且獲取返回的操作,直接 new 線程 start 的話,獲取返回值又不太方便,如果通過線程池的 Callable/Runnable + Future 就方便了:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
// do sth...
return data;
}
});
String data = future.get();
executorService.shutdown();
復(fù)制代碼對于單線程異步的場景來說,甚至都不需要維護(hù)一個(gè)單例的線程池,每次 new/shutdown 也可以。可是我都單線程了,每次還要 shutdown 是不是有點(diǎn)不太方便,萬一哪里忘了 shutdown 了,那可不完蛋了……
JDK 的設(shè)計(jì)者也想到了這個(gè)問題,而且他們也已經(jīng)解決了這個(gè)問題。和上面的例子類似,利用一個(gè)簡單的委托模式,就可以完美解決這個(gè)問題:
public static ExecutorService newSingleThreadExecutor() {
//創(chuàng)建 FinalizableDelegatedExecutorService 委托類
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 委托類里,在 finalize 被委托的線程池對象的 shutdown方法,自動(dòng)關(guān)閉線程池
static class FinalizableDelegatedExecutorService
extends DelegatedExecutorService {
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}
protected void finalize() {
super.shutdown();
}
}
// 公共的抽象委托線程池……
static class DelegatedExecutorService extends AbstractExecutorService {
private final ExecutorService e;
DelegatedExecutorService(ExecutorService executor) { e = executor; }
public void execute(Runnable command) { e.execute(command); }
public void shutdown() { e.shutdown(); }
//...
}
復(fù)制代碼這樣一來,在使用 newSingleThreadExecutor的時(shí)候,甚至都不需要顯示 shutdown 了……
注意:雖然JDK 幫我們關(guān)了……但還是建議手動(dòng) shutdown,把 JDK 的這個(gè)機(jī)制當(dāng)做一個(gè)防呆設(shè)計(jì),萬一忘了 JDK 還能自動(dòng)關(guān)閉,避免泄露的問題
總結(jié)
結(jié)合上面兩個(gè)例子來看,一旦跳出業(yè)務(wù)代碼的范圍,應(yīng)用設(shè)計(jì)模式是不是變得很簡單?甚至都不需要硬往設(shè)計(jì)模式上套,遇到問題你自然會(huì)想到用設(shè)計(jì)模式來解決問題,而不是用設(shè)計(jì)模式在代碼里養(yǎng)蠱……
在純業(yè)務(wù)代碼中,適當(dāng)?shù)牟鸱郑3执a整潔可讀性強(qiáng)帶來的收益,遠(yuǎn)比套一堆設(shè)計(jì)模式要強(qiáng)
重復(fù)一遍:設(shè)計(jì)模式是用來解決問題,降低/轉(zhuǎn)移復(fù)雜度的,而不是增加復(fù)雜度
以上僅個(gè)人看法,如有不同意見歡迎評論區(qū)留言
作者:空無
鏈接:https://juejin.cn/post/6972372366131200036
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
