剛才在園子里閑逛, 看見這么一篇博文在首頁:
有感於四人幫那套書對廣大的編程人員誤導之嚴重, 決定寫一個小系列,專門說這個. 此文權當第一篇, 為什么工廠模式是不必要的?
做一件事之前,要想的不是怎么做,而是為什么要做,工廠到底要解決什么問題?
其實歸根結底就是為了不必在創建時顯式指定要創建的類型,因為幾個工廠其實本質是一樣的, 抽象工廠是完整的, 普通工廠是化簡了, 簡單工廠方法又再化簡一次. 如果連抽象工廠這個最復雜的都是沒必要存在的, 那么另外兩個就更沒存在的意義了.所以這里就對着抽象工廠來開刀.
按照四人幫最早原文說的抽象工廠存在的意義是為了: Create related objects without specifying concrete class at point of creation.
那我們來看看這件事到底需要不需要引入"工廠". 典型的工廠模式的用例(不要再用工廠生產自行車和汽車做例子了....這是在偷概念, 讓factory同時扮演業務對象和程序引入的不必要的噪音,這掩蓋了問題) 做一個跨平台的UI庫, 支持Windows,Mac,Linux等很多系統, 有按鈕,滾動條等很多不同控件. 這是典型的抽象工廠案例,按照四人幫的思路, 應該有3個工廠分別給這3個平台, 如果有第4個平台就再加一個工廠, 然后還要一個抽象的工廠的父類, 用來給調用方使用(這樣調用方就不需要關心到底現在在哪個系統), 然后每個平台有自己的控件的基類, 比如WindowsControl, LinuxCintrol....這些控件基類又是從一個通用的控件類繼承, 然后不同的滾動條,按鈕什么的再從各自平台的Control繼承... ..這樣就形成龐大的一大堆類和對象...
對於同一個控件, 比如button,需要一堆各種平台的工廠, 還有一堆各種平台各自的按鈕. 各種平台各自的按鈕是客觀上業務需要(畢竟Windows和Linux顯然有不同底層接口..這種分別是客觀存在的), 但是那些Factory真的有必要存在么?
為什么我們不能讓控件類自己來管自己的創建過程呢?
也許有人說, 我們剛才要的不就是要創建的時候不需要知道創建的是什么嗎, 如果用控件類自己來直接new,那就必須要確定要new的是什么類. 而抽象類是無法new出來對象的.
等等, 為什么只能new,不能像工廠那樣 create ?
比如 Button 類(這是抽象類)
1 public abstract class Button 2 { 3 public static Button CreateButton(......) 4 { 5 //這里可以跟工廠里一樣,根據參數或者配置,new出不同的子類. 6 } 7 } 8 9 public class WindowsButton : Button {...} 10 public class LinuxButton : Button {...} 11 12 //客戶端調用跟使用工廠沒有區別...對應於抽象工廠的每個子工廠,都這么做
最終結果就是少了一大堆工廠類, 但是目的一樣達成, 對維護沒有什么影響,因為你增加一種控件要干的事情是一樣的,只是把改工廠變成改父類了,而且改的一樣是一處地方而已.
對於新添加一個平台, 可以比抽象工廠少加一個類.
類更少,代碼更少,出bug的機會也就更少, 而且代碼更加內聚, 本來問題里面就沒工廠什么事情. 代碼應該只表達需要表達的邏輯, 任何額外的附加都是應該避免的(但是有時候因為編程語言不夠強大而無法避免).
事實上, "在創建對象時不需要指定具體創建的類",這個需求如果是很罕見的話, 那么不應該作為一種設計模式,因為模式應該是常見的可復用的模板和方法. 反過來如果這是一件常見的事情, 那么就應該是語言的 編譯器 要做的事情, 如果一門語言, 要做這件事需求求助於 像 抽象工廠這樣復雜的結構,(這個結構是實現技術疊加給它的,並不是問題本身有的) 那只能說明這門語言的設計者語法設計得有問題,不夠完善,以至於程序員遇到一個"常見"的問題的時候, 需要 自己寫一大堆類和代碼來 完成本該編譯器完成的工作(也就是程序員當人肉編譯器了....)
C#的語法設計上允許一個抽象類有靜態的方法,這就讓factory不必要存在了.
PS: 如果有人要說,萬一父類是系統庫里面的,我改不了呢? 那不就需要個工廠了嗎? 其實依然不需要工廠, 不過,這就涉及到另外一個設計模式遇到的問題, 你需要給一個類添加方法, 而這個類的代碼你無法修改. 四人幫的書里用的方法是裝飾器. 這依然可以是個不必要的的模式..本文后面會簡單說一些不需要的模式.
事實上, 四人幫寫書之前, 已經有 設計模式 這個詞, 當時的含義跟現在完全不同. 是很泛化的, 比如匯編語言不支持函數調用語法, 於是形成了模式, 就是你要進入一個子程序, 就把你要傳的數據(參數) 按順序 push 入棧, 然后再進入子程序, 子程序里如果還有子程序, 再里面再push, 然后每退出一層就pop出來... 這就是最早的模式, 但是當像C這樣的支持函數調用的語言里, 這個模式是不需要的. 因為入棧出棧的操作, 編譯器幫你生成了.
其實所有的所謂的模式, 都是因為語言設計的不完善, 才使得程序員需要使用 "模式" 來解決問題, (另外一個角度,因為語法完美的語言可遇不可求,所以設計模式一直流行着...). 語言語法越強大,模式越不需要.
再舉個例子,單件模式, 為了產生一個全局的訪問點. 其實產生就產生吧,為什么需要搞個模式呢? 因為C++不能保證產生過程是線程安全的, 如果兩個線程同時進入, 有可能會產生出兩個對象,這樣"全局訪問點"的目的就落空了, 因此需要使用加鎖的機制, 加鎖影響性能, 因為其實創建只有第一次的時候需要發生以后都是讀取訪問應該是不需要鎖的, 於是, 在外面加了個if 來跳過鎖,但是又因為C++里這兩個沒法保證原子性,於是又需要使用 " 雙檢鎖" 策略...於是一個簡單的new操作就變成了 3層嵌套...那么有必要把這個邏輯單獨抽出來, 於是單件模式就誕生了. 在C#里這一系列問題都不復存在, 雖然可以模仿C++寫出一樣的代碼,但是你寫來干嘛? 如果原始目的是要全局訪問點, 靜態類已經完全保證了全局訪問點並絕不會創建出第2個....
還有剛才上面說的裝飾器模式, 裝飾器的目的是什么? C#有了 擴展方法, 還要裝飾器干什么?
最后,C#語法經過這些年微軟式的快速更新換代,已經很復雜很強大了, 但是它不是完美的, 它無法完整地解決上面說的那幾個...比如擴展方法不允許實現接口(也許以后哪個版本可以...), 所以上面那種情況沒法通過擴展方法去解決工廠的問題.... 靜態類因為繼承的問題也帶來一些制約...
不過,確實有語言完整地解決這些問題, 比如 Clojure語言的 "協議" 語法(跟C#的接口類似, 但是不需要在類定義的時候指定這個類實現什么協議..., 可以在以后任何時候需要的話給一個類或者對象附加一個協議並實現上去....並且可以不破壞封裝性, 而且只在特定模塊可見....用C#的話說就是你可以給String加上一個Interface並實現那些方法,但是這個實現只對某個namespace 可見, 所以你不怕會影響微軟的那些使用String類的方法,因為對他們來說String並沒有這些方法和接口...他們在跟你不同的namespace里 )完整解決了裝飾器模式的問題.
至於上面那個工廠的問題, 很多動態語言比如javascript或者lisp等,因為動態的特性, 可以完全避免, 比如某些動態語言可以直接 make(抽象基類,參數1,參數2) 來構建出子類對象而不需要指定子類, 那么工廠相關方法就真的完全不需要了...
在Dylan或者Lisp里, 四人幫23種設計模式里面完全不需要的模式達到有16種之多...所以設計模式其實應該跟語言相關的, 不是說你把C++的東西改下語法就是java的就是C#的,很多用其他語言寫的設計模式的書,就是照把四人幫的內容照着搬一次然后代碼改成XXX語言, 這么些年下去,讓很多人迷失在設計模式里而把簡單的東西復雜化.
寫了很多年代碼以后,
11年前的一天, 我第一次知道有設計模式這種東西的時候,覺得太無聊了,把東西弄復雜了.
9年前在一個大型項目中因為需求變更代碼一團糟的時候,我才終於知道設計模式是干什么的,有什么用,於是開始研究和迷戀設計模式.
到了大約5年前,我已經可以不管什么模式不模式, 隨便寫一段代碼就帶着這種或那種模式的變體,組合各種模式來創建出所謂 柔軟的代碼. 手中無劍而心中有劍.
但是后來在stackoverflow我認識了一個網友,一個老外,他說設計模式是語言的BUG, 這些BUG需要人肉修復,於是就有了模式. 我仔細思考了很久, 越來越發現他說的有道理. 當我因為工作需要而接觸了LISP系語言以后, 發現那個世界里沒人把那23種當事情, 而解決那些問題的代碼都是又短又漂亮而優雅.
這個文章是突然看見那個上面鏈接的文章而有感而發,沒打草稿,一氣敲出,所以可能有手誤的地方,.... 而且我此文是針對的"設計模式" 這個東西,不是針對連接所指的文章,那個文章寫的還是挺細致的.......
剛才發現我上篇博客還是在8年前,多年未寫,文字有些混亂, 歡迎有疑惑或異議者留言探討.