【使用工廠模式的好處】-講解常見的工廠模式
很多人都會糾結於“既然都有了構造函數,何必再折騰那麽多事情呢”。為了解答這個問題,先解釋下構造函數是幹什麽用的。
先用最早出現的C,創建資源差不多要這麽幹:
some_struct * p = (some_struct*)malloc(sizeof(some_struct));init_some_struct(p);do_something(p);
即先分配內存,再做類型轉換,再初始化,然後使用。而在OOP的時代,創建一個對象是很頻繁的事情。同時,一個沒初始化的數據結構是無法使用的。因此,構造函數被發明出來,將分配內存+初始化合並到了一起。如C++的語法是:
SomeClz *p = new SomeClz();do_something(p);// orp.do_something_else();
java也沿用了這個設計。
但是,整個構造函數完成的工作從更高層的代碼設計角度還是太過於初級。因此複雜的創建邏輯還是需要寫代碼來控製。所以還是需要:
SomeClz * createSomeClz(...) {// 做一些邏輯SomeClz *p = new SomeClz(); // 或者複用已經有的對象// 再做一些額外的初始化return p;}
這就是Factory的雛形。
Factroy要解決的問題是:希望能夠創建一個對象,但創建過程比較複雜,希望對外隱藏這些細節。
請特別留意“創建過程比較複雜“這個條件。如果不複雜,用構造函數就夠了。比如你想用一個HashMap時也要搞一個factory,這就很中2了。
好,那什麽是“複雜的創建過程呢“?舉幾個例子:
例子1: 創建對象可能是一個pool裏的,不是每次都憑空創建一個新的。而pool的大小等參數可以用另外的邏輯去控製。比如連接池對象,線程池對象就是個很好的例子。
例子2:對象代碼的作者希望隱藏對象真實的的類型,而構造函數一定要真實的類名才能用。比如作者提供了
abstract class Foo {//...}
而真實的實現類是
public class FooImplV1 extends Foo {// ...}
但他不希望你知道FooImplV1的存在(沒準下次就改成V2了),隻希望你知道Foo,所以他必須提供某種類似於這樣的方式讓你用:
Foo foo = FooCreator.create();// do something with foo ...
例子3:對象創建時會有很多參數來決定如何創建出這個對象。比如你有一個數據寫在文件裏,可能是xml也可能是json。這個文件的數據可以變成一個對象,大概就可以搞成。
Foo foo = FooCreator.fromFile("/path/to/the/data-file.ext");
再比如這個文件是描述一個可以顯示在瀏覽器的UI的基礎數據。而不同瀏覽器可以正確顯示的需要的數據不太一樣。這個“不一樣”可以表達為:
Foo foo = FooCreator.fromFile("/path/to/the/data-file.ext", BrowserType.CHROME);
這裏第二個參數"BrowserType"是一個枚舉,表示如何去生成指定要求的對象。所以這個fromFile內部可能是:
public Foo fromFile(String path, BrowserType type) {byte[] bytes = Files.load(path);switch (type) {case CHROME: return new FooChromeImpl(bytes);case IE8: return new FooIE8V1Impl(bytes);// ...}}
當然,實際場景可能會複雜得多,會有大量的配置參數。
Foo foo = FooCreator.fromFile("....", param1, param2, param3, ...);
如果需要,可以幫params弄成一個Config對象。而如果這個Config對象也很複雜,也許還得給Config弄個Factory。如果Factory本身的創建也挺複雜呢?嗯,弄個Factory的Factory。
例子4:簡化一些常規的創建過程。上麵可以看到根據配置去創建一個對象也很複雜。但可能95%的情況我們就創建某個特定類型的對象。這時可以弄個函數直接省略那些配置過程。純粹就是為了方便。
Foo foo = FooCreator.chromeFromFile("/path/to/the/date-file.ext");
現實當中,比如Java的線程池的相關創建api(如
Executors.newFixedThreadPool等)就是這麽幹的。
例子5:創建一個對象有複雜的依賴關係,比如Foo對象的創建依賴A,A又依賴B,B又依賴C……。於是創建過程是一組對象的的創建和注入。手寫太麻煩了。所以要把創建過程本身做很好地維護。對,Spring IoC就是這麽幹的。
例子6:你知道怎麽創建一個對象,但是無法把控創建的時機。你需要把“如何創建”的代碼塞給“負責什麽時候創建”的代碼。後者在適當的時機,就回調創建的函數。
在支持用函數傳參的語言,比如js,go等,直接塞創建函數就行了。對於名詞王國java,就得搞個XXXXFactory的類再去傳。Spring IoC 也利用了這個機製,可以了解下FactoryBean
例子7:避免在構造函數中拋出異常。"構造函數裏不要拋出異常"這條原則很多人都知道。不在這裏展開討論。但問題是,業務要求必須在這裏拋一個異常怎麽辦?就像上麵的Foo要求從文件讀出來數據並創建對象。但如果文件不存在或者磁盤有問題讀不出來都會拋異常。因此用FooCreator.fromFile這個工廠來搞定異常這件事。
其實還有很多例子,就不繼續擴展了。要點是,當你有任何複雜的的創建對象過程時,你都需要寫一個某種createXXXX的函數幫你實現。再拓展一下範圍,哪怕創建的不是對象,而是任何資源,也都得這麽幹。一句話:
不管你用什麽語言,創建什麽資源。當你開始為“創建”本身寫代碼的時候,就是在使用“工廠模式”了。
具體形式可以根據當時的場景去調整,不管你用的是靜態函數,抽象類還是模版等,那都是細節。不同語言的支持也不太一樣。比如Java這方麵就略微土一些,函數不是一等公民限製了表達力。所以你會看到各種XXXXFactory,AbstractXXXXFactory的類。
kotlin提倡用靜態工廠方法解決一部分問題,即給一個class的companion object做一個表示工廠的函數。在Effective Koltin第一條就是這個。
interface ImageReader {fun read(file: File): Bitmapcompanion object {// 提供靜態工廠方法fun newImageReader(format: String) = when (format) {"jpg" -> JpegReader()"gif" -> GifReader()else -> throw IllegalStateException("Unknown format")}}}// 使用靜態工廠方法val reader = ImageReader.newImageReader("jpg")Bitmap bitmap = reader.read(someFile)
而對於go,一般用一個函數去創建一個初始化好的對象(或者叫struct?)。go的想法很簡單:反正你總是要寫一個函數,就寫函數吧,不要搞出那麽多幺蛾子概念。
type SomeStruct struct {// ...}func NewSomeStruct() *SomeStruct {s := SomeStruct{...}// 做一些初始化return &s}
最後特別提醒下初學者,我很理解你們剛學到了一招馬上就想試試的心情,但如果是上生產,請總是使用可以滿足需求的最簡單的方案。不要為了工廠模式而工廠模式。搞工廠這麽一套(或者任何其他模式)都是有成本的。開閉原則是沒錯,但隻應該在合適的時候使用。更麻煩的是假如你一開始搞錯了,做出來的工廠的接口抽象後來發現是不符合需求變更,改起來還不如一開始沒有做工廠,直接new。越簡單的代碼越容易改,哪怕看起來會有些體力勞動,但不費神。當然,這也不是說盡量不要用模式。這完全取決於你對需求的理解。所以多花時間理解需求和業務,然後問自己“這裏可能會變得很複雜嗎?這裏未來3個月多大可能需要擴展?“
同時也不要照著《設計模式》去寫代碼。你可以將《設計模式》理解為是一本字典。它的內容是沒錯,但一般隻用來做參考。對於一個模式要不要用,怎麽用,要看場景。正常寫文章的人,除非是學生,沒人會在寫文章的時候抱著本字典去寫,對吧。
本文到此結束,希望對大家有所幫助呢。