Clojure 設(shè)計(jì)模式 Design Patterns
譯自 Clojure Design Patterns
Author: Mykhailo Kozik
第十五集:單例
Feverro O'Neal 抱怨我們的 UI 樣式太多了。
把每個(gè)應(yīng)用的 UI 配置都統(tǒng)一成一份。
Pedro: 等下,我看這里要求每個(gè)用戶都可以保存 UI 樣式啊。
Eve: 可能需求有變吧。
Pedro: 好吧,那我們應(yīng)該使用 單例 (Singleton) 保存配置,然后在需要用到的地方調(diào)用。
public final class UIConfiguration {
public static final UIConfiguration INSTANCE = new UIConfiguration("ui.config");
private String backgroundStyle;
private String fontStyle;
/* other UI properties */
private UIConfiguration(String configFile) {
loadConfig(configFile);
}
private static void loadConfig(String file) {
// process file and fill UI properties
INSTANCE.backgroundStyle = "black";
INSTANCE.fontStyle = "Arial";
}
public String getBackgroundStyle() {
return backgroundStyle;
}
public String getFontStyle() {
return fontStyle;
}
}
Pedro: 這樣就可以在不同的 UI 之間共享配置了。
Eve: 沒錯(cuò)是沒錯(cuò),但是為啥寫了這么多代碼?
Pedro: 因?yàn)槲覀冃枰WC只會(huì)有一個(gè) UIConfiguration 的實(shí)例存在。
Eve: 那我問你一個(gè)問題:單例和全局變量之間有啥區(qū)別。
Pedro: 你說啥?
Eve: ……單例和全局變量之間的區(qū)別啊。
Pedro: Java 不支持全局變量。
Eve: 但是 UIConfiguration.INSTANCE 就是全局變量啊。
Pedro: 好吧,就算是吧。
Eve: 單例模式在 Clojure 里面的實(shí)現(xiàn)就是最簡單的 def。
(def ui-config (load-config "ui.config"))
(defn load-config [config-file]
;; process config file and return map with configuratios
{:bg-style "black" :font-style "Arial"})
Pedro: 但是你這樣怎么改變樣式呢?
Eve: 你怎么在你的代碼里改,我就怎么改。
Pedro: 額……好吧,我們?cè)黾狱c(diǎn)難度。把 UIConfiguration.loadConfig 變成公共的,這樣當(dāng)需要改配置的時(shí)候就可以調(diào)用它來改了。
Eve: 那我就把 ui-config 改成 atom 然后想改配置的時(shí)候就調(diào)用 swap! 。
Pedro: 但是 atoms 只在并發(fā)環(huán)境下才有用啊。
Eve: 第一點(diǎn),雖然 atoms 在并發(fā)環(huán)境下有用,但是并不是只能用在并發(fā)環(huán)境下。第二點(diǎn),atom 的讀操作并不是你想的那么緩慢。第三點(diǎn),這種改變 UI 配置的方式是原子性 的。
Pedro: 在這個(gè)簡單例子里需要關(guān)心原子性么?
Eve: 需要啊??紤]這種可能性,UI 配置發(fā)生了變化,一些渲染器讀取到了新的 backgroundStyle,卻讀取到了老的 fontStyle。
Pedro: 好吧,那就給 loadConfig 加上 synchronized 關(guān)鍵字。
Eve: 那你必須還得給 getter 也加上 synchonized,這樣會(huì)導(dǎo)致運(yùn)行速度變慢。
Pedro: 我可以用雙重檢查鎖定 習(xí)語啊。
Eve: 雙重檢查鎖定是很巧妙,但是并不總是管用。
Pedro: 好吧我認(rèn)輸,你贏了。
第十六集:責(zé)任鏈
紐約營銷組織 "A Profit NY" 需要在他們的公共聊天系統(tǒng)上開啟敏感詞過濾。
Pedro: 臥槽, 他們不喜歡"槽 "這個(gè)字兒?
Eve: 他們是營利組織,如果有人在公共聊天室說臟話會(huì)造成經(jīng)濟(jì)損失的。
Pedro: 那又是誰定義了臟話列表?
Eve: George Carlin 。
(譯注:原文給出的鏈接是 youtube 上 George Carlin 的演講視頻,由于你懂的原因這里替換成一個(gè)可以訪問的介紹。)
邊看邊笑
Pedro: 好吧,那就加一個(gè)過濾器把這些臟字替換成星號(hào)好了。
Eve: 還要確保你的方案是可擴(kuò)展的,或許還要添加其它的過濾器呢。
Pedro: 使用責(zé)任鏈模式應(yīng)該是一個(gè)不錯(cuò)的候選。首先我們需要搞個(gè)抽象的過濾器。
public abstract class Filter {
protected Filter nextFilter;
abstract void process(String message);
public void setNextFilter(Filter nextFilter) {
this.nextFilter = nextFilter;
}
}
Pedro: 然后,實(shí)現(xiàn)你所需要的具體的過濾器
class LogFilter extends Filter {
@Override
void process(String message) {
Logger.info(message);
if (nextFilter != null) nextFilter.process(message);
}
}
class ProfanityFilter extends Filter {
@Override
void process(String message) {
String newMessage = message.replaceAll("fuck", "f*ck");
if (nextFilter != null) nextFilter.process(newMessage);
}
}
class RejectFilter extends Filter {
@Override
void process(String message) {
System.out.println("RejectFilter");
if (message.startsWith("[A PROFIT NY]")) {
if (nextFilter != null) nextFilter.process(message);
} else {
// reject message - do not propagate processing
}
}
}
class StatisticsFilter extends Filter {
@Override
void process(String message) {
Statistics.addUsedChars(message.length());
if (nextFilter != null) nextFilter.process(message);
}
}
Pedro: 最后,組合成一個(gè)過濾器鏈,傳給它需要處理的信息。
Filter rejectFilter = new RejectFilter();
Filter logFilter = new LogFilter();
Filter profanityFilter = new ProfanityFilter();
Filter statsFilter = new StatisticsFilter();
rejectFilter.setNextFilter(logFilter);
logFilter.setNextFilter(profanityFilter);
profanityFilter.setNextFilter(statsFilter);
String message = "[A PROFIT NY] What the fuck?";
rejectFilter.process(message);
Eve: 好的,現(xiàn)在輪到 Clojure了。只需把各種過濾器定義為函數(shù)。
;; define filters
(defn log-filter [message]
(logger/log message)
message)
(defn stats-filter [message]
(stats/add-used-chars (count message))
message)
(defn profanity-filter [message]
(clojure.string/replace message "fuck" "f*ck"))
(defn reject-filter [message]
(if (.startsWith message "[A Profit NY]")
message))
Eve: 然后使用 some-> 宏鏈接各個(gè)過濾器。
(defn chain [message]
(some-> message
reject-filter
log-filter
stats-filter
profanity-filter))
Eve: 你看到有多簡單了么,不需要每次都調(diào)用 if (nextFilter != null) nextFilter.process(),他們就自然地鏈接在一起。調(diào)用鏈的順序自然地依照 some-> 里從上到下填寫函數(shù)的順序,無需手動(dòng)使用 setNext。
Pedro: 這東西的可組合性真的強(qiáng)啊,但是為啥這里你選擇使用 some->,而不是選擇用 ->。
Eve: 是為了實(shí)現(xiàn) 有阻過濾器 (reject-filter)。它可以盡早地停止處理過程,一旦有過濾器返回 nil,some-> 就會(huì)直接返回 nil。
Pedro: 可以進(jìn)一步解釋一下么?
Eve: 看看實(shí)際用法你就懂了
(chain "fuck") => nil
(chain "[A Profit NY] fuck") => "f*ck"
Pedro: 懂了。
Eve: 責(zé)任鏈模式 不過是一種函數(shù)組合。
第十七集:組合
女演員 Bella Hock 投訴說,在她的電腦上看不到我們社交網(wǎng)站的用戶頭像。
“看誰都是黑的,這是黑洞么?”
Pedro: 技術(shù)上來說是黑色方框。
Eve: 額,在我的電腦上也出現(xiàn)了這個(gè)問題。
Pedro: 應(yīng)該是最近的一次更新把頭像顯示給搞壞了。
Eve: 奇怪啊,渲染頭像的方式和渲染其它節(jié)點(diǎn)的方式是一樣的啊,但是其它節(jié)點(diǎn)的顯示都正常啊。
Pedro: 你確定是同一種渲染方式?
Eve: 額……不確定
開始扒拉代碼
Pedro: 這里?發(fā)生了什么?
Eve: 不知誰從哪復(fù)制的代碼,粘貼過來之后忘記改頭像這部分了。
Pedro: 強(qiáng)烈譴責(zé),開啟譴責(zé)工具 git-blame。
Eve: 譴責(zé) 雖然是好東西,但是我們還是得修復(fù)這個(gè)問題啊。
Pedro: 修復(fù)很簡單啊,就在這加一行代碼。
Eve: 我的意思是,真正解決掉這個(gè)問題。為啥我們要使用兩段相似的代碼來處理同一個(gè)模塊?
Pedro: 對(duì)耶,我覺得我們可以用組合模式來搞定整個(gè)界面的渲染問題。我們定義最小的渲染元素是一個(gè)塊 (Block)。
public interface Block {
void addBlock(Block b);
List<Block> getChildren();
void render();
}
Pedro: 很顯然一個(gè)塊里面可以嵌套著其它的塊,這是組合模式的核心所在,首先我們可以創(chuàng)造出一些塊的實(shí)現(xiàn)。
public class Page implements Block { }
public class Header implements Block { }
public class Body implements Block { }
public class HeaderTitle implements Block { }
public class UserAvatar implements Block { }
Pedro: 然后把各種具體實(shí)現(xiàn)依然當(dāng)作 Block 來處理
Block page = new Page();
Block header = new Header();
Block body = new Body();
Block title = new HeaderTitle();
Block avatar = new UserAvatar();
page.addBlock(header);
page.addBlock(body);
header.addBlock(title);
header.addBlock(avatar);
page.render();
Pedro: 這是一種關(guān)于組織結(jié)構(gòu)的模式,是一種組合 (compose) 對(duì)象的好方式。所以我們叫它組合結(jié)構(gòu) (composite)
Eve: 喂,組合結(jié)構(gòu)不就是個(gè)樹形結(jié)構(gòu)么。
Pedro: 是的。
Eve: 這種模式適用于所有的數(shù)據(jù)結(jié)構(gòu)么?
Pedro: 不,只適用于列表和樹形結(jié)構(gòu)。
Eve: 實(shí)際上,樹形可以用列表來表示。
Pedro: 怎么表示?
Eve: 第一個(gè)元素表示父節(jié)點(diǎn),后續(xù)元素表示子節(jié)點(diǎn),依次這樣……
Pedro: 我懂了。
Eve: 為了更詳細(xì)地進(jìn)行說明,假如有這樣一棵樹
A
/ | \
B C D
| | / \
E H J K
/ \ /|\
F G L M N
Eve: 然后這是這棵樹的列表形式表達(dá)
(def tree
'(A (B (E (F) (G))) (C (H)) (D (J) (K (L) (M) (N)))))
Pedro: 這括號(hào)數(shù)量有點(diǎn)夸張??!
Eve: 用來明確定義結(jié)構(gòu),你懂的。
Pedro: 但是這樣理解起來很困難啊。
Eve: 但適合機(jī)器識(shí)別,這里提供了一個(gè)十分酷炫的功能 tree-seq,用來解析這顆樹。
(map first (tree-seq next rest tree)) => (A B E F G C H D J K L M N)
Eve: 如果你需要更強(qiáng)大的遍歷功能,可以試試 clojure.walk
Pedro: 我看不懂,這東西好像有點(diǎn)難。
Eve: 不用全部理解,你就只需了解用一種數(shù)據(jù)結(jié)構(gòu)就可以表示整棵數(shù),一個(gè)函數(shù)就可以操作它。
Pedro: 這個(gè)函數(shù)都會(huì)干點(diǎn)啥?
Eve: 它會(huì)遍歷這顆樹,然后把指定的函數(shù)作用于所有的節(jié)點(diǎn),在我們的例子里就是渲染每個(gè)塊。
Pedro: 我還是不懂,可能我還是太年輕了,我們跳過樹的這個(gè)部分。
第十八集:工廠方法
Sir Dry Bang 提議要給他們熱賣的游戲增加新的關(guān)卡。關(guān)卡多多,圈錢多多。
Pedro: 我們要搞出來一個(gè)啥樣的新關(guān)卡?
Eve: 就簡單改一下道具資源然后加一點(diǎn)新的物體材質(zhì):紙,木頭,鐵……
Pedro: 這么做是不是有點(diǎn)腦殘?
Eve: 反正本身就是個(gè)腦殘游戲。如果玩家愿意砸錢給他的游戲角色買個(gè)彩色帽子,那肯定也愿意買個(gè)木頭材質(zhì)的塊塊兒。
Pedro: 我也這么覺得,不管咋說,先搞一個(gè)通用的 MazeBuilder 然后為每種類型的方塊創(chuàng)建具體的 builder。這叫工廠模式。
class Maze { }
class WoodMaze extends Maze { }
class IronMaze extends Maze { }
interface MazeBuilder {
Maze build();
}
class WoodMazeBuilder {
@Override
Maze build() {
return new WoodMaze();
}
}
class IronMazeBuilder {
@Override
Maze build() {
return new IronMaze();
}
}
Eve: 難道 IronMazeBuilder 還能不返回 IronMazes?
Pedro: 這不是重點(diǎn),重點(diǎn)是,如果你想要生產(chǎn)其它材質(zhì)的方塊,只需要改變具體的生產(chǎn)工廠。
MazeBuilder builder = new WoodMazeBuilder();
Maze maze = builder.build();
Eve: 這好像和之前的哪個(gè)模式挺像的。
Pedro: 你說哪個(gè)?
Eve: 我覺得像策略模式和狀態(tài)模式。
Pedro: 怎么可能!策略模式是關(guān)于選擇哪一種合適的操作,而工廠模式是為了生產(chǎn)適合的對(duì)象。
Eve: 但是生產(chǎn)同樣可以看作一種操作。
(defn maze-builder [maze-fn])
(defn make-wood-maze [])
(defn make-iron-maze [])
(def wood-maze-builder (partial maze-builder make-wood-maze))
(def iron-maze-builder (partial maze-builder make-iron-maze))
Pedro: 嗯,的確看起來很像。
Eve: 對(duì)吧。
Pedro: 有什么使用范例沒?
Eve: 用不著,按照你的直覺來使用就行,你可以回到上面再看一下 策略、狀態(tài) 或 模板方法 這些章節(jié)。
第十九集:抽象工廠
玩家不愿意購買游戲推出的新關(guān)卡。于是 Saimank Gerr 搭了一個(gè)反饋云平臺(tái)供玩家吐槽。根據(jù)反饋結(jié)果分析,出現(xiàn)最多的負(fù)面詞匯是:“丑”,“垃圾”,“渣”。
改進(jìn)一下關(guān)卡構(gòu)建系統(tǒng)。
Pedro: 我就說了吧這是個(gè)垃圾游戲。
Eve: 是啊,雪地背景配木墻,太空侵入配木墻,啥都東西都搭配木制墻體是要鬧哪樣。
Pedro: 所以我們必須得把每關(guān)的游戲世界分離出來,然后再給每種世界分配一組具體的對(duì)象。
Eve: 解釋一下。
Pedro: 我們不用以前構(gòu)建具體方塊的工廠方法了,取而代之的是使用抽象工廠,以創(chuàng)建一組相關(guān)對(duì)象,這樣以來構(gòu)建關(guān)卡的方式看起來就不會(huì)那么糟糕了。
Eve: 舉個(gè)栗子。
Pedro: 看代碼。首先我們定義抽象 關(guān)卡工廠的行為
public interface LevelFactory {
Wall buildWall();
Back buildBack();
Enemy buildEnemy();
}
Pedro: 然后是關(guān)卡元素的層次結(jié)構(gòu),關(guān)卡就是由這些內(nèi)容組成的
class Wall {}
class PlasmaWall extends Wall {}
class StoneWall extends Wall {}
class Back {}
class StarsBack extends Back {}
class EarthBack extends Back {}
class Enemy {}
class UFOSoldier extends Enemy {}
class WormScout extends Enemy {}
Pedro: 看到?jīng)]?我們給每個(gè)關(guān)卡都提供了具體的對(duì)象,現(xiàn)在就可以給它們創(chuàng)建工廠了。
class SpaceLevelFactory implements LevelFactory {
@Override
public Wall buildWall() {
return new PlasmaWall();
}
@Override
public Back buildBack() {
return new StarsBack();
}
@Override
public Enemy buildEnemy() {
return new UFOSoldier();
}
}
class UndergroundLevelFactory implements LevelFactory {
@Override
public Wall buildWall() {
return new StoneWall();
}
@Override
public Back buildBack() {
return new EarthBack();
}
@Override
public Enemy buildEnemy() {
return new WormScout();
}
}
Pedro: 關(guān)卡工廠的實(shí)現(xiàn)類為各個(gè)關(guān)卡生產(chǎn)出相關(guān)的一組對(duì)象。這樣肯定比以前的關(guān)卡好看。
Eve: 讓我冷靜一下。我真的看不出這和工廠方法有啥區(qū)別。
Pedro: 工廠方法把創(chuàng)建對(duì)象推遲到子類,抽象工廠也一樣,只不過創(chuàng)建的是一組相關(guān)對(duì)象 。
Eve: 啊哈,也就是說我需要一組相關(guān)的函數(shù)來實(shí)現(xiàn)抽象工廠。
(defn level-factory [wall-fn back-fn enemy-fn])
(defn make-stone-wall [])
(defn make-plasma-wall [])
(defn make-earth-back [])
(defn make-stars-back [])
(defn make-worm-scout [])
(defn make-ufo-soldier [])
(def underground-level-factory
(partial level-factory
make-stone-wall
make-earth-back
make-worm-scout))
(def space-level-factory
(partial level-factory
make-plasma-wall
make-stars-back
make-ufo-soldier))
Pedro: 很眼熟。
Eve: 就是這么直接。你掛在嘴邊的“一組相關(guān)的東西”,在我看來“東西”就是函數(shù)。
Pedro: 是的,很清晰,不過 partial 是干啥的。
Eve: partial 用來向函數(shù)提供參數(shù)。所以,underground-level-factory 只需考慮構(gòu)建什么樣式的墻體、背景和敵人。其余的功能都是從抽象的 level-factory 方法繼承而來的。
Pedro: 很方便。
第二十集:適配
Deam Evil 舉辦了一場復(fù)古風(fēng)格中世紀(jì)騎士對(duì)決。獎(jiǎng)金高達(dá) $100.000
我分你一半獎(jiǎng)金,只要你能黑掉他的系統(tǒng),允許我的武裝突擊隊(duì)加入比賽。
Pedro: 終于,我們接到一個(gè)好玩的活了。
Eve: 我非常期待這場比賽啊。尤其是 M16 對(duì)陣鐵劍的部分。
Pedro: 但是騎士們都穿著良好的盔甲啊。
Eve: F1 手榴彈根本不在乎 什么盔甲。
Pedro: 管他呢,只管干活拿錢。
Eve: 五萬大洋,好價(jià)錢啊。
Pedro: 可不是嘛,瞅瞅這個(gè),我搞到了他們競賽系統(tǒng)的源碼,雖然我們不大可能直接修改源碼吧,但是說不準(zhǔn)能找到一些漏洞。
Eve: 我找到漏洞了
public interface Tournament {
void accept(Knight knight);
}
Pedro: 啊哈!系統(tǒng)只用了 Knight 做傳入?yún)?shù)檢查。 只需要把突擊隊(duì)員偽造 (to adapt) 成騎士就行了。讓我們看看騎士都長什么樣子
interface Knight {
void attackWithSword();
void attackWithBow();
void blockWithShield();
}
class Galahad implements Knight {
@Override
public void blockWithShield() {
winkToQueen();
take(shield);
block();
}
@Override
public void attackWithBow() {
winkToQueen();
take(bow);
attack();
}
@Override
public void attackWithSword() {
winkToQueen();
take(sword);
attack();
}
}
Pedro: 為了能傳入突擊隊(duì)員,我們先看看突擊隊(duì)員的原始實(shí)現(xiàn)
class Commando {
void throwGrenade(String grenade) { }
shot(String rifleType) { }
}
Pedro: 開始改造 (adapt)
class Commando implements Knight {
@Override
public void blockWithShield() {
// commando don't block
}
@Override
public void attackWithBow() {
throwGrenade("F1");
}
@Override
public void attackWithSword() {
shotWithRifle("M16");
}
}
Pedro: 這樣就搞定了。
Eve: Clojure 里更簡單。
Pedro: 真的?
Eve: 我們不喜歡類型,所以根本沒有類型檢查。
Pedro: 那你是怎么把騎士替換成突擊隊(duì)員的呢?
Eve: 本質(zhì)上,騎士是什么?就是一個(gè)由數(shù)據(jù)和行為組成的 map 而已。
{:name "Lancelot"
:speed 1.0
:attack-bow-fn attack-with-bow
:attack-sword-fn attack-with-sword
:block-fn block-with-shield}
Eve: 為了能適配突擊隊(duì)員,只需把原始的函數(shù)替換為突擊隊(duì)員的函數(shù)
{:name "Commando"
:speed 5.0
:attack-bow-fn (partial throw-grenade "F1")
:attack-sword-fn (partial shot "M16")
:block-fn nil}
Pedro: 我們?cè)趺?del>分贓分錢?
Eve: 五五開。
Pedro: 我寫的代碼行多啊,我要七。
Eve: 行,七七開。
Pedro: 成交。
第二十一集:裝飾者
Podrea Vesper 抓到我們?cè)诒荣惿献鞅住,F(xiàn)在有兩條路可以走:要么進(jìn)局子,要么就幫他的超級(jí)騎士加入比賽。
Pedro: 我不想進(jìn)監(jiān)獄。
Eve: 我也不想。
Pedro: 那我們就再幫他做一次弊吧。
Eve: 和上一次一樣,是吧?
Pedro 不完全是。突擊隊(duì)員是軍隊(duì)的人,本來是不允許參加比賽的。我們適配 (adapted) 了一下。但是騎士本來就允許參加比賽,不需要我們?cè)俑脑炝?。我?em>必須 給現(xiàn)有的對(duì)象增加新的行為。
Eve: 繼承還是組合?
Pedro: 組合,裝飾者模式的主要目的就是要在運(yùn)行時(shí)改變行為。
Eve: 所以,我們要怎么造出一個(gè)超級(jí)騎士呢?
Pedro: 他們計(jì)劃派出騎士 Galahad,然后給他裝飾 一下,讓他擁有超多血量 和強(qiáng)力盔甲 。
Eve: 嘿,這個(gè)條子竟然還玩兒輻射[1]呢。
Pedro: 嗯哪,讓我們先寫一個(gè)抽象騎士類
public class Knight {
protected int hp;
private Knight decorated;
public Knight() { }
public Knight(Knight decorated) {
this.decorated = decorated;
}
public void attackWithSword() {
if (decorated != null) decorated.attackWithSword();
}
public void attackWithBow() {
if (decorated != null) decorated.attackWithBow();
}
public void blockWithShield() {
if (decorated != null) decorated.blockWithShield();
}
}
Eve: 所以我們改造了哪些功能?
Pedro: 首先我們使用 Knight 類取代原來的接口,增加了血量屬性。然后我們提供了兩個(gè)不同的構(gòu)造方法,默認(rèn)無參的是標(biāo)準(zhǔn)行為,decorated 參數(shù)表示需要裝飾的對(duì)象。
Eve: 用類代替接口是不是因?yàn)轭惛苯右恍?br>
Pedro: 不是因?yàn)檫@個(gè),是因?yàn)檫@樣可以避免出現(xiàn)兩個(gè)功能相似的類,同時(shí)不必強(qiáng)制對(duì)象實(shí)現(xiàn)所有的方法,因?yàn)槲覀兘o每個(gè)待裝飾對(duì)象提供了方法的默認(rèn)實(shí)現(xiàn)。
Eve: 好吧,那強(qiáng)力的盔甲在哪里?
Pedro: 很簡單
public class KnightWithPowerArmor extends Knight {
public KnightWithPowerArmor(Knight decorated) {
super(decorated);
}
@Override
public void blockWithShield() {
super.blockWithShield();
Armor armor = new PowerArmor();
armor.block();
}
}
public class KnightWithAdditionalHP extends Knight {
public KnightWithAdditionalHP(Knight decorated) {
super(decorated);
this.hp += 50;
}
}
Pedro: 兩個(gè)裝飾者就可以滿足 FBI 的要求,然后我們就可以著手制造看起來和 Galahad 差不多,但是擁有強(qiáng)力盔甲和額外 50 點(diǎn)血量的超級(jí)騎士了。
Knight superKnight =
new KnightWithAdditionalHP(
new KnightWithPowerArmor(
new Galahad()));
Eve: 這個(gè)特技加的可以。
Pedro: 接下來有請(qǐng)你來展示一下 Clojure 是怎么實(shí)現(xiàn)類似功能的。
Eve: 好的
(def galahad {:name "Galahad"
:speed 1.0
:hp 100
:attack-bow-fn attack-with-bow
:attack-sword-fn attack-with-sword
:block-fn block-with-shield})
(defn make-knight-with-more-hp [knight]
(update-in knight [:hp] + 50))
(defn make-knight-with-power-armor [knight]
(update-in knight [:block-fn]
(fn [block-fn]
(fn []
(block-fn)
(block-with-power-armor)))))
;; create the knight
(def superknight (-> galahad
make-knight-with-power-armor
make-knight-with-more-hp)
Pedro: 的確也可以滿足要求。
Eve: 是的,這里要提一下,強(qiáng)力盔甲裝飾器是個(gè)亮點(diǎn)。
(譯注:亮點(diǎn)可能是使用了閉包。)
第二十二集:代理
Deren Bart 是一個(gè)調(diào)酒制造系統(tǒng)的管理員。這個(gè)系統(tǒng)非常地死板難用,因?yàn)槊看握{(diào)制完畢之后,Bart 都必須手動(dòng)的從酒吧庫存中扣除已使用的原材料。把它改成自動(dòng)的。
Pedro: 能搞到他代碼庫的權(quán)限么?
Eve: 不能,但是他給了一些 API。
interface IBar {
void makeDrink(Drink drink);
}
interface Drink {
List<Ingredient> getIngredients();
}
interface Ingredient {
String getName();
double getAmount();
}
Pedro: Bart 不想讓我們修改源碼,所以我們得通過實(shí)現(xiàn) IBar 接口來提供一些額外的功能 --- 自動(dòng)扣除已用原料。
Eve: 怎么搞啊?
Pedro: 用代理模式 ,前幾天我還看這個(gè)模式來著。
Eve: 講給我聽聽唄。
Pedro: 基本思路就是,所有已有功能依然調(diào)用之前標(biāo)準(zhǔn)的 IBar 實(shí)現(xiàn)來執(zhí)行,然后在 ProxiedBar 里提供新的功能
class ProxiedBar implements IBar {
BarDatabase bar;
IBar standardBar;
public void makeDrink(Drink drink) {
standardBar.makeDrink(drink);
for (Ingredient i : drink.getIngredients()) {
bar.subtract(i);
}
}
}
Pedro:
Pedro: 他們只需要把老的 StandardBar 實(shí)現(xiàn)類替換成我們的 ProxiedBar。
Eve: 看起來超級(jí)簡單啊。
Pedro: 是的,額外加入的功能并不會(huì)破壞已有功能。
Eve: 你確定?我們還沒有做回歸測(cè)試呢。
Pedro: 所有的功能都是委派給已經(jīng)通過測(cè)試的 StandardBar 去執(zhí)行的啊。
Eve: 但是同時(shí)你還調(diào)用了 BarDatabase 扣除了已用原材料啊。
Pedro: 我們可以認(rèn)為他們是解耦的 (decoupled) 。
Eve: 哦……
Pedro: Clojure 里有什么替代方案么?
Eve: 這個(gè),我也不清楚。在我看來你只是在用函數(shù)組合 (function composition)。
Pedro: 怎么說。
Eve: IBar 的實(shí)現(xiàn)類是一組函數(shù),其它什么的各種 IBar 都不過是一組函數(shù)。你所謂的一切額外加入的功能都可以通過函數(shù)組合來實(shí)現(xiàn)。就好比在 make-drink 之后對(duì)酒吧庫存進(jìn)行 subtract-ingredients 操作不就行了。
Pedro: 可能用代碼描述會(huì)更清晰一點(diǎn)?
Eve: 嗯,不過我并不覺得這有啥特別的
;; interface
(defprotocol IBar
(make-drink [this drink]))
;; Bart's implementation
(deftype StandardBar []
IBar
(make-drink [this drink]
(println "Making drink " drink)
:ok))
;; our implementation
(deftype ProxiedBar [db ibar]
IBar
(make-drink [this drink]
(make-drink ibar drink)
(subtract-ingredients db drink)))
;; this how it was before
(make-drink (StandardBar.)
{:name "Manhattan"
:ingredients [["Bourbon" 75] ["Sweet Vermouth" 25] ["Angostura" 5]]})
;; this how it becomes now
(make-drink (ProxiedBar. {:db 1} (StandardBar.))
{:name "Manhattan"
:ingredients [["Bourbon" 75] ["Sweet Vermouth" 25] ["Angostura" 5]]})
Eve: 我們可以利用協(xié)議 (protocol) 和類型 (types) 把一組函數(shù)聚合在一個(gè)對(duì)象里。
Pedro: 看起來 Clojure 也有著面向?qū)ο蟮哪芰Π ?br>
Eve: 沒錯(cuò),不僅如此,我們還可以使用 reify 功能,它可以允許我們?cè)谶\(yùn)行時(shí)創(chuàng)建代理。
Pedro: 就好比在運(yùn)行時(shí)創(chuàng)建類?
Eve: 差不多。
(reify IBar
(make-drink [this drink]
;; implementation goes here
))
Pedro: 感覺挺好用的。
Eve: 是啊,但是我還是沒有理解它和裝飾者的區(qū)別在哪。
Pedro: 完全不一樣啊。
Eve: 裝飾者給接口增加功能,代理也是給接口增加功能。
Pedro: 好吧,但是代理……
Eve: 甚至,適配器看起來也沒啥區(qū)別嘛。
Pedro: 適配器用了另一個(gè)接口。
Eve: 但是從實(shí)現(xiàn)的角度來說,這些模式都是一樣的道理,把一些東西包裝起來,然后把調(diào)用委派給包裝者。我覺得叫它們“包裝者 (Wrapper)” 模式更好一些。
第二十三集:橋接
一位來自人力資源代理機(jī)構(gòu) "Hurece's Sour Man" 的女孩需要審核應(yīng)征者是否滿足職位要求。問題在于,一般來說工作崗位是顧客設(shè)計(jì)的,但是職位要求卻是人力部門設(shè)計(jì)的。給他們提供一個(gè)靈活的方式來協(xié)調(diào)這個(gè)問題。
(譯注:“工作崗位是顧客設(shè)計(jì)的”,人力資源代理機(jī)構(gòu)的顧客就是用人單位了。也就是說工作崗位是用人公司設(shè)計(jì)的,職位要求是人力資源代理機(jī)構(gòu)設(shè)計(jì)的。)
Eve: 說實(shí)話我沒看明白這個(gè)問題。
Pedro: 我倒是有點(diǎn)這方面背景。他們的系統(tǒng)非常的奇怪,職位要求是用一個(gè)接口來描述的。
interface JobRequirement {
boolean accept(Candidate c);
}
Pedro: 通過實(shí)現(xiàn)這個(gè)接口,來表示每一個(gè)具體的職位要求。
class JavaRequirement implements JobRequirement {
public boolean accept(Candidate c) {
return c.hasSkill("Java");
}
}
class Experience10YearsRequirement implements JobRequirement {
public boolean accept(Candidate c) {
return c.getExperience() >= 10;
}
}
Eve: 我好像明白了點(diǎn)。
Pedro: 你要諒解,畢竟這個(gè)層次結(jié)構(gòu)是人力部設(shè)計(jì)的。
Eve: 好的。
Pedro: 然后還有一個(gè) Job 層級(jí),用來描述崗位,和職位要求一樣,每個(gè)具體崗位都要實(shí)現(xiàn) Job。
Eve: 為啥他們要把每種崗位都用一個(gè)類來表示?明明一個(gè)對(duì)象就可以了啊。
Pedro: 這個(gè)系統(tǒng)設(shè)計(jì)的時(shí)候就是類要比對(duì)象還多,所以你就先湊合著。
Eve: 類比對(duì)象還多?!
Pedro: 是的,好好聽別打岔。崗位和職位要求是兩個(gè)完全分離開的層級(jí),而且崗位是由用人單位設(shè)計(jì)的。現(xiàn)在我們有請(qǐng) 橋接 (Bridge) 模式來關(guān)聯(lián)這兩個(gè)分離的層級(jí),并允許兩者繼續(xù)獨(dú)立運(yùn)轉(zhuǎn)。
abstract class Job {
protected List<? extends JobRequirement> requirements;
public Job(List<? extends JobRequirement> requirements) {
this.requirements = requirements;
}
protected boolean accept(Candidate c) {
for (JobRequirement j : requirements) {
if (!j.accept(c)) {
return false;
}
}
return true;
}
}
class CognitectClojureDeveloper extends Job {
public CognitectClojureDeveloper() {
super(Arrays.asList(
new ClojureJobRequirement(),
new Experience10YearsRequirement()
));
}
}
Eve: 橋呢?
Pedro: JobRequirement, JavaRequirement, ExperienceRequirement 是一個(gè)層級(jí),是吧?
Eve: 是啊。
Pedro: Job, CongnitectClojureDeveloperJob, OracleJavaDeveloperJob 是另一個(gè)層級(jí)。
Eve: 哦,我明白了。職位和職位要求之間的聯(lián)系就是那個(gè)橋。
Pedro: 非常對(duì)!這樣以來人事部的人員就可以像這樣來進(jìn)行審核了。
Candidate joshuaBloch = new Candidate();
(new CognitectClojureDeveloper()).accept(joshuaBloch);
(new OracleSeniorJavaDeveloper()).accept(joshuaBloch);
Pedro: 總結(jié)一下要點(diǎn)。用人單位使用抽象的 Job 以及 JobRequirement 的實(shí)現(xiàn)。他們只需要大概描述一下崗位的情況就行了,然后人力資源部門負(fù)責(zé)把描述轉(zhuǎn)換成一組 JobRequirement 對(duì)象。
Eve: 明白了。
Pedro: 據(jù)我了解,Clojure 可以用 defprotocol 和 defrecord 來模擬這個(gè)模式?
Eve: 是的,不過我想重溫一下這個(gè)問題。
Pedro: 為啥???
Eve: 我們先整理一下套路:顧客描述崗位,人力資源部把它轉(zhuǎn)換成一組職位要求,然后在求職數(shù)據(jù)庫里跑一段腳本去逐一嘗試看沒有沒有符合要求的人員?
Pedro: 沒錯(cuò)。
Eve: 所以這里還是存在依賴關(guān)系啊,沒有職位空缺的話 HR 啥也干不了。
Pedro: 這個(gè),算是吧。但是他們還是可以在沒有職位空缺的情況下設(shè)計(jì)出一組職位要求。
Eve: 目的何在?
Pedro: 提前搞出來,留著以后碰見一樣的要求就可以直接拿來用了啊。
Eve: 行吧,但是這不就是自找麻煩了。本來我們只是想要找到一種在抽象與實(shí)現(xiàn)之間協(xié)調(diào)的方式而已。
Pedro: 也許吧,我想看看你是怎么在 Clojure 里用橋接模式解決這個(gè)特定問題的。
Eve: 簡單。用專設(shè)層級(jí) (adhoc hierarchies)。
Pedro: 要給抽象設(shè)置層級(jí)?
Eve: 是的,崗位是抽象 層級(jí),然后我們只需要對(duì)其進(jìn)行擴(kuò)展。
;; abstraction
(derive ::clojure-job ::job)
(derive ::java-job ::job)
(derive ::senior-clojure-job ::clojure-job)
(derive ::senior-java-job ::java-job)
Eve: HR 部門就好比開發(fā)者 , 他們提供抽象的具體實(shí)現(xiàn)。
;; implementation
(defmulti accept :job)
(defmethod accept :java [candidate]
(and (some #{:java} (:skills candidate))
(> (:experience candidate) 1)))
Eve: 如果以后有新崗位出現(xiàn),但是崗位需求還沒有被確認(rèn),當(dāng)然也沒有與之對(duì)應(yīng)的 accept 方法,這個(gè)時(shí)候就會(huì)回退到上個(gè)層級(jí)。
Pedro: 蛤?
Eve: 假如某人創(chuàng)建了一個(gè)新的下屬于 ::java 崗位的 ::senior-java 崗位。
Pedro: 哦!如果 HR 沒有給委派值 ::senior-java 提供 accept 實(shí)現(xiàn),多重方法就會(huì)委派給 ::java 對(duì)應(yīng)的方法,對(duì)吧?
Eve: 小伙子學(xué)的挺快嘛。
Pedro: 但是這還是橋接模式么?
Eve: 這里本來就沒有什么橋 ,但是同樣得以讓抽象與實(shí)現(xiàn)可以獨(dú)立地運(yùn)轉(zhuǎn)。
劇終。
速查表 (代替總結(jié))
模式非常難以理解,關(guān)于它們的介紹,通常都是使用面向?qū)ο蟮姆绞?,再配上一?UML 圖表和花哨的名詞,而且還是為了解決特定語言下的問題,所以這里提供了一張迷你復(fù)習(xí)速查表,希望能用類比的方式幫助你理解模式的本質(zhì)。
- 命令 (Command) - 函數(shù)
- 策略 (Strategy) - 接受函數(shù)的函數(shù)
- 狀態(tài) (State) - 依據(jù)狀態(tài)的策略
- 訪問者 (Visitor) - 多重分派
- 模板方法 (Template Method) - 默認(rèn)策略
- 迭代器 (Iterator) - 序列
- 備忘錄 (Memento) - 保存和恢復(fù)
- 原型 (Prototype) - 不可變值
- 中介者 (Mediator) - 解耦和
- 觀察者 (Observer) - 在函數(shù)后調(diào)用函數(shù)
- 解釋器 (Interpreter) - 一組解析樹形結(jié)構(gòu)的函數(shù)
- 羽量 (Flyweight) - 緩存
- 建造者 (Builder) - 可選參數(shù)列表
- 外觀 (Facade) - 單一訪問點(diǎn)
- 單例 (Singleton) - 全局變量
- 責(zé)任鏈 (Chain of Responsibility) - 函數(shù)組合
- 組合 (Composite) - 樹形結(jié)構(gòu)
- 工廠方法 (Factory Method) - 制造對(duì)象的策略
- 抽象工廠 (Abstract Factory) - 制造一組相關(guān)對(duì)象的策略
- 適配 (Adapter) - 包裝,功能相同,類型不同
- 裝飾者 (Decorator) - 包裝, 類型相同, 但增加了新功能
- 代理 (Proxy) - 包裝, 函數(shù)組合
- 橋接 (Bridge) - 分離抽象和實(shí)現(xiàn)
演員表
很久很久以前,在一個(gè)很遠(yuǎn)很遠(yuǎn)的星系…… [2]
由于思維匱乏,所有登場的名字都是字母倒置游戲。
Pedro Veel - Developer
Eve Dopler - Developer
Serpent Hill & R.E.E. - Enterprise Hell
Sven Tori - Investor
Karmen Git - Marketing
Natanius S. Selbys - Business Analyst
Mech Dominore Fight Saga - Heroes of Might and Magic
Kent Podiololis - I don't like loops
Chad Bogue - Douchebag
Dex Ringeus - UX Designer
Veerco Wierde - Code Review
Dartee Hebl - Heartbleed
Bertie Prayc - Cyber Pirate
Cristopher, Matton & Pharts - Important Charts & Reports
Tuck Brass - Starbucks
Eugenio Reinn Jr. - Junior Engineer
Feverro O'Neal - Forever Alone
A Profit NY - Profanity
Bella Hock - Black Hole
Sir Dry Bang - Angry Birds
Saimank Gerr - Risk Manager
Deam Evil - Medieval
Podrea Vesper - Eavesdropper
Deren Bart - Bartender
Hurece's Sour Man - Human Resources
P.S. 從剛開始提筆距今早已超過 兩年。時(shí)間飛逝,物換星移,Java 8 也已經(jīng)發(fā)布了。
clojure programming java story patterns
18 December 2015