一文分清Java開發中容易混淆的四大設計模式
原創作者 | 蔡柱梁
可能很多人認為設計模式只有面試時用到,這也不能算錯吧。但是如果僅僅只是面試時背背八股文,在實際工作中遇到了應該使用,卻不知道要用,那么你的代碼能有多好也是自欺欺人的了。那么什么時候應該使用設計模式呢?
換個角度說吧,大家覺得設計模式是怎么出來的?其實就是大牛們寫代碼多了,覺得一些高度重復或相似的地方,可以做到更好的“高內聚”,“低耦合”。他們不斷改進,然后對他們的這些設計思路進行總結,最后得到的就是我們現在的設計模式。本文就給大家介紹幾個常用的設計模式。
1.工廠模式
為什么先說工廠模式呢?因為“工廠”這個概念在我們代碼中到處都體現著,很多設計模式也離不開它,以它作為基礎演進,所以先要學習它。什么是“工廠”?就像我們生活中的工廠是一樣的,就是生產某些“物品”的地方。
在 Java 代碼中,我們用各式各樣的對象做各種事情,而這個過程中,我們往往是不關心創建過程的,僅僅關注它有那些方法可使用,提供了什么功能。這時,我們可以使用工廠模式進行解耦——創建的行為放在工廠里,而使用的人專注于使用工廠產生的工具。在下面的模板方法、策略模式、適配器模式中,都能看到工廠模式的身影。
我們所說的工廠模式一般有兩種:
- 工廠方法
- 抽象工廠
(1)工廠方法
工廠方法模式是一種創建型設計模式, 其在父類中提供一個創建對象的方法, 允許子類決定實例化對象的類型。
工廠方法的具體實現思路是:
- 制定一個創建對象 A 的接口工廠
- 這個工廠的實現類構建 A 的子類,如:A1、A2、A3……
通過這種方式實現對象和對象的創建的分離,可能覺得很雞肋吧?下面通過場景對比說明它的好處。
用傳統做法與使用了工廠方法的場景對比:
- 傳統寫代碼
- 我需要用到某個類,比如 A1,我就 new A1 出來,然后進行業務操作。有一天產品告訴我這段邏輯需要增加一個 A2 的業務操作邏輯,我就得通過條件判斷增加邏輯。可是 A1 和 A2 在業務抽象上是一致的,僅僅是實現細節不同(舉個例子:好比運輸,我用貨車運輸是運輸,我用火車運輸也是運輸,也就是說運輸是目的,我的實現方式可以多樣化)。這時,通過 if/else 或 switch 來寫就不符合開閉原則了。
- 用了工廠方法寫代碼
- 我代碼上一開始就寫著是運輸工具,用這個運輸工具運輸(注意這里是抽象概念運輸工具而已)。這樣,我就可以根據業務計算得到的條件(如:公路/鐵路/海運/空運)丟給工廠,工廠給我返回具體的運輸工具就行(反正子類能強轉成父類)。
使用了工廠方法后,我的業務代碼不需要關注具體的運輸工具是什么,然后再去看它怎么運輸,后續產品加再多運輸工具,transport()的這段代碼都不會被干擾,符合了開閉原則。
偽代碼如下:
public interface TransportToolFactory {
TransportTool createTool();
}
public class TruckTransportToolFactory implements TransportToolFactory {
@Override
public TransportTool createTool() {
...
}
}
public class BoatTransportToolFactory implements TransportToolFactory {
@Override
public TransportTool createTool() {
...
}
}
public class Transport {
private TransportToolFactory factory;
public Transport(int way) {
if (way == 0) {
factory = new TruckTransportToolFactory();
}
...
}
public void transport() {
TransportTool tool = factory.createTool();
// 繼續業務處理
}
}
簡單說下“簡單工廠”,偽代碼如下:
public void transport() {
int way = getWay();// 經過計算也好,前端傳過來也好,反正得到了具體的運輸方式
TransportTool tool = new TransportToolFactory(way).createTool();
// 繼續業務處理
}
public TransportTool createTool() {
if (way == 0) {
// 貨車
}
...
}
不過簡單工廠的缺點很明顯:
沒有做到單一職責,從上面的例子不難看出,汽車、輪船、飛機、大炮都包了,如果業務足夠復雜,這個工廠類真的是誰維護誰知道!
(2)抽象工廠
抽象工廠模式是一種創建型設計模式, 它能創建一系列相關的對象, 而無需指定其具體類。
在我看來,JDBC 對抽象工廠模式的應用就十分經典。DB 有很多種,但是在不同的公司選擇可能都不太一樣,有些是 MySQL,有些是 Oracle,甚至有些是 SQL Sever 等等。但是對于我們開發而言,這些都是 DB,如果它們的連接,提交事務,回滾事務等細節都需要我們注意的話(不同 DB 的具體實現處理會有差異),這顯然是很麻煩的,而且我們也不關心。我們要的只是使用 Connection 創建 Session,Session 開啟事務等等。
如果有一個類可以將這一系列共性的行為都提取出來(如連接,事務處理等),我們只要使用這個抽象類和它提供的方法就好了。事實上,JDBC 也的確是這么做的,我們在配置好具體的數據庫配置后,在代碼上只要用接口 Factory 創建連接、會話,開啟事務……
首先,連接是個對象,會話也是對象,事務也是,創建這些對象的方法都抽象到一個工廠里面,而這個工廠本身也只是一個接口定義,這就是所謂的抽象工廠;如果這時我使用的是MySQL,那么剛剛羅列的那些對象都是MySQL定制化的一系列相關對象,這就是所謂的“能創建一系列相關的對象”。
2.模板方法模式
模板方法模式是一種行為設計模式,它在超類中定義了一個算法的框架, 允許子類在不修改結構的情況下重寫算法的特定步驟。
模板方法的核心在于抽象上行為性質一樣,實際行為上有差別。
舉個例子:
我們產品常常要收集各式各樣的數據來分析用戶行為。有時他們為了效率會給開發一堆電子文檔(如 CSV、DOC、TXT等等,這些文檔記錄著類似的數據,但是數據結構肯定是不同的),讓開發按照他們要求開發個系統功能可以導入,按他們的要求統計這些數據。
對于開發而言,代碼是差不多的,都要導入文件,解析文件,邏輯計算后入庫。偏偏我們導入文件后,解析文件代碼不同,邏輯計算有時也會有差異,但是對于最后一步落庫卻大概率是一樣的。對于這種類型的業務場景,我們可以定個類去規定好這些流程,上游調用時就是調用我這個類的子類,子類會根據自己的業務場景重寫自己需要的流程節點的邏輯。
3.策略模式
策略模式是一種行為設計模式, 它能讓你定義一系列算法, 并將每種算法分別放入獨立的類中, 以使算法的對象能夠相互替換。
舉例子說明:
我們接入一個審批流組件,我們自己后臺也要留一份提審的記錄(方便查詢和回溯),現在我們希望我們做的這個功能通用性要強一些,也就是可以做到讓其他功能想加入這個審批流程就加入,如:功能鑒權的授權,工作流配置等等。
那么一開始審批時,一定是只有提審數據,而我們的鑒權授權或者工作流配置肯定是沒生成到對應表的,只有審批通過后才會真的授權或者生成配置。這時問題來了,當工作流組件回調我們,難道我們每加入一個就 copy 上一個功能的回調代碼,刪掉修改審批狀態后的代碼,改改就好了嗎?這里得冗余多少代碼,哪怕你修改審批流的代碼抽取成一個方法,你也會發現每個回調方法里都有你那個方法。
具體偽代碼如下:
public class CallBack {
public void callback1(Param param) {
// 查詢審批記錄的合法性
// 修改審批記錄
// 處理業務邏輯1
}
public void callback1(Param param) {
// 查詢審批記錄的合法性
// 修改審批記錄
// 處理業務邏輯2
}
......
}
這種場景我們可以使用策略模式優化,我們將處理業務邏輯當成個算法對象抽離出來,不同業務場景的回調業務處理器實現這個抽離接口,用策略自動分配對應的處理器即可。
偽代碼如下:
public class CallBack {
private Strategy strategy = new Strategy();
public void callback(Param param) {
// 查詢審批記錄的合法性
// 修改審批記錄
// 處理業務邏輯
strategy.getHandle(param.getServiceName()).invokeHandle();
}
}
public class Strategy {
private Map<String, Iservice> map;
static {
map = new HashMap<String, Iservice>();
map.put("Service1", new Service1());
map.put("Service2", new Service2());
......
}
public Iservice getHandle(String serviceName) {
return map.get(serviceName);
}
}
public class Service1 implements Iservice {
@Override
public void invokeHandle() {
...
}
}
public class Service2 implements Iservice {
@Override
public void invokeHandle() {
...
}
}
......
4.適配器模式
適配器模式是一種結構型設計模式, 它能使接口不兼容的對象能夠相互合作。
說到適配器,我想大家很快就想到了一個場景:
我們家庭的標準電壓是220V左右(實際會有點誤差),我們大家電自然需要這么高的電壓才能工作,但是我們小家電呢?如:手機充電,電腦等等。這些小家電往往都會有個“中介”——適配器去幫他們將標準電壓轉化成他們的適用電壓。
其實我們的適配模式也是一樣的。這里我們來看下 Spring 的實戰使用,上源碼:
/**
* Extended variant of the standard {@link ApplicationListener} interface,
* exposing further metadata such as the supported event and source type.
*
* <p>As of Spring Framework 4.2, this interface supersedes the Class-based
* {@link SmartApplicationListener} with full handling of generic event types.
*
* @author Stephane Nicoll
* @since 4.2
* @see SmartApplicationListener
* @see GenericApplicationListenerAdapter
*/
public interface GenericApplicationListener extends ApplicationListener<ApplicationEvent>, Ordered {
...
在 4.2 版本之前,Spring 監聽觸發事件的監聽器使用的是 ApplicationListener,經過這么多迭代后,它想增強下該功能,所以又定義了一個 GenericApplicationListener。但是這里有個問題,以前實現 ApplicationListener 的那些子類也還是要兼容的!!!全部重寫,那很累人;不兼容,作為高熱度的開源框架,這是不能接受的。這時,Spring 的作者就采用了適配模式,具體應用代碼如下:
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
// 我們都知道 spring 的廣播事件都是是用了這個接口,我們看下 spring 是怎么做兼容的
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
// 重點在這 getApplicationListeners(event, type),看看他們是怎么 get 這個 list 的
// getApplicationListeners 是父類 AbstractApplicationEventMulticaster 的方法
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
}
AbstractApplicationEventMulticaster#getApplicationListeners 里面做了大量的性能優化,不是本文的重點,所以這里跳過了。大家只要知道它第一次拿的地方是:
AbstractApplicationEventMulticaster#retrieveApplicationListeners 。
這就夠了,而這個方法里面給 list 添加元素的方法是:
AbstractApplicationEventMulticaster#supportsEvent(ApplicationListener, ResolvableType, Class),這才是我們要看的代碼。
protected boolean supportsEvent(
ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) {
// 這里先看下這個 listener 是不是 GenericApplicationListener 的子類
// 不是就轉化成 GenericApplicationListener,這樣以前 ApplicationListener 的子類就能被兼容了
GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}
5.總結
希望上面的幾個設計模式的應用例子能給大家一點啟發,能在自己工作中找到共同點去嘗試應用。不過,也不要濫用設計模式,因為一些剛起步的公司,業務方向也還不穩定,很難去抽取共同的抽象部分又或者由于業務太簡單了,造成了過度設計,這些都是不可取的。
作者介紹
蔡柱梁,51CTO社區編輯,從事Java后端開發8年,做過傳統項目廣電BOSS系統,后投身互聯網電商,負責過訂單,TMS,中間件等。