[譯] Clojure 中的設(shè)計模式(下)

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 可以用 defprotocoldefrecord 來模擬這個模式?
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


  1. 輻射 (Fallout) 是 Bethesda 出品的游戲钳垮。在游戲中有一種可駕駛的重型盔甲機器人。 ?

  2. A long time ago in a galaxy far, far away... 出自電影《星球大戰(zhàn)》额港。 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末饺窿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子锹安,更是在濱河造成了極大的恐慌短荐,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叹哭,死亡現(xiàn)場離奇詭異忍宋,居然都是意外死亡,警方通過查閱死者的電腦和手機风罩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門糠排,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人超升,你說我怎么就攤上這事入宦。” “怎么了室琢?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵乾闰,是天一觀的道長。 經(jīng)常有香客問我盈滴,道長涯肩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任巢钓,我火速辦了婚禮病苗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘症汹。我一直安慰自己硫朦,他們只是感情好,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布背镇。 她就那樣靜靜地躺著咬展,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瞒斩。 梳的紋絲不亂的頭發(fā)上挚赊,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機與錄音济瓢,去河邊找鬼荠割。 笑死,一個胖子當著我的面吹牛旺矾,可吹牛的內(nèi)容都是我干的蔑鹦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼箕宙,長吁一口氣:“原來是場噩夢啊……” “哼嚎朽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起柬帕,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤哟忍,失蹤者是張志新(化名)和其女友劉穎狡门,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锅很,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡其馏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了爆安。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叛复。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扔仓,靈堂內(nèi)的尸體忽然破棺而出褐奥,到底是詐尸還是另有隱情,我是刑警寧澤翘簇,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布撬码,位于F島的核電站,受9級特大地震影響版保,放射性物質(zhì)發(fā)生泄漏耍群。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一找筝、第九天 我趴在偏房一處隱蔽的房頂上張望蹈垢。 院中可真熱鬧,春花似錦袖裕、人聲如沸曹抬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谤民。三九已至,卻和暖如春疾宏,著一層夾襖步出監(jiān)牢的瞬間张足,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工坎藐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留为牍,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓岩馍,卻偏偏與公主長得像碉咆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蛀恩,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容