Clojure
設(shè)計模式
Design Patterns
譯自 Clojure Design Patterns
Author: Mykhailo Kozik
第十五集:單例
Feverro O'Neal 抱怨我們的 UI 樣式太多了。
把每個應(yīng)用的 UI 配置都統(tǒng)一成一份。
Pedro: 等下绑榴,我看這里要求每個用戶都可以保存 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: 沒錯是沒錯,但是為啥寫了這么多代碼挽荡?
Pedro: 因為我們需要保證只會有一個 UIConfiguration
的實例存在藐石。
Eve: 那我問你一個問題:單例和全局變量之間有啥區(qū)別。
Pedro: 你說啥定拟?
Eve: ……單例和全局變量之間的區(qū)別啊于微。
Pedro: Java 不支持全局變量。
Eve: 但是 UIConfiguration.INSTANCE
就是全局變量啊青自。
Pedro: 好吧株依,就算是吧。
Eve: 單例模式在 Clojure 里面的實現(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: 額……好吧荠藤,我們增加點難度。把 UIConfiguration.loadConfig
變成公共的获高,這樣當需要改配置的時候就可以調(diào)用它來改了哈肖。
Eve: 那我就把 ui-config
改成 atom 然后想改配置的時候就調(diào)用 swap!
。
Pedro: 但是 atoms 只在并發(fā)環(huán)境下才有用啊谋减。
Eve: 第一點,雖然 atoms 在并發(fā)環(huán)境下有用扫沼,但是并不是只能用在并發(fā)環(huán)境下出爹。第二點庄吼,atom 的讀操作并不是你想的那么緩慢。第三點严就,這種改變 UI 配置的方式是原子性 的总寻。
Pedro: 在這個簡單例子里需要關(guān)心原子性么?
Eve: 需要啊梢为〗バ校考慮這種可能性,UI 配置發(fā)生了變化铸董,一些渲染器讀取到了新的 backgroundStyle
祟印,卻讀取到了老的 fontStyle
。
Pedro: 好吧粟害,那就給 loadConfig
加上 synchronized
關(guān)鍵字蕴忆。
Eve: 那你必須還得給 getter 也加上 synchonized
,這樣會導致運行速度變慢悲幅。
Pedro: 我可以用雙重檢查鎖定 習語啊套鹅。
Eve: 雙重檢查鎖定是很巧妙,但是并不總是管用汰具。
Pedro: 好吧我認輸卓鹿,你贏了。
第十六集:責任鏈
紐約營銷組織 "A Profit NY" 需要在他們的公共聊天系統(tǒng)上開啟敏感詞過濾留荔。
Pedro: 臥槽, 他們不喜歡"槽 "這個字兒吟孙?
Eve: 他們是營利組織,如果有人在公共聊天室說臟話會造成經(jīng)濟損失的存谎。
Pedro: 那又是誰定義了臟話列表拔疚?
Eve: George Carlin 。
(譯注:原文給出的鏈接是 youtube 上 George Carlin 的演講視頻既荚,由于你懂的原因這里替換成一個可以訪問的介紹稚失。)
邊看邊笑
Pedro: 好吧,那就加一個過濾器把這些臟字替換成星號好了恰聘。
Eve: 還要確保你的方案是可擴展的句各,或許還要添加其它的過濾器呢。
Pedro: 使用責任鏈模式應(yīng)該是一個不錯的候選晴叨。首先我們需要搞個抽象的過濾器凿宾。
public abstract class Filter {
protected Filter nextFilter;
abstract void process(String message);
public void setNextFilter(Filter nextFilter) {
this.nextFilter = nextFilter;
}
}
Pedro: 然后,實現(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: 最后兼蕊,組合成一個過濾器鏈初厚,傳給它需要處理的信息。
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->
宏鏈接各個過濾器。
(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ù)的順序楞件,無需手動使用 setNext
衫生。
Pedro: 這東西的可組合性真的強啊,但是為啥這里你選擇使用 some->
土浸,而不是選擇用 ->
罪针。
Eve: 是為了實現(xiàn) 有阻過濾器
(reject-filter)。它可以盡早地停止處理過程栅迄,一旦有過濾器返回 nil
站故,some->
就會直接返回 nil
。
Pedro: 可以進一步解釋一下么毅舆?
Eve: 看看實際用法你就懂了
(chain "fuck") => nil
(chain "[A Profit NY] fuck") => "f*ck"
Pedro: 懂了西篓。
Eve: 責任鏈模式 不過是一種函數(shù)組合。
第十七集:組合
女演員 Bella Hock 投訴說憋活,在她的電腦上看不到我們社交網(wǎng)站的用戶頭像岂津。
“看誰都是黑的,這是黑洞么悦即?”
Pedro: 技術(shù)上來說是黑色方框吮成。
Eve: 額,在我的電腦上也出現(xiàn)了這個問題辜梳。
Pedro: 應(yīng)該是最近的一次更新把頭像顯示給搞壞了粱甫。
Eve: 奇怪啊,渲染頭像的方式和渲染其它節(jié)點的方式是一樣的啊作瞄,但是其它節(jié)點的顯示都正常啊茶宵。
Pedro: 你確定是同一種渲染方式?
Eve: 額……不確定
開始扒拉代碼
Pedro: 這里?發(fā)生了什么宗挥?
Eve: 不知誰從哪復制的代碼乌庶,粘貼過來之后忘記改頭像這部分了。
Pedro: 強烈譴責契耿,開啟譴責工具 git-blame
瞒大。
Eve: 譴責 雖然是好東西,但是我們還是得修復這個問題啊搪桂。
Pedro: 修復很簡單啊透敌,就在這加一行代碼。
Eve: 我的意思是,真正解決掉這個問題酗电。為啥我們要使用兩段相似的代碼來處理同一個模塊淌山?
Pedro: 對耶,我覺得我們可以用組合模式來搞定整個界面的渲染問題顾瞻。我們定義最小的渲染元素是一個塊 (Block)。
public interface Block {
void addBlock(Block b);
List<Block> getChildren();
void render();
}
Pedro: 很顯然一個塊里面可以嵌套著其它的塊德绿,這是組合模式的核心所在荷荤,首先我們可以創(chuàng)造出一些塊的實現(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: 然后把各種具體實現(xiàn)依然當作 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) 對象的好方式蕴纳。所以我們叫它組合結(jié)構(gòu) (composite)
Eve: 喂,組合結(jié)構(gòu)不就是個樹形結(jié)構(gòu)么个粱。
Pedro: 是的古毛。
Eve: 這種模式適用于所有的數(shù)據(jù)結(jié)構(gòu)么?
Pedro: 不都许,只適用于列表和樹形結(jié)構(gòu)稻薇。
Eve: 實際上,樹形可以用列表來表示胶征。
Pedro: 怎么表示塞椎?
Eve: 第一個元素表示父節(jié)點,后續(xù)元素表示子節(jié)點睛低,依次這樣……
Pedro: 我懂了案狠。
Eve: 為了更詳細地進行說明,假如有這樣一棵樹
A
/ | \
B C D
| | / \
E H J K
/ \ /|\
F G L M N
Eve: 然后這是這棵樹的列表形式表達
(def tree
'(A (B (E (F) (G))) (C (H)) (D (J) (K (L) (M) (N)))))
Pedro: 這括號數(shù)量有點夸張扒住骂铁!
Eve: 用來明確定義結(jié)構(gòu),你懂的罩抗。
Pedro: 但是這樣理解起來很困難啊拉庵。
Eve: 但適合機器識別,這里提供了一個十分酷炫的功能 tree-seq
澄暮,用來解析這顆樹名段。
(map first (tree-seq next rest tree)) => (A B E F G C H D J K L M N)
Eve: 如果你需要更強大的遍歷功能,可以試試 clojure.walk
Pedro: 我看不懂泣懊,這東西好像有點難伸辟。
Eve: 不用全部理解,你就只需了解用一種數(shù)據(jù)結(jié)構(gòu)就可以表示整棵數(shù)馍刮,一個函數(shù)就可以操作它信夫。
Pedro: 這個函數(shù)都會干點啥?
Eve: 它會遍歷這顆樹,然后把指定的函數(shù)作用于所有的節(jié)點静稻,在我們的例子里就是渲染每個塊警没。
Pedro: 我還是不懂,可能我還是太年輕了振湾,我們跳過樹的這個部分杀迹。
第十八集:工廠方法
Sir Dry Bang 提議要給他們熱賣的游戲增加新的關(guān)卡。關(guān)卡多多押搪,圈錢多多树酪。
Pedro: 我們要搞出來一個啥樣的新關(guān)卡?
Eve: 就簡單改一下道具資源然后加一點新的物體材質(zhì):紙大州,木頭续语,鐵……
Pedro: 這么做是不是有點腦殘?
Eve: 反正本身就是個腦殘游戲厦画。如果玩家愿意砸錢給他的游戲角色買個彩色帽子疮茄,那肯定也愿意買個木頭材質(zhì)的塊塊兒。
Pedro: 我也這么覺得根暑,不管咋說力试,先搞一個通用的 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: 這不是重點,重點是躏率,如果你想要生產(chǎn)其它材質(zhì)的方塊躯畴,只需要改變具體的生產(chǎn)工廠。
MazeBuilder builder = new WoodMazeBuilder();
Maze maze = builder.build();
Eve: 這好像和之前的哪個模式挺像的薇芝。
Pedro: 你說哪個蓬抄?
Eve: 我覺得像策略模式和狀態(tài)模式。
Pedro: 怎么可能夯到!策略模式是關(guān)于選擇哪一種合適的操作嚷缭,而工廠模式是為了生產(chǎn)適合的對象。
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: 對吧荐开。
Pedro: 有什么使用范例沒付翁?
Eve: 用不著,按照你的直覺來使用就行晃听,你可以回到上面再看一下 策略百侧、狀態(tài) 或 模板方法 這些章節(jié)砰识。
第十九集:抽象工廠
玩家不愿意購買游戲推出的新關(guān)卡。于是 Saimank Gerr 搭了一個反饋云平臺供玩家吐槽佣渴。根據(jù)反饋結(jié)果分析辫狼,出現(xiàn)最多的負面詞匯是:“丑”,“垃圾”辛润,“渣”膨处。
改進一下關(guān)卡構(gòu)建系統(tǒng)。
Pedro: 我就說了吧這是個垃圾游戲砂竖。
Eve: 是啊灵迫,雪地背景配木墻,太空侵入配木墻晦溪,啥都東西都搭配木制墻體是要鬧哪樣。
Pedro: 所以我們必須得把每關(guān)的游戲世界分離出來挣跋,然后再給每種世界分配一組具體的對象三圆。
Eve: 解釋一下。
Pedro: 我們不用以前構(gòu)建具體方塊的工廠方法了避咆,取而代之的是使用抽象工廠舟肉,以創(chuàng)建一組相關(guān)對象,這樣以來構(gòu)建關(guān)卡的方式看起來就不會那么糟糕了查库。
Eve: 舉個栗子路媚。
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)]整慎?我們給每個關(guān)卡都提供了具體的對象,現(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)卡工廠的實現(xiàn)類為各個關(guān)卡生產(chǎn)出相關(guān)的一組對象裤园。這樣肯定比以前的關(guān)卡好看。
Eve: 讓我冷靜一下剂府。我真的看不出這和工廠方法有啥區(qū)別拧揽。
Pedro: 工廠方法把創(chuàng)建對象推遲到子類,抽象工廠也一樣腺占,只不過創(chuàng)建的是一組相關(guān)對象 淤袜。
Eve: 啊哈,也就是說我需要一組相關(guān)的函數(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 舉辦了一場復古風格中世紀騎士對決彪笼。獎金高達 $100.000
我分你一半獎金,只要你能黑掉他的系統(tǒng)蚂且,允許我的武裝突擊隊加入比賽配猫。
Pedro: 終于,我們接到一個好玩的活了杏死。
Eve: 我非常期待這場比賽啊泵肄。尤其是 M16 對陣鐵劍的部分。
Pedro: 但是騎士們都穿著良好的盔甲啊淑翼。
Eve: F1 手榴彈根本不在乎 什么盔甲腐巢。
Pedro: 管他呢,只管干活拿錢玄括。
Eve: 五萬大洋冯丙,好價錢啊。
Pedro: 可不是嘛遭京,瞅瞅這個胃惜,我搞到了他們競賽系統(tǒng)的源碼,雖然我們不大可能直接修改源碼吧哪雕,但是說不準能找到一些漏洞蛹疯。
Eve: 我找到漏洞了
public interface Tournament {
void accept(Knight knight);
}
Pedro: 啊哈!系統(tǒng)只用了 Knight
做傳入?yún)?shù)檢查热监。 只需要把突擊隊員偽造 (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: 為了能傳入突擊隊員,我們先看看突擊隊員的原始實現(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: 那你是怎么把騎士替換成突擊隊員的呢?
Eve: 本質(zhì)上陌选,騎士是什么理郑?就是一個由數(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: 為了能適配突擊隊員,只需把原始的函數(shù)替換為突擊隊員的函數(shù)
{:name "Commando"
:speed 5.0
:attack-bow-fn (partial throw-grenade "F1")
:attack-sword-fn (partial shot "M16")
:block-fn nil}
Pedro: 我們怎么分贓分錢您炉?
Eve: 五五開柒爵。
Pedro: 我寫的代碼行多啊,我要七赚爵。
Eve: 行棉胀,七七開。
Pedro: 成交冀膝。
第二十一集:裝飾者
Podrea Vesper 抓到我們在比賽上作弊⊙渖荩現(xiàn)在有兩條路可以走:要么進局子,要么就幫他的超級騎士加入比賽窝剖。
Pedro: 我不想進監(jiān)獄麻掸。
Eve: 我也不想。
Pedro: 那我們就再幫他做一次弊吧赐纱。
Eve: 和上一次一樣蒜埋,是吧火俄?
Pedro 不完全是谱仪。突擊隊員是軍隊的人,本來是不允許參加比賽的呜舒。我們適配 (adapted) 了一下乡范。但是騎士本來就允許參加比賽瓶佳,不需要我們再改造了芋膘。我們必須 給現(xiàn)有的對象增加新的行為。
Eve: 繼承還是組合霸饲?
Pedro: 組合为朋,裝飾者模式的主要目的就是要在運行時改變行為。
Eve: 所以厚脉,我們要怎么造出一個超級騎士呢习寸?
Pedro: 他們計劃派出騎士 Galahad
,然后給他裝飾 一下傻工,讓他擁有超多血量 和強力盔甲 霞溪。
Eve: 嘿,這個條子竟然還玩兒輻射[1]呢中捆。
Pedro: 嗯哪威鹿,讓我們先寫一個抽象騎士類
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òu)造方法,默認無參的是標準行為臂容,decorated
參數(shù)表示需要裝飾的對象科雳。
Eve: 用類代替接口是不是因為類更直接一些根蟹?
Pedro: 不是因為這個,是因為這樣可以避免出現(xiàn)兩個功能相似的類糟秘,同時不必強制對象實現(xiàn)所有的方法简逮,因為我們給每個待裝飾對象提供了方法的默認實現(xiàn)。
Eve: 好吧尿赚,那強力的盔甲在哪里散庶?
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: 兩個裝飾者就可以滿足 FBI 的要求,然后我們就可以著手制造看起來和 Galahad 差不多凌净,但是擁有強力盔甲和額外 50 點血量的超級騎士了悲龟。
Knight superKnight =
new KnightWithAdditionalHP(
new KnightWithPowerArmor(
new Galahad()));
Eve: 這個特技加的可以。
Pedro: 接下來有請你來展示一下 Clojure 是怎么實現(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: 是的,這里要提一下斩芭,強力盔甲裝飾器是個亮點轻腺。
(譯注:亮點可能是使用了閉包。)
第二十二集:代理
Deren Bart 是一個調(diào)酒制造系統(tǒng)的管理員划乖。這個系統(tǒng)非常地死板難用贬养,因為每次調(diào)制完畢之后,Bart 都必須手動的從酒吧庫存中扣除已使用的原材料琴庵。把它改成自動的误算。
Pedro: 能搞到他代碼庫的權(quán)限么?
Eve: 不能细卧,但是他給了一些 API尉桩。
interface IBar {
void makeDrink(Drink drink);
}
interface Drink {
List<Ingredient> getIngredients();
}
interface Ingredient {
String getName();
double getAmount();
}
Pedro: Bart 不想讓我們修改源碼筒占,所以我們得通過實現(xiàn) IBar
接口來提供一些額外的功能 --- 自動扣除已用原料贪庙。
Eve: 怎么搞啊翰苫?
Pedro: 用代理模式 止邮,前幾天我還看這個模式來著。
Eve: 講給我聽聽唄奏窑。
Pedro: 基本思路就是导披,所有已有功能依然調(diào)用之前標準的 IBar
實現(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
實現(xiàn)類替換成我們的 ProxiedBar
埃唯。
Eve: 看起來超級簡單啊撩匕。
Pedro: 是的,額外加入的功能并不會破壞已有功能墨叛。
Eve: 你確定止毕?我們還沒有做回歸測試呢模蜡。
Pedro: 所有的功能都是委派給已經(jīng)通過測試的 StandardBar
去執(zhí)行的啊。
Eve: 但是同時你還調(diào)用了 BarDatabase
扣除了已用原材料啊扁凛。
Pedro: 我們可以認為他們是解耦的 (decoupled) 忍疾。
Eve: 哦……
Pedro: Clojure 里有什么替代方案么?
Eve: 這個谨朝,我也不清楚卤妒。在我看來你只是在用函數(shù)組合 (function composition)。
Pedro: 怎么說字币。
Eve: IBar
的實現(xiàn)類是一組函數(shù)则披,其它什么的各種 IBar
都不過是一組函數(shù)。你所謂的一切額外加入的功能都可以通過函數(shù)組合來實現(xiàn)纬朝。就好比在 make-drink
之后對酒吧庫存進行 subtract-ingredients
操作不就行了收叶。
Pedro: 可能用代碼描述會更清晰一點?
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ù)聚合在一個對象里判没。
Pedro: 看起來 Clojure 也有著面向?qū)ο蟮哪芰Π ?br>
Eve: 沒錯,不僅如此,我們還可以使用 reify
功能泄鹏,它可以允許我們在運行時創(chuàng)建代理馋没。
Pedro: 就好比在運行時創(chuàng)建類俏竞?
Eve: 差不多。
(reify IBar
(make-drink [this drink]
;; implementation goes here
))
Pedro: 感覺挺好用的堂竟。
Eve: 是啊魂毁,但是我還是沒有理解它和裝飾者的區(qū)別在哪。
Pedro: 完全不一樣啊出嘹。
Eve: 裝飾者給接口增加功能席楚,代理也是給接口增加功能。
Pedro: 好吧税稼,但是代理……
Eve: 甚至烦秩,適配器看起來也沒啥區(qū)別嘛。
Pedro: 適配器用了另一個接口郎仆。
Eve: 但是從實現(xiàn)的角度來說只祠,這些模式都是一樣的道理,把一些東西包裝起來扰肌,然后把調(diào)用委派給包裝者抛寝。我覺得叫它們“包裝者 (Wrapper)” 模式更好一些。
第二十三集:橋接
一位來自人力資源代理機構(gòu) "Hurece's Sour Man" 的女孩需要審核應(yīng)征者是否滿足職位要求。問題在于盗舰,一般來說工作崗位是顧客設(shè)計的猴凹,但是職位要求卻是人力部門設(shè)計的。給他們提供一個靈活的方式來協(xié)調(diào)這個問題岭皂。
(譯注:“工作崗位是顧客設(shè)計的”郊霎,人力資源代理機構(gòu)的顧客就是用人單位了。也就是說工作崗位是用人公司設(shè)計的爷绘,職位要求是人力資源代理機構(gòu)設(shè)計的书劝。)
Eve: 說實話我沒看明白這個問題。
Pedro: 我倒是有點這方面背景土至。他們的系統(tǒng)非常的奇怪购对,職位要求是用一個接口來描述的。
interface JobRequirement {
boolean accept(Candidate c);
}
Pedro: 通過實現(xiàn)這個接口陶因,來表示每一個具體的職位要求骡苞。
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: 我好像明白了點。
Pedro: 你要諒解楷扬,畢竟這個層次結(jié)構(gòu)是人力部設(shè)計的解幽。
Eve: 好的。
Pedro: 然后還有一個 Job
層級烘苹,用來描述崗位躲株,和職位要求一樣,每個具體崗位都要實現(xiàn) Job
镣衡。
Eve: 為啥他們要把每種崗位都用一個類來表示霜定?明明一個對象就可以了啊。
Pedro: 這個系統(tǒng)設(shè)計的時候就是類要比對象還多廊鸥,所以你就先湊合著望浩。
Eve: 類比對象還多?惰说!
Pedro: 是的磨德,好好聽別打岔。崗位和職位要求是兩個完全分離開的層級助被,而且崗位是由用人單位設(shè)計的∑收牛現(xiàn)在我們有請 橋接 (Bridge)
模式來關(guān)聯(lián)這兩個分離的層級切诀,并允許兩者繼續(xù)獨立運轉(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
是一個層級幅虑,是吧丰滑?
Eve: 是啊。
Pedro: Job
, CongnitectClojureDeveloperJob
, OracleJavaDeveloperJob
是另一個層級。
Eve: 哦褒墨,我明白了炫刷。職位和職位要求之間的聯(lián)系就是那個橋。
Pedro: 非常對郁妈!這樣以來人事部的人員就可以像這樣來進行審核了浑玛。
Candidate joshuaBloch = new Candidate();
(new CognitectClojureDeveloper()).accept(joshuaBloch);
(new OracleSeniorJavaDeveloper()).accept(joshuaBloch);
Pedro: 總結(jié)一下要點。用人單位使用抽象的 Job
以及 JobRequirement
的實現(xiàn)噩咪。他們只需要大概描述一下崗位的情況就行了顾彰,然后人力資源部門負責把描述轉(zhuǎn)換成一組 JobRequirement
對象。
Eve: 明白了胃碾。
Pedro: 據(jù)我了解涨享,Clojure 可以用 defprotocol
和 defrecord
來模擬這個模式?
Eve: 是的仆百,不過我想重溫一下這個問題厕隧。
Pedro: 為啥啊俄周?
Eve: 我們先整理一下套路:顧客描述崗位吁讨,人力資源部把它轉(zhuǎn)換成一組職位要求,然后在求職數(shù)據(jù)庫里跑一段腳本去逐一嘗試看沒有沒有符合要求的人員峦朗?
Pedro: 沒錯挡爵。
Eve: 所以這里還是存在依賴關(guān)系啊,沒有職位空缺的話 HR 啥也干不了甚垦。
Pedro: 這個茶鹃,算是吧。但是他們還是可以在沒有職位空缺的情況下設(shè)計出一組職位要求艰亮。
Eve: 目的何在闭翩?
Pedro: 提前搞出來,留著以后碰見一樣的要求就可以直接拿來用了啊迄埃。
Eve: 行吧疗韵,但是這不就是自找麻煩了。本來我們只是想要找到一種在抽象與實現(xiàn)之間協(xié)調(diào)的方式而已侄非。
Pedro: 也許吧蕉汪,我想看看你是怎么在 Clojure 里用橋接模式解決這個特定問題的。
Eve: 簡單逞怨。用專設(shè)層級 (adhoc hierarchies)者疤。
Pedro: 要給抽象設(shè)置層級?
Eve: 是的叠赦,崗位是抽象 層級驹马,然后我們只需要對其進行擴展。
;; abstraction
(derive ::clojure-job ::job)
(derive ::java-job ::job)
(derive ::senior-clojure-job ::clojure-job)
(derive ::senior-java-job ::java-job)
Eve: HR 部門就好比開發(fā)者 , 他們提供抽象的具體實現(xiàn)。
;; implementation
(defmulti accept :job)
(defmethod accept :java [candidate]
(and (some #{:java} (:skills candidate))
(> (:experience candidate) 1)))
Eve: 如果以后有新崗位出現(xiàn)糯累,但是崗位需求還沒有被確認算利,當然也沒有與之對應(yīng)的 accept
方法,這個時候就會回退到上個層級泳姐。
Pedro: 蛤效拭?
Eve: 假如某人創(chuàng)建了一個新的下屬于 ::java
崗位的 ::senior-java
崗位。
Pedro: 哦胖秒!如果 HR 沒有給委派值 ::senior-java
提供 accept
實現(xiàn)允耿,多重方法就會委派給 ::java
對應(yīng)的方法,對吧扒怖?
Eve: 小伙子學的挺快嘛较锡。
Pedro: 但是這還是橋接模式么?
Eve: 這里本來就沒有什么橋 盗痒,但是同樣得以讓抽象與實現(xiàn)可以獨立地運轉(zhuǎn)蚂蕴。
劇終。
速查表 (代替總結(jié))
模式非常難以理解俯邓,關(guān)于它們的介紹骡楼,通常都是使用面向?qū)ο蟮姆绞剑倥渖弦欢?UML 圖表和花哨的名詞稽鞭,而且還是為了解決特定語言下的問題鸟整,所以這里提供了一張迷你復習速查表,希望能用類比的方式幫助你理解模式的本質(zhì)朦蕴。
- 命令 (Command) - 函數(shù)
- 策略 (Strategy) - 接受函數(shù)的函數(shù)
- 狀態(tài) (State) - 依據(jù)狀態(tài)的策略
- 訪問者 (Visitor) - 多重分派
- 模板方法 (Template Method) - 默認策略
- 迭代器 (Iterator) - 序列
- 備忘錄 (Memento) - 保存和恢復
- 原型 (Prototype) - 不可變值
- 中介者 (Mediator) - 解耦和
- 觀察者 (Observer) - 在函數(shù)后調(diào)用函數(shù)
- 解釋器 (Interpreter) - 一組解析樹形結(jié)構(gòu)的函數(shù)
- 羽量 (Flyweight) - 緩存
- 建造者 (Builder) - 可選參數(shù)列表
- 外觀 (Facade) - 單一訪問點
- 單例 (Singleton) - 全局變量
- 責任鏈 (Chain of Responsibility) - 函數(shù)組合
- 組合 (Composite) - 樹形結(jié)構(gòu)
- 工廠方法 (Factory Method) - 制造對象的策略
- 抽象工廠 (Abstract Factory) - 制造一組相關(guān)對象的策略
- 適配 (Adapter) - 包裝篮条,功能相同,類型不同
- 裝飾者 (Decorator) - 包裝, 類型相同, 但增加了新功能
- 代理 (Proxy) - 包裝, 函數(shù)組合
- 橋接 (Bridge) - 分離抽象和實現(xià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. 從剛開始提筆距今早已超過 兩年疹娶。時間飛逝伴栓,物換星移,Java 8 也已經(jīng)發(fā)布了雨饺。
clojure
programming
java
story
patterns
18 December 2015