天天看點

設計模式之抽象工廠模式

在這裡我們暫時先不談論抽象工廠是什麼,因為如果直接上來就去描述、解釋什麼是抽象工廠,以及如何使用抽象工廠模式來設計代碼,這樣是無法很好的明白抽象工廠模式的概念以及它所帶來的好處或壞處的,隻會讓人下意識的隻去記住實作代碼,而不是設計模式的思想。講解其他模式也是一樣,如果一上來就是代碼+理論一頓灌,隻會讓人看得億臉懵逼或似懂非懂。這就好比給你一塊披薩告訴你很好吃,以及這塊披薩上用了哪些好食材,你隻管吃就可以了,那麼如果你沒有吃過難吃的披薩,可能就會以為披薩就應該是這個味道的。

是以我們先從有些糟糕的代碼入手,并且分析這些代碼哪些地方有問題,然後再演進成使用設計模式去重構代碼,這樣就能有一個明顯的對比,畢竟有對比才有傷害嘛2333。

下面我們來寫一些簡單的代碼,這些代碼用于對MySQL資料庫的表格資料進行通路:

1.User類,封裝User表的資料,假設隻有uid和uname兩個字段:

2.MysqlUser類,用于對mysql資料庫進行通路,這裡隻是簡單的進行模拟,并沒有實際的通路資料庫的代碼:

3.用戶端代碼如下:

從以上的用戶端代碼可以很明顯到看到一個問題,就是<code>MysqlUser mysqlUser=new MysqlUser();</code>這一句代碼使得mysqlUser 這個對象被寫死在了MysqlUser 上。如果需求變更,資料庫方面不用MySQL而改用Oracle了呢,那麼與之有關聯的代碼都得需要進行更改。

這是因為代碼上依賴了具體的實作類,導緻與 MysqlUser 耦合,如果熟悉多态或工廠模式的話,可能就已經想到可以用工廠模式來改造它了,通過工廠方法模式可以封裝 <code>new MysqlUser();</code> 所造成的變化,因為工廠方法模式可以定義一個用于建立對象的接口,讓子類決定執行個體化哪一個類。

使用工廠方法重構以上的代碼,代碼結構圖如下:

設計模式之抽象工廠模式

IUser接口,用于用戶端通路,解除與具體資料庫通路的耦合:

MysqlUser類,用于通路MySQL資料庫的User表:

OracleUser類,用于通路Oracle資料庫的User表:

IFactory接口,定義一個抽象的工廠接口,該工廠用于生産通路User表的對象:

MysqlFactory類,實作IFactory接口,用于生産 MysqlUser 的執行個體對象:

OracleFactory 類,實作IFactory接口,用于生産 OracleUser 的執行個體對象:

用戶端代碼如下:

以上我們使用工廠方法模式重構的之前的代碼,現在如果需求改變,要更換資料庫,隻需要把 <code>MysqlFactory();</code> 改為 <code>OracleFactory();</code> 就可以了,此時由于多态的特性,使得 IUser 接口的對象 userOperation 根本不知道是在通路哪個資料庫,卻可以在運作時很好的完成工作,這就是所謂的業務邏輯與資料通路的解耦。

但是,問題還沒有解決完,因為資料庫裡不可能隻有一個表吧,很有可能會有其他表,比如與使用者表相關的登入記錄表(Login表),此時該如何解決?

Login 類,封裝 Login 表的資料,假設隻有 id 和 date 兩個字段:

其實即便是資料庫中會有多個表,那也是屬于資料通路這一類的,屬于這一系列的,是以我們隻需要增加一些相關的類即可。

代碼結構圖如下:

設計模式之抽象工廠模式

ILogin接口,用于用戶端通路,解除與具體資料庫通路的耦合:

MysqlLogin類,用于通路MySQL資料庫的Login表:

OracleLogin 類,用于通路MySQL資料庫的Login表:

IFactory,定義一個抽象的工廠接口,該工廠用于生産通路User表以及Login表的對象:

MysqlFactory類,實作IFactory接口,用于生産 MysqlUser 以及 MysqlLogin 的執行個體對象:

OracleFactory 類,實作IFactory接口,用于生産 OracleUser 以及 OracleLogin 的執行個體對象:

運作結果:

從用戶端的代碼中,我們隻需要更改 <code>IFactory factory=new MysqlFactory();</code> 為 <code>IFactory factory=new OracleFactory();</code>,就實作了資料庫通路的切換。而且實際上我們這次代碼的重構已經使用到了抽象工廠模式,抽象工廠可能表面上看起來貌似與工廠方法模式沒什麼差別,其實不然,是以我之前才說抽象工廠模式是基于工廠方法模式的。

隻有一個User表的封裝類和User表的操作類時,我們隻用到了工廠方法模式,而且也隻需要使用到工廠方法模式。但是顯然現在我們的資料庫已經不止一個User表了,而 MySQL 和 Oracle 又是兩大不同的分類,是以解決這種涉及到多個産品系列的問題,就需要使用到專門解決這種問題的模式:抽象工廠模式。這時候再回過頭去看DP對抽象工廠模式的定義就不難了解了。

是以抽象工廠與工廠方法模式的差別在于:抽象工廠是可以生産多個産品的,例如 MysqlFactory 裡可以生産 MysqlUser 以及 MysqlLogin 兩個産品,而這兩個産品又是屬于一個系列的,因為它們都是屬于MySQL資料庫的表。而工廠方法模式則隻能生産一個産品,例如之前的 MysqlFactory 裡就隻可以生産一個 MysqlUser 産品。

示意圖:

設計模式之抽象工廠模式

抽象工廠模式(Abstract Factory)結構圖:

設計模式之抽象工廠模式

AbstractProductA 和 AbstractProductB是兩個抽象的産品,之是以為抽象,是因為他們都有可能有兩種或多種不同的實作,就剛才的例子來說就是 User 和 Login 表的不同資料庫的通路對象,而ProductA1、ProductA2和ProductB1、ProductB2 就是對兩個抽象産品的具體分類的實作,例如 ProductA1可以對比為 MysqlUser ,而 ProductB1 則可以對比為 MysqlLogin。

IFactory 則是一個抽象的工廠接口,它裡面應該包含所有的産品建立的抽象方法。而ConcreteFactory 1 和 ConcreteFactory 2 就是具體的工廠了。就像MysqlFactory和OracleFactory一樣。

我們通常是在運作時再建立一個 ConcreteFactory 類的執行個體對象,這個具體的工廠再建立具有特定實作的産品對象,也就是說,為建立不同的産品對象,用戶端應該使用不同的具體工廠。

優點:

抽象工廠模式最大的好處是易于交換産品系列,由于具體工廠類,例如 <code>IFactory factory=new OracleFactory();</code> 在一個應用中隻需要在初始化的時候出現一次,這就使得改變一個應用的具體工廠變得非常容易,它隻需要改變具體工廠即可使用不同的産品配置。不管是任何人的設計都無法去完全防止需求的更改,或者項目的維護,那麼我們的理想便是讓改動變得最小、最容易,例如我現在要更改以上代碼的資料庫通路時,隻需要更改具體的工廠即可。

抽象工廠模式的另一個好處就是它讓具體的建立執行個體過程與用戶端分離,用戶端是通過它們的抽象接口操作執行個體,産品實作類的具體類名也被具體的工廠實作類分離,不會出現在用戶端代碼中。就像我們上面的例子,用戶端隻認識IUser和ILogin,至于它是MySQl裡的表還是Oracle裡的表就不知道了。

缺點:

但是任何的設計模式都有自身的缺陷都不是完美的,都有不适用的時候,例如抽象工廠模式雖然可以很友善的幫我們切換兩個不同的資料庫通路的代碼。但是如果我們的需求來自于增加功能,例如我們還需要加多一個會員資料表 MemberData,那麼我們就得先在以上代碼的基礎上,增加三個類:IMemberData,MysqlMemberData,OracleMemberData,還需要修改IFactory、MysqlFactory以及OracleFactory才可以完全實作。增加類還好說,畢竟我們是對擴充開放的,但是卻要修改三個類,就有點糟糕了。

而且還有一個問題就是用戶端程式類在實際的開發中,肯定不止一個,很多地方都會需要使用 IUser 或 ILogin ,而這樣的設計,其實在每一個類的開始都需要寫上 <code>IFactory factory=new OracleFactory();</code> 這樣的代碼,如果我有一百個通路 User 或 Login 表的類,那不就得改一百個類?很多人都喜歡說程式設計是門藝術,但也的确如此,對于藝術我們應該去追求美感,是以這樣大批量的代碼更改,顯然是非常醜陋的做法。

我們要有不向醜陋代碼低頭的精神,是以我們再來改進一下這些代碼。實際上,在這種情況下與其用那麼多的工廠類,不如直接用一個簡單工廠來實作,我們将IFactory、MySQLFactory以及OracleFactory三個工廠類都抛棄掉,取而代之的是一個簡單工廠類EasyFactory,代碼結構圖如下:

設計模式之抽象工廠模式

EasyFactory類,簡單工廠:

由于事先在簡單工廠類裡設定好了db的值,是以簡單工廠的方法都不需要由用戶端來輸入參數,這樣在用戶端就隻需要使用 <code>EasyFactory.createUser();</code> 和 <code>EasyFactory.createLogin();</code> 方法來獲得具體的資料庫通路類的執行個體,用戶端代碼上沒有出現任何一個 MySQL 或 Oracle 的字樣,達到了解耦的目的,用戶端已經不再受改動資料庫通路的影響了。

但是我們都知道簡單工廠也存在一個缺陷,例如我要增加一個 SQL Server 資料庫的通路類,那麼本來抽象工廠模式隻需要增加一個 SQLServerFactory 工廠類就可以了,而簡單工廠則需要在每個方法的switch中增加case條件了。

是以我們要考慮的是可以不可以不在代碼裡寫明條件分支語句,而是根據字元串db的值來去某個地方找需要執行個體化的那個類,這樣的話,我們就可以和switch語句say goodbye了。

而在Java中有一種技術可以做到這一點,那就是反射機制,有了反射機制我們隻需要使用字元串就可以擷取某個類的執行個體,例如:

這種反射的寫法和 <code>IUser result = new MysqlUser();</code> 一樣可以拿到 MysqlUser 類的執行個體。而它們的差別在于,反射可以通過字元串來擷取 MysqlUser 類的執行個體,使用new關鍵字則不行,編譯後就無法改變了。我們都知道字元串是可以存儲在變量中的,可以通過變量來處理字元串,也就是說可以根據需求來進行動态更換。

以上我們使用簡單工廠模式設計的代碼中,是用一個字元串類型的db變量來存儲資料庫名稱的,是以變量的值到底是 MySQL 還是 Oracle ,完全可以由事先設定的那個db變量來決定,而我們又可以通過反射來去擷取執行個體,這樣就可以去除switch語句了。

下面我們就來使用反射機制改造一下之前的簡單工廠類:

用戶端代碼如下,除了抛多一個異常,其他代碼都不需要改動:

現在如果需要增加 SQL Server資料庫的通路功能,那麼增加相關的類是不可避免的,這點無論如何都無法解決,但是這叫擴充,開-閉原則告訴我們對于擴充要開放,但對于修改就要盡量關閉。就目前而言,如果要切換資料庫需要更改db變量的值即可,也就是說隻需要改動一下代碼的注釋就可以了:

那麼如果我還需要增加會員資料表 MemberData 的話,隻需要增加三個與 MemberData 相關的類,再修改一下 EasyFactory 類,在裡面增加一個建立執行個體的方法即可。

如果項目比較大的話,就可以直接使用工廠方法模式了,那樣隻需要增加新的類即可,不需要對原有的代碼進行改動,靈活性比簡單工廠更強。是以在實際的項目中,我們應該根據情況來選擇使用哪種設計模式,不然使用哪種模式也好,都有可能會導緻設計過度或不足。

雖然我們已經使用了反射機制改進了代碼,但是總感覺還是有點缺憾,因為在更換資料庫通路時,我們還是需要去打開代碼更改db變量的值,然後再重新進行編譯。是以如果能夠不打開代碼修改程式,就能達到更改變量的效果,那才是完全符合開-閉原則。

這也不是沒辦法解決的,例如典型的配置檔案就可以解決這種問題,我們可以在外部檔案寫好這些資訊,讓程式去讀檔案中配置的資訊來給變量指派就可以了,以後修改也隻需要修改配置檔案,而不需要去打開代碼來修改,修改之後還得重新編譯那麼麻煩了。

在工程的根目錄下建立一個.json的配置檔案,内容如下:

由于用的是json來作為配置檔案的格式,是以我這裡使用了解析json的包:

設計模式之抽象工廠模式

EasyFactory 類代碼如下:

用戶端代碼無需改動,運作結果如下:

小結:經過了一系列的改進代碼,這下對于目前的需求來說基本算得上是滿分了,我們最後應用了反射機制+配置檔案+簡單工廠模式解決了資料庫通路時的可維護、可擴充的問題。而且從這個角度上講,幾乎所有在用簡單工廠的地方,都可以考慮利用反射機制來去除 switch case 或 if else 等條件分支語句,進一步解除分支判斷帶來的耦合,是以在後面我才沒有用工廠方法模式而是用簡單工廠方法來去改進之前的抽象工廠。

本文轉自 ZeroOne01 51CTO部落格,原文連結:http://blog.51cto.com/zero01/2070033,如需轉載請自行聯系原作者