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

Clojure 設(shè)計模式 Design Patterns


譯自 Clojure Design Patterns
Author: Mykhailo Kozik

快速 了解如何在 Clojure 中運(yùn)用最基本的設(shè)計模式

免責(zé)聲明 絕大多數(shù)模式非常易于實(shí)現(xiàn),因?yàn)槲覀兛梢允褂脛討B(tài)類型、函數(shù)式和……嗯,和 Clojure。 文中的個別模式實(shí)現(xiàn)看起來很爛。好吧我承認(rèn),這里寫的每個字兒都有可能出錯,有時候老司機(jī)也難免翻車。

(譯注:譯者水平有限,使用本文代碼所導(dǎo)致的損失概不負(fù)責(zé)。
如果你在文中發(fā)現(xiàn)錯誤,或有任何意見或建議,請直接在文后留言。2333)


引子

我們所使用的語言本身已經(jīng)完?duì)僮恿?。所以我們搞出來一個叫設(shè)計模式的東西。
--- 尼古拉斯?趙四

這里有兩位謙遜的程序猿 --- Pedro VeelEve Dopler 正在運(yùn)用設(shè)計模式,來著手解決一些通用而常見的軟件工程問題。

第一集:命令

IT 外包商大佬 "Serpent Hill & R.E.E" 新接了一個來自美國的大單子。這次的首要交付任務(wù)是完成該品牌官網(wǎng)的用戶注冊、登錄、注銷功能。

Pedro: 這還不簡單,只需要搞一個像這樣的 Command 接口……

interface Command {
  void execute();
}

Pedro: 然后讓每個具體的功能去實(shí)現(xiàn)這個接口,定義自己的 execute 行為。

public class LoginCommand implements Command {

  private String user;
  private String password;

  public LoginCommand(String user, String password) {
    this.user = user;
    this.password = password;
  }

  @Override
  public void execute() {
    DB.login(user, password);
  }
}
public class LogoutCommand implements Command {

  private String user;

  public LogoutCommand(String user) {
    this.user = user;
  }

  @Override
  public void execute() {
    DB.logout(user);
  }
}

Pedro: 用起來也很容易。

(new LoginCommand("django", "unCh@1ned")).execute();
(new LogoutCommand("django")).execute();

Pedro: Eve 你瞅瞅咋樣?
Eve: 為啥你費(fèi)那么大老勁在 LoginCommand 里面包裹了一層,為啥不直接調(diào)用 DB.login
Pedro: 這個包裹可重要了,因?yàn)檫@樣就可以用同一個方法來操作任意實(shí)現(xiàn) Command 的對象了。
Eve: 有啥子用呢?
Pedro: 延時調(diào)用、登陸、歷史跟蹤、緩存……等等一火車應(yīng)用。
Eve: 好吧,那你看這樣搞行么?

(defn execute [command]
  (command))

(execute #(db/login "django" "unCh@1ned"))
(execute #(db/logout "django"))

Pedro: 這?是什么亂七八糟的東西?
Eve: 給出一個 Java 近似版本。

new SomeInterfaceWithOneMethod() {
  @Override
  public void execute() {
    // do
  }
};

Pedro:Command 接口差不多一個意思嘛……
Eve: 還有可以有一個你想要的不太“混亂”的版本。

(defn execute [command & args]
  (apply command args))

(execute db/login "django" "unCh@1ned")

Pedro: 那你怎么延后方法執(zhí)行做延時調(diào)用呢?
(譯注:延時調(diào)用指的是先進(jìn)行參數(shù)設(shè)置,最后再調(diào)用方法的意思么?求教。)
Eve: 你自己琢磨一下。當(dāng)你需要函數(shù)執(zhí)行的時候,只需準(zhǔn)備好什么呢?
Pedro: 方法名……
Eve: 還有?
Pedro: ……參數(shù)。
Eve: Bingo。你只需記得 (函數(shù)名, 參數(shù)列表) ,就可以在任何你需要的位置像這樣來讓函數(shù)執(zhí)行 (apply function-name arguments)。
Pedro: 嗯…… 看起來好像挺簡單的樣子。
Eve: 那可不, 命令模式只需一個函數(shù)而已

第二集:策略

Sven Tori 花大價錢雇人制作一張用戶表單。但是有幾個要求,用戶必須按照姓名排序,而且呢,會員用戶必須排在所有普通用戶之。這不廢話么,因?yàn)槿思姨湾X了。倒序排序依然要保持付費(fèi)用戶在上面。

Pedro: 蛤,自定義一個比較器,再調(diào)用一下 Collections.sort(users, comparator) 就能搞定了。
Eve: 那要怎么搞才能實(shí)現(xiàn) 自定義比較器 呢?
Pedro: 首先要實(shí)現(xiàn) Comparator 接口、實(shí)現(xiàn) compare(Object o1, Object o2) 方法。然后反序比較器 ReverseComparator 也需要類似的步驟來搞一哈。
Eve: 停!憋說話給我看代碼!

class SubsComparator implements Comparator<User> {

  @Override
  public int compare(User u1, User u2) {
    if (u1.isSubscription() == u2.isSubscription()) {
      return u1.getName().compareTo(u2.getName());
    } else if (u1.isSubscription()) {
      return -1;
    } else {
      return 1;
    }
  }
}

class ReverseSubsComparator implements Comparator<User> {

  @Override
  public int compare(User u1, User u2) {
    if (u1.isSubscription() == u2.isSubscription()) {
      return u2.getName().compareTo(u1.getName());
    } else if (u1.isSubscription()) {
      return -1;
    } else {
      return 1;
    }
  }
}

// forward sort
Collections.sort(users, new SubsComparator());

// reverse sort
Collections.sort(users, new ReverseSubsComparator());

Pedro: 你能搞一個類似的功能出來么?
Eve: 當(dāng)然,差不多像是這樣。

(sort (comparator 
       (fn [u1 u2]
         (cond
          (= (:subscription u1) (:subscription u2)) 
                (neg? (compare (:name u1) (:name u2)))
          (:subscription u1)
                true
          :else
                false)))
        users)

Pedro: 和我寫的挺像的。
Eve: 不過我還有一個改進(jìn)版。

;; forward sort
(sort-by (juxt (complement :subscription) :name) users)

;; reverse sort
(sort-by (juxt :subscription :name) #(compare %2 %1) users)

Pedro: 哦我的⑦舅老爺哦,這什么可怕的一行代碼。
Eve: 函數(shù),你懂的。
Pedro: 管他什么鬼,總之這也太難理解了吧。

Eve 正在解釋 juxt、complement 和 sort-by 函數(shù)的功能
10 分鐘后

Pedro: 這真的是一種非常玄學(xué)的策略模式實(shí)現(xiàn)。
Eve: 反正對我來說,實(shí)現(xiàn)策略模式只需 函數(shù)傳遞與組合。

第三集:狀態(tài)

銷售員 Karmen Git 調(diào)查了市場情況之后,決定要給不同用戶提供專屬功能。

Pedro: 很合理的需求嘛。
Eve: 我們來仔細(xì)研究一下。

  • 如果是用戶是付費(fèi)用戶,則可以看到所有的消息記錄。
  • 普通用戶則只能看到最近的 10 條消息。
  • 如果用戶進(jìn)行了充值,要記錄用戶當(dāng)前總余額。
  • 如果普通用戶當(dāng)前總余額已經(jīng)足夠購買會員,那就讓他(自動)升級為……

Pedro: 狀態(tài)!這模式特別帶勁。首先我們要搞一個表示用戶狀態(tài)的枚舉。

public enum UserState {
  SUBSCRIPTION(Integer.MAX_VALUE),
  NO_SUBSCRIPTION(10);

  private int newsLimit;

  UserState(int newsLimit) {
    this.newsLimit = newsLimit;
  }

  public int getNewsLimit() {
    return newsLimit;
  }
}

Pedro: 接下來是寫用戶邏輯部分。

public class User {
  private int money = 0;
  private UserState state = UserState.NO_SUBSCRIPTION;
  private final static int SUBSCRIPTION_COST = 30;

  public List<News> newsFeed() {
    return DB.getNews(state.getNewsLimit());
  }

  public void pay(int money) {
    this.money += money;
    if (state == UserState.NO_SUBSCRIPTION
        && this.money >= SUBSCRIPTION_COST) {
      // buy subscription
      state = UserState.SUBSCRIPTION;
      this.money -= SUBSCRIPTION_COST;
    }
  }
}

Pedro: 開始調(diào)用吧。

User user = new User(); // create default user
user.newsFeed(); // show him top 10 news
user.pay(10); // balance changed, not enough for subs
user.newsFeed(); // still top 10
user.pay(25); // balance enough to apply subscription
user.newsFeed(); // show him all news

Eve: 你就是把有關(guān)于那些值的邏輯藏在 User 類里面而已。我們可以直接像這樣使用策略模式啊 user.newsFeed(subscriptionType)。
Pedro: 同意。狀態(tài)和策略非常相似。甚至連它倆的 UML 表示形式都是一樣的。 但是我們把余額信息封裝了起來,這樣用戶接觸不到啊。
Eve: 我覺得用另一套方案也能實(shí)現(xiàn)相同的功能。無需顯式地說明使用哪個策略,而是可以依據(jù)某些狀態(tài)來決定所使用的策略。在 Clojure 里面,這東西和策略模式做的事兒差不多。
Pedro: 但是(在狀態(tài)模式里)如果成功調(diào)用,是可以改變對象的狀態(tài)哦。
Eve: 話是這樣沒錯,不過這和是不是策略模式?jīng)]啥關(guān)系吧,只是細(xì)節(jié)實(shí)現(xiàn)有些不同而已。
Pedro: 話說你剛才說的 "另一種方案" 是個啥子?
Eve: 多重方法。
Pedro: 多重 啥子?
Eve: 看這個:

(defmulti news-feed :user-state)

(defmethod news-feed :subscription [user]
  (db/news-feed))

(defmethod news-feed :no-subscription [user]
  (take 10 (db/news-feed)))

Eve: 這里 pay 函數(shù)的任務(wù)就是改變對象的狀態(tài)。雖然 Clojure 不喜歡修改對象的狀態(tài),但是非要改的話還是可以的。

(def user (atom {:name "Jackie Brown"
                 :balance 0
                 :user-state :no-subscription}))

(def ^:const SUBSCRIPTION_COST 30)

(defn pay [user amount]
  (swap! user update-in [:balance] + amount)
  (when (and (>= (:balance @user) SUBSCRIPTION_COST)
             (= :no-subscription (:user-state @user)))
    (swap! user assoc :user-state :subscription)
    (swap! user update-in [:balance] - SUBSCRIPTION_COST)))

(news-feed @user) ;; top 10
(pay user 10)
(news-feed @user) ;; top 10
(pay user 25)
(news-feed @user) ;; all news

Pedro: 使用多重方法來轉(zhuǎn)發(fā),比使用枚舉更好么?
Eve: 也許在上面的例子中并不是,不過通常來說用多重方法更好。
Pedro: 為啥,給解釋一下。
Eve: 你知道啥是 雙重分派 么?
(譯注:雙重分派* 原文為 double dispatch,也譯為雙重轉(zhuǎn)發(fā),雙重分發(fā)。)*
Pedro: 不造啊。
Eve: 好吧,講到訪問者模式的時候再說吧。

第四集:訪問者

Natanius S. Selbys 想要搞一個可以讓用戶以不同格式導(dǎo)出他們的消息、活動和成就的功能。

Eve: 所以這次你又有什么計劃?
Pedro: 我們可以先整一個 item 類型,包括 (消息,活動),然后再給文件格式比如 (PDF, XML) 搞一套。
(譯注:這里 item 類型沒有提到“成就”,可能是原作者遺落了。)

abstract class Format { }
class PDF extends Format { }
class XML extends Format { }

public abstract class Item {
  void export(Format f) {
    throw new UnknownFormatException(f);
  }
  abstract void export(PDF pdf);
  abstract void export(XML xml);
}

class Message extends Item {
  @Override
  void export(PDF f) {
    PDFExporter.export(this);
  }

  @Override
  void export(XML xml) {
    XMLExporter.export(this);
  }
}

class Activity extends Item {
  @Override
  void export(PDF pdf) {
    PDFExporter.export(this);
  }

  @Override
  void export(XML xml) {
    XMLExporter.export(this);
  }
}

Pedro: 大功告成。
Eve: 還不錯,不過你怎么處理參數(shù)類型的分發(fā)呢?
Pedro: 啥子意思?
Eve: 瞅一下這樣一段代碼:

Item i = new Activity();
Format f = new PDF();
i.export(f);

Pedro: 沒瞅出來啥毛病啊。
Eve: 其實(shí),如果執(zhí)行這段代碼會產(chǎn)生 UnknownFormatException。
Pedro: 蛤?真的?!
Eve: 在 Java 里只有 單一分派。這也就是說,如果你調(diào)用 i.export(f),只有 i 能被分派到具體實(shí)現(xiàn)類,而 f 無法被找到具體的實(shí)現(xiàn)類別。
(譯注1:C++ / Java / C# 等都只支持單一分派,也就是 i 的分派,也就是我們熟悉的 多態(tài) 概念。如在上面的例子中,選擇使用哪個 export 方法,取決于 i 的運(yùn)行時類型。而雙重分派不僅根據(jù) i 的運(yùn)行時類型,同時還取決于參數(shù) f 的運(yùn)行時類型。訪問者模式實(shí)際上提供了對于支持單分派語言的雙分派策略。
(譯注2:關(guān)于單一分派、雙重分派與訪問者模式的更多細(xì)節(jié)可以閱讀一下這篇文章。)
Pedro: 我懵逼了。所以你的意思是說,這里沒有根據(jù)參數(shù)類型進(jìn)行分派?
Eve: 這時候就需要祭出訪問者模式了。在依據(jù) i 分派之后,緊接著手工使用 f.someMethod(i) 進(jìn)行 f 的分派。
Pedro: 代碼長啥樣給看看唄。
Eve: 你需要在 Visitor 里給每一種類型都定義自己的導(dǎo)出操作。

public interface Visitor {
  void visit(Activity a);
  void visit(Message m);
}

public class PDFVisitor implements Visitor {
  @Override
  public void visit(Activity a) {
    PDFExporter.export(a);
  }

  @Override
  public void visit(Message m) {
    PDFExporter.export(m);
  }
}

Eve: 改造一下剛才的 Item 讓它可以接收各種 Visitor 實(shí)現(xiàn)。

public abstract class Item {
  abstract void accept(Visitor v);
}

class Message extends Item {
  @Override
  void accept(Visitor v) {
    v.visit(this);
  }
}

class Activity extends Item {
  @Override
  void accept(Visitor v) {
    v.visit(this);
  }
}

Eve: 像這樣調(diào)用就可以了。

Item i = new Message();
Visitor v = new PDFVisitor(); 
i.accept(v);

Eve: 運(yùn)轉(zhuǎn)良好。你甚至無需修改 MessageActivity 的代碼,就可以增加新的導(dǎo)出格式。只需增加新的訪問者即可。
Pedro: 這玩意兒挺實(shí)用的。就是實(shí)現(xiàn)起來有點(diǎn)復(fù)雜啊。 用 Clojure 實(shí)現(xiàn)這個是不是也很復(fù)雜?。?br> Eve: 并不復(fù)雜。因?yàn)?Clojure 使用多重方法來原生支持雙重分派。
Pedro: 多重 啥子 ?
Eve: 不解釋,看代碼……首先我們定義一個分派函數(shù)。
(譯注:分派 原文 dispatcher,也譯為 轉(zhuǎn)發(fā)。)

(defmulti export (fn [item format] [(:type item) format]))

Eve: 它依據(jù)接受的 itemformat 進(jìn)行分派。itemformat 的格式如下:

;; Message
{:type :message :content "Say what again!"}
;; Activity
{:type :activity :content "Quoting Ezekiel 25:17"}
;; Formats
:pdf, :xml

Eve: 現(xiàn)在你只需提供一系列函數(shù),來接收各種不同的分派,分派器會自動決定使用最合適的函數(shù)進(jìn)行處理。

(defmethod export [:activity :pdf] [item format]
  (exporter/activity->pdf item))

(defmethod export [:activity :xml] [item format]
  (exporter/activity->xml item))

(defmethod export [:message :pdf] [item format]
  (exporter/message->pdf item))

(defmethod export [:message :xml] [item format]
  (exporter/message->xml item))

Pedro: 如果遇見未知的導(dǎo)出格式該怎么進(jìn)行處理呢?
Eve: 我們可以定義默認(rèn)情況下的分派處理。

(defmethod export :default [item format]
  (throw (IllegalArgumentException. "not supported")))

Pedro: 好吧,但是 :pdf:xml 沒有層次繼承什么之類的關(guān)系啊。就只是關(guān)鍵字而已?
Eve: 答對了,簡單問題簡單處理嘛。如果你的確需要高級特性,可以使用專設(shè)層級 或者依據(jù) class 進(jìn)行轉(zhuǎn)發(fā)。
(譯注:專設(shè)層級 原文 adhoc hierarchies。暫未發(fā)現(xiàn)準(zhǔn)確的翻譯。)

(derive ::pdf ::format)
(derive ::xml ::format)

Pedro: 雙重冒號?!
Eve: 你就假裝它就是個關(guān)鍵字。
Pedro: 好吧就當(dāng)是吧。
Eve: 接下來就可以把接收分派的類型換成 ::pdf::xml 或者 ::format 了。

(defmethod export [:activity ::pdf])
(defmethod export [:activity ::xml])
(defmethod export [:activity ::format])

Eve: 如果系統(tǒng)中出現(xiàn)了新的格式(比如 csv):

(derive ::csv ::format)

Eve: 接受 ::csv 的函數(shù)還沒有出現(xiàn)時,:csv 會被分派到接受 ::format 的函數(shù)那里。
Pedro: 看起來挺棒的。
Eve: 那可不,簡單多了。
Pedro: 所以也就是說,如果語言本身支持多重分派,就無需訪問者模式?
Eve: 完全正確。

第五集:模板方法

MMORPG 游戲 機(jī)械多米諾爾大戰(zhàn)撒加 需要給 VIP 玩家們(單獨(dú))調(diào)整電腦難度。破壞平衡性。

Pedro: 首先,我們要搞清楚自動機(jī)器角色都應(yīng)該有些什么行為。
Eve: 你以前玩過 RPG 游戲沒?
Pedro: 很慶幸,沒。
Eve: 我類乖……來,讓你長長見識……

兩星期后

Pedro: ……我去,我剛找到一把 +100 攻擊力的史詩級大保健大寶劍。
Eve: 這么叼。不過……該起來干活了。
Pedro: 好啦好啦,淡定,這還不手到擒來。我們應(yīng)該實(shí)現(xiàn)這些事件:

  • 戰(zhàn)斗
  • 探索
  • 開寶箱

Pedro: 不同的人物會根據(jù)不同的事件作出不同的行為,比如法師在戰(zhàn)斗中喜歡使用遠(yuǎn)程法術(shù),但是盜賊更偏愛安靜地進(jìn)行近戰(zhàn)刺殺;絕大多數(shù)玩家對上鎖的箱子束手無策,但是盜賊卻可以打開它們,等等……
Eve: 看起來 模板方法 是個合適的選擇?
Pedro: 嗯呢。我們先定義抽象的規(guī)則,然后在子類里實(shí)現(xiàn)不同的具體方法。

public abstract class Character {
  void moveTo(Location loc) {
    if (loc.isQuestAvailable()) {
      Journal.addQuest(loc.getQuest());
    } else if (loc.containsChest()) {
      handleChest(loc.getChest());
    } else if (loc.hasEnemies()) {
      attack(loc.getEnemies());
    }
    moveTo(loc.getNextLocation());
  }

  private void handleChest(Chest chest) {
    if (!chest.isLocked()) {
      chest.open();
    } else {
      handleLockedChest(chest);
    }
  }

  abstract void handleLockedChest(Chest chest);
  abstract void attack(List<Enemy> enemies);
}

Pedro: 我們已經(jīng)分離出了 Character 類中所有通用的方法,提供給所有的角色。接下來就可以創(chuàng)造子類了,定義屬于他們自己的行為以適應(yīng)具體情景。在我們的游戲里面就是:處理上鎖的箱子攻擊敵人。
Eve: 那我們先來寫一個法師類吧。
Pedro: 法師?好的。首先他不能打開上鎖的箱子,所以重寫的方法里面啥都不干 就行了。然后是攻擊模式,如果遇見十個以上的敵人,就施放冰凍術(shù)把他們?nèi)珒鲎?,然后開傳送逃跑。如果遇見十個或者更少的敵人,就對敵人依次使用火球術(shù)。

public class MageCharacter extends Character {
  @Override
  void handleLockedChest(Chest chest) {
    // do nothing
  }

  @Override
  void attack(List<Enemy> enemies) {
    if (enemies.size() > 10) {
      castSpell("Freeze Nova");
      castSpell("Teleport");
    } else {
      for (Enemy e : enemies) {
        castSpell("Fireball", e);
      }
    }
  }
}

Eve: 感覺很不錯,盜賊類應(yīng)該怎么寫呢?
Pedro: 同樣很容易,盜賊可以開鎖,然后攻擊偏好是近距離暗殺,一個一個地做掉敵人。

public class RogueCharacter extends Character {
  @Override
  void handleLockedChest(Chest chest) {
    chest.unlock();
  }

  @Override
  void attack(List<Enemy> enemies) {
    for (Enemy e : enemies) {
      invisibility();
      attack("backstab", e);
    }
  }
}

Eve: 做的不錯。但是這個東西和策略模式有啥區(qū)別呢?
Pedro: 啥子意思?
Eve: 我的意思是說,你用子類來重新定義行為,但是策略模式也是重新定義行為啊,只不過是使用函數(shù)來實(shí)現(xiàn)的。
Pedro: 那個,那個的確是另一種實(shí)現(xiàn)方式。
Eve: 同理,狀態(tài)模式也是另一種實(shí)現(xiàn)方式咯。
Pedro: 你想表達(dá)什么?
Eve: 明明是同一類問題,你卻使用了不同的方式去解決。
Pedro: 那 Clojure 里是怎么用策略模式來解決這個游戲角色問題的?
Eve: 只需要通過給每個角色搞一些專屬函數(shù)。你看,你寫的抽象 move 就會變成像這個樣子:

(defn move-to [character location]
  (cond
   (quest? location)
   (journal/add-quest (:quest location))

   (chest? location)
   (handle-chest (:chest location))

   (enemies? location)
   (attack (:enemies location)))
  (move-to character (:next-location location)))

Eve: 角色需要實(shí)現(xiàn)函數(shù) handle-chestattack,然后把這兩個函數(shù)作為參數(shù)傳遞給 move-to

;; Mage-specific actions
(defn mage-handle-chest [chest])

(defn mage-attack [enemies]
  (if (> (count enemies) 10)
    (do (cast-spell "Freeze Nova")
        (cast-spell "Teleport"))
    ;; otherwise
    (doseq [e enemies]
      (cast-spell "Fireball" e))))

;; Signature of move-to will change to

(defn move-to [character location
               & {:keys [handle-chest attack]
                  :or {handle-chest (fn [chest])
                       attack (fn [enemies] (run-away))}}]
  ;; previous implementation
)

Pedro: 我的太上老君吶。這發(fā)生了什么?我要報警了。
Eve: 就是改了一下 move-to 所接受的參數(shù)啊,這樣就可以接受 handle-chestattack 函數(shù)了。
而且他們只是可選參數(shù)。

(move-to character location
  :handle-chest mage-handle-chest
  :attack       mage-attack)

Eve: 這里要提一下,如果沒有傳進(jìn)來這些函數(shù)的時候,會自動使用我們提供的默認(rèn)值:handle-chest 里面什么也不做,然后 attack 里面寫的是,見了敵人就跑。
Pedro: 好吧,但是好像用子類繼承更好一些吧?你看你這多次調(diào)用 move-to 的時候就會產(chǎn)生很多重復(fù)的代碼。
Eve: 這個可以改進(jìn),比如給它起個名,把它定義成一個函數(shù)。

(defn mage-move [character location]
  (move-to character location
    :handle-chest mage-handle-chest
    :attack       mage-attack))

Eve: 用多重方法也行,這樣更強(qiáng)大一些。

(defmulti move 
  (fn [character location] (:class character)))

(defmethod move :mage [character location]
  (move-to character location
    :handle-chest mage-handle-chest
    :attack       mage-attack))

Pedro: 我明白了,但是你為啥覺得這樣比使用子類繼承更好呢?
Eve: 因?yàn)檫@樣可以動態(tài)的改變他們的行為。假設(shè)你的法師魔法值耗盡了,就別扔火球了,他大可以開一個傳送門逃跑,只需提供一個新的函數(shù)就能實(shí)現(xiàn)了。
Pedro: 說的對啊。函數(shù)隨處可用。

第六集:迭代器

技術(shù)顧問 Kent Podiololis 正在吐槽 C 語言風(fēng)格的循環(huán)。
“活在 1980 年還是咋地?” --- Kent

Pedro: 肯定要用 Java 里面的迭代器模式啊。
Eve: 別犯傻了,根本沒人用 java.util.Iterator。
Pedro: 但是大家都在 for-each 循環(huán)里隱式地使用它啊。用它來遍歷容器感覺特別爽。
Eve: “遍歷容器 ”是個什么意思?
Pedro: 專業(yè)點(diǎn)來說就是,一個容器需要提供這兩個方法:
next(),用來返回下一個元素。hasNext(),如果容器中還存在元素就返回真。
Eve: 那個,你知道啥是鏈表么?
Pedro: 你說單鏈表?
Eve: 是的,單鏈表。
Pedro: 肯定知道啊。它也算一種容器,是由一系列節(jié)點(diǎn)組成的。每個節(jié)點(diǎn)包括數(shù)據(jù)部分和指向下一個節(jié)點(diǎn)的部分。如果是最后一個節(jié)點(diǎn),那么它下個節(jié)點(diǎn)就是 null。
Eve: 很懂行啊。那你給我說說遍歷鏈表和使用迭代器遍歷有啥區(qū)別?
Pedro: 呃……

Pedro 寫了兩段遍歷的代碼:

  • 使用迭代器遍歷
Iterator i;
while (i.hasNext()) {
  i.next();
}
  • 遍歷鏈表
Node next = root;
while (next != null) {
  next = next.next;
}

Pedro: 你別說還真是挺像的……那 Clojure 里面有什么類似 Iterator 的東西么?
Eve: seq 函數(shù)。

(seq [1 2 3])       => (1 2 3)
(seq (list 4 5 6))  => (4 5 6)
(seq #{7 8 9})      => (7 8 9)
(seq (int-array 3)) => (0 0 0)
(seq "abc")         => (\a \b \c)

Pedro: 它返回了一個列表……
Eve: 準(zhǔn)確來說是 序列,因?yàn)椋ㄔ?Clojure 里) 序列代替了迭代器。
Pedro: seq 可以操作自定義數(shù)據(jù)結(jié)構(gòu)么?
Eve: 實(shí)現(xiàn) clojure.lang.Seqable 接口就可以了:

(deftype RedGreenBlackTree [& elems]
  clojure.lang.Seqable
  (seq [self]
    ;; traverse element in needed order
    ))

Pedro: 好吧好吧。但是我聽說迭代器通常用來實(shí)現(xiàn)惰性,比如等到 getNext() 被調(diào)用的時候才會進(jìn)行求值,用列表能解決這類問題么?
Eve: 能啊,Clojure 里管它叫“惰性序列 ”。

(def natural-numbers (iterate inc 1))

Eve: 我們剛才定義了一個表示 全體 自然數(shù)的東西,但是并沒有 OutOfMemory,因?yàn)槲覀冞€沒有從里面取任何值,它是惰性的。
(譯注:0 是否屬于自然數(shù)仍有爭議。目前國際標(biāo)準(zhǔn)和中國國家標(biāo)準(zhǔn)都把 0 算作自然數(shù)。)
Pedro: 能仔細(xì)解釋一下么?
Eve: 對不起哦,我好像也 “惰性” 起來了。(跑咯)
Pedro: 你給我等著我記住你了!

第七集:備忘錄

一位名叫 Chad Bogue 的用戶丟失了他已經(jīng)寫了兩天的消息。給他一個保存按鈕吧。

Pedro: 我簡直不敢相信有人會在那個輸入框里面打字打了兩天,整整兩天!
Eve: 讓我們來拯救 他吧。
(譯注:拯救 原文 save,有保存之意。雙關(guān)。)
Pedro: 我剛才在 Google [1] 上查了一下。實(shí)現(xiàn)保存按鈕的通常做法是使用備忘錄模式。 需要三個東西,
創(chuàng)作者 (originator)
,管理者 (caretaker),備忘錄 (memento)。
Eve: 這些都是干啥用的?
Pedro: 創(chuàng)作者 就是我們需要保存的對象或者狀態(tài)(例如輸入框里面的文本就是創(chuàng)作者),管理者 的功能就是保存需要保存的狀態(tài)(例如那個保存按鈕就是管理者),最后 備忘錄 就是用來存儲狀態(tài)的對象。

public class TextBox {
  // state for memento
  private String text = "";

  // state not handled by memento
  private int width = 100;
  private Color textColor = Color.BLACK;

  public void type(String s) {
    text += s;
  }

  public Memento save() {
    return new Memento(text);
  }

  public void restore(Memento m) {
    this.text = m.getText();
  }

  @Override
  public String toString() {
    return "[" + text + "]";
  }
}

Pedro: 備忘錄是一個不可變的對象。

public final class Memento {
  private final String text;

  public Memento(String text) {
    this.text = text;
  }

  public String getText() {
    return text;
  }
}

Pedro: 管家就是這樣一段代碼:

// open browser, init empty textbox
TextBox textbox = new TextBox();

// type something into it
textbox.type("Dear, Madonna\n");
textbox.type("Let me tell you what ");

// press button save
Memento checkpoint1 = textbox.save();

// type again
textbox.type("song 'Like A Virgin' is about. ");
textbox.type("It's all about a girl...");

// suddenly browser crashed, restart it, reinit textbox
textbox = new TextBox();

// but it's empty! All work is gone!
// not really, you rollback to last checkpoint
textbox.restore(checkpoint1);

Pedro: 這里要提個醒,如果你想要保存多次記錄,那就建立一個備忘錄列表。
Eve: 作家, 管家, 備忘 - 這么些專業(yè)詞匯,其實(shí)本質(zhì)上是為了實(shí)現(xiàn) saverestore 這兩個功能。

(def textbox (atom {}))

(defn init-textbox [] 
 (reset! textbox {:text ""
                  :color :BLACK
                  :width 100}))

(def memento (atom nil))

(defn type-text [text]
  (swap! textbox
    (fn [m]
      (update-in m [:text] (fn [s] (str s text))))))

(defn save []
  (reset! memento (:text @textbox)))

(defn restore []
  (swap! textbox assoc :text @memento))

Eve: 這是測試代碼:

(init-textbox)
(type-text "'Like A Virgin' ")
(type-text "it's not about this sensitive girl ")
(save)
(type-text "who meets nice fella")
;; crash
(init-textbox)
(restore)

Pedro: 這和我的寫的基本上差不多啊。
Eve: 但是你必須小心備忘錄的不變性。
Pedro: 啥子意思?
Eve: 幸好這個例子里使用的是 String 類型,String 是不可變的。但是如果你還有一些內(nèi)部狀態(tài)可能會發(fā)生改變的對象,你就必須對這些備忘錄對象進(jìn)行深層克隆了。
Pedro: 哦,謝謝提醒。所以這里還需要對得到的原型遞歸使用 clone() 方法。
Eve: 過一會兒我們就會見到原型模式,但是一定要搞清楚,備忘錄模式 的本質(zhì)不是 管理者創(chuàng)作者,而是 保存恢復(fù)。

第八集:原型

經(jīng)過分析之后 Dex Ringeus 發(fā)現(xiàn),用戶并不喜歡填寫登記表。要想辦法提升易用性才行。

Pedro: 所以,那個登記表問題出在哪里?
Eve: 因?yàn)闊┤说谋眄?xiàng)實(shí)在是太多了啊。
Pedro: 比如說?
Eve: 比如說,體重。這項(xiàng)嚇跑了 90% 的女性用戶。
Pedro: 但是這項(xiàng)對我們的分析系統(tǒng)來說很重要啊,推薦食品和衣服的時候要用到這一項(xiàng)。
Eve: 那就,把它改成非必填項(xiàng)吧,如果用戶沒有填這一項(xiàng),就隨便取個默認(rèn)值。
Pedro: 60 千克 咋樣。
Eve: 行。
Pedro: 好的,給我兩分鐘。

兩小時后

Pedro: 我建議先建立一張*原型 *登記表,所有表項(xiàng)都預(yù)先填上默認(rèn)值。等用戶把內(nèi)容填進(jìn)來的時候我們再去改這些值。
Eve: 不錯的建議。
Pedro: 這里就是我們的標(biāo)準(zhǔn)注冊表原型了,它實(shí)現(xiàn)了 clone() 方法:

public class RegistrationForm implements Cloneable {
  private String name = "Zed";
  private String email = "zzzed@gmail.com";
  private Date dateOfBirth = new Date(1970, 1, 1);
  private int weight = 60;
  private Gender gender = Gender.MALE;
  private Status status = Status.SINGLE;
  private List<Child> children = Arrays.asList(new Child(Gender.FEMALE));
  private double monthSalary = 1000;
  private List<Brand> favouriteBrands = Arrays.asList("Adidas", "GAP");
  // few hundreds more properties

  @Override
  protected RegistrationForm clone() throws CloneNotSupportedException {
    RegistrationForm prototyped = new RegistrationForm();
      prototyped.name = name;
      prototyped.email = email;
      prototyped.dateOfBirth = (Date)dateOfBirth.clone();
      prototyped.weight = weight;
      prototyped.status = status;
      List<Child> childrenCopy = new ArrayList<Child>();
      for (Child c : children) {
        childrenCopy.add(c.clone());
      }
      prototyped.children = childrenCopy;
      prototyped.monthSalary = monthSalary;
      List<String> brandsCopy = new ArrayList<String>();
      for (String s : favouriteBrands) {
        brandsCopy.add(s);
      }
      prototyped.favouriteBrands = brandsCopy;
    return  prototyped;
  }
}

Pedro: 需要創(chuàng)建一個新表格的時候,調(diào)用 clone() 就可以得到一個同樣的表格了,然后就可以對這個新表格進(jìn)行修改了。
Eve: 哎呦我去太嚇人了!在可變的世界里,想復(fù)制一個對象就必須依賴 clone() 方法。之所以嚇人就在于復(fù)制必須要足夠深,也就是說,如果你想復(fù)制一個引用,就必須遞歸地調(diào)用 clone(),萬一引用對象不支持 clone()……
Pedro: 這個模式就是用來解決這個問題的。
Eve: 我不覺得每次加入新對象都必須費(fèi)老勁實(shí)現(xiàn)新對象的 clone 方法是一個很好的解決方案。
Pedro: 那 Clojure 有啥靈丹妙藥么?
Eve: Clojure 的數(shù)據(jù)結(jié)構(gòu)是不可變的。就這樣。
Pedro: 這樣就能解決原型問題了?
Eve: 每次修改數(shù)據(jù),你都會得到一個全新的不可變的原數(shù)據(jù)的拷貝,原來的數(shù)據(jù)不會發(fā)生任何變化。 不可變數(shù)據(jù)類型的世界里不需要原型模式

(def registration-prototype
     {:name          "Zed"
      :email         "zzzed@gmail.com"
      :date-of-birth "1970-01-01"
      :weight        60
      :gender        :male
      :status        :single
      :children      [{:gender :female}]
      :month-salary  1000
      :brands        ["Adidas" "GAP"]})

;; return new object
(assoc registration-prototype 
     :name "Mia Vallace"
     :email "tomato@gmail.com"
     :weight 52
     :gender :female
     :month-salary 0)

Pedro: 厲害了!但是這東西性能咋樣?復(fù)制上百萬行的數(shù)據(jù)也要返回一個全新的?好像要消耗巨大的運(yùn)算資源啊。
Eve: 并不是你想的那樣。你可以去搜一下可持久化數(shù)據(jù)結(jié)構(gòu) (persistent data structures)結(jié)構(gòu)共享 (structural sharing) 的相關(guān)資料。
Pedro: 謝了啊。

第九集:中介者

最近,公司對當(dāng)前代碼庫進(jìn)行了外部代碼審查,暴露出許多問題。Veerco Wierde 強(qiáng)調(diào),這個聊天應(yīng)用耦合度太高。

Eve: 耦合度太高是啥意思。
Pedro: 就是說,對象彼此之間的關(guān)系太過緊密。對象之間彼此知道的太多就會出問題。
Eve: 能詳細(xì)說明一下么?
Pedro: 直接看一下目前的聊天代碼實(shí)現(xiàn):

public class User {
  private String name;
  List<User> users = new ArrayList<User>();

  public User(String name) {
    this.name = name;
  }

  public void addUser(User u) {
    users.add(u);
  }

  void sendMessage(String message) {
    String text = String.format("%s: %s\n", name, message);
    for (User u : users) {
      u.receive(text);
    }
  }

  private void receive(String message) {
    // process message
  }
}

Pedro: 問題就在于用戶必須知道其它所有的用戶。這樣維護(hù)起來非常的麻煩。每當(dāng)有新的用戶加入聊天,你必須通過 addUser 方法給所有已存在的用戶的添加這個新用戶的引用。
Eve: 所以,我們就把這個添加新用戶的職能移動到另一個類里面?
Pedro: 是的,基本上就是這樣。我們創(chuàng)造一個*超限の覺醒 * 類,其名為終結(jié)者(誤)中介者,它把眾生綁定在一起。很顯然,這樣每個用戶只會感應(yīng)到中介者的存在。

public class User {
  String name;
  private Mediator m;

  public User(String name, Mediator m) {
    this.name = name;
    this.m = m;
  }

  public void sendMessage(String text) {
    m.sendMessage(this, text);
  }

  public void receive(String text) {
    // process message
  }
}

public class Mediator {

  List<User> users = new ArrayList<User>();

  public void addUser(User u) {
    users.add(u);
  }

  public void sendMessage(User u, String text) {
    for (User user : users) {
      u.receive(text);
    }
  }
}

Eve: 貌似就是簡單的重構(gòu)了一下啊。
Pedro: 看起來貌似沒啥改進(jìn),但是如果你有上百個組建需要互相關(guān)聯(lián)(比如 UI),偉大的救世主,中介者,就出現(xiàn)了。
Eve: 這倒是。
Pedro: 下面該 Clojure 出招了。
Eve: 行吧……我看看……你的中介者所擁有的能力就是保存用戶列表 和*發(fā)送消息 *。

(def mediator
  (atom {:users []
         :send (fn [users text]
                 (map #(receive % text) users))}))

(defn add-user [u]
    (swap! mediator 
      (fn [m]
        (update-in m [:users] conj u))))

(defn send-message [u text]
    (let [send-fn (:send @mediator)
          users (:users @mediator)]
      (send-fn users (format "%s: %s\n" (:name u) text))))

(add-user {:name "Mister White"})
(add-user {:name "Mister Pink"})
(send-message {:name "Joe"} "Toby?")

Pedro: 好了行了。
Eve: 不吹不黑,不就是 減小耦合 么,輕輕松松。

第十集:觀察者

經(jīng)探查,某第三方安全機(jī)構(gòu)在黑客 Dartee Hebl 賬戶上發(fā)現(xiàn)了高達(dá)數(shù)十億美元的不明資金。你的任務(wù)是追蹤這個賬戶的大額資金流動。

Pedro: 我們是福爾摩斯 么?
Eve: 不是,但是這個系統(tǒng)里并沒有日志記錄,所以要想個法子去追蹤這個賬戶上所有的資金流動。
Pedro: 我們需要增加點(diǎn)觀察者。每當(dāng)有資金變化,如果流動金額*足夠大 *,就發(fā)出通知,然后追蹤源頭。首先我們需要一個 Observer 接口:

public interface Observer {
  void notify(User u);
}

Pedro: 然后實(shí)現(xiàn)兩個具體觀察者。

class MailObserver implements Observer {
  @Override
  public void notify(User user) {
    MailService.sendToFBI(user);
  }
}

class BlockObserver implements Observer {
  @Override
  public void notify(User u) {
    DB.blockUser(u);
  }
}

Pedro: Tracker 類的職責(zé)就是用來管理這些觀察者。

public class Tracker {
  private Set<Observer> observers = new HashSet<Observer>();

  public void add(Observer o) {
    observers.add(o);
  }

  public void update(User u) {
    for (Observer o : observers) {
      o.notify(u);
    }
  }
}

Pedro: 最后的步驟就是:開啟賬戶追蹤,對 addMoney 方法做點(diǎn)手腳。如果賬戶的流動金額高于 100$,通知 FBI,凍結(jié)他的賬戶。

public class User {
  String name;
  double balance;
  Tracker tracker;

  public User() {
    initTracker();
  }

  private void initTracker() {
    tracker = new Tracker();
    tracker.add(new MailObserver());
    tracker.add(new BlockObserver());
  }


  public void addMoney(double amount) {
    balance += amount;
    if (amount > 100) {
      tracker.update(this);
    }
  }
}

Eve: 為啥你分別搞了兩個觀察者?我覺得一個就行了啊。

class MailAndBlock implements Observer {
  @Override
  public void notify(User u) {
    MailService.sendToFBI(u);
    DB.blockUser(u);
  }
}

Pedro: 單一職責(zé)原則。
Eve: 哦,對。
Pedro: 這樣就可以動態(tài)地對觀察者的功能進(jìn)行搭配組合了。
Eve: 我懂你的意思了。

;; Tracker

(def observers (atom #{}))

(defn add [observer]
  (swap! observers conj observer))

(defn notify [user]
  (map #(apply % user) @observers))

;; Fill Observers

(add (fn [u] (mail-service/send-to-fbi u)))
(add (fn [u] (db/block-user u)))

;; User

(defn add-money [user amount]
  (swap! user
    (fn [m]
      (update-in m [:balance] + amount)))
  ;; tracking
  (if (> amount 100) (notify)))

Pedro: 基本上沒看出差別???
Eve: 對啊,實(shí)際上觀察者就是把一些函數(shù)記錄下來,然后這些函數(shù)就可以等著被其它函數(shù)調(diào)用了。
Pedro: 這不還是一種模式啊。
Eve: 對,不過我們可以借助 Clojure 自帶的觀察者功能對其進(jìn)行進(jìn)一步地改進(jìn)。

(add-watch
  user
  :money-tracker 
  (fn [k r os ns] 
    (if (< 100 (- (:balance ns) (:balance os)))
      (notify))))

Pedro: 這樣寫有啥優(yōu)點(diǎn)呢。
Eve: 首先是,我們的 add-money 方法更干凈了,只負(fù)責(zé)增加金額。然后是,這種方式可以監(jiān)聽到*所有 * 的狀態(tài)改變,不僅僅是那個我們做了手腳的 add-money 方法。
Pedro: 解釋一下唄。
Eve: 假如這里提供了一個隱藏的秘密方法 secret-add-money 也可以改動資金,那么我這種觀察者一樣可以很好地處理它。
Pedro: 這個有點(diǎn)酷炫??!

第十一集:解釋器

Bertie Prayc 從我們的的服務(wù)器上偷走了重要的數(shù)據(jù),而且還做了 BT 種子上傳到了網(wǎng)上。搞一個叫 Bertie 的假帳戶整一下他。

Pedro: BT 系統(tǒng)建立在 .torrent 文件之上。我們需要進(jìn)行 Bencode 編碼。
Eve: 是的,不過我們首先要了解它的編碼格式

Bencode 編碼規(guī)范:

  • 支持以下兩種數(shù)據(jù)類型:

    • 整形 N 被編碼為 i<N>e。 (42 = i42e)
    • 字符串 S 被編碼為 <長度>:<內(nèi)容> (hello = 5:hello)
  • 支持以下兩種容器類型:

    • 列表類型被編碼為 l<內(nèi)容>e ([1, "Bye"] = li1e3:Byee)
    • 鍵值類型被編碼為 d<內(nèi)容>e ({"R" 2, "D" 2} = d1:Ri2e1:Di2ee)
      • 鍵必須是字符串,值可以是任何允許的 bencode 元素節(jié)點(diǎn)

Pedro: 看上去不難。
Eve: 但愿吧,考慮到值是可以進(jìn)行嵌套的,列表套列表之類的。
Pedro: 好的。我認(rèn)為我們可以使用*解釋器 *模式來對付 bencode 編碼問題。
Eve: 試試看。
Pedro: 我們先把所有 bencode 元素抽象為一個接口

interface BencodeElement {
  String interpret();
}

Pedro: 然后我們再依次搞出數(shù)據(jù)類型和容器類型的實(shí)現(xiàn)

class IntegerElement implements BencodeElement {
  private int value;

  public IntegerElement(int value) {
    this.value = value;
  }

  @Override
  public String interpret() {
    return "i" + value + "e";
  }
}

class StringElement implements BencodeElement {
  private String value;

  StringElement(String value) {
    this.value = value;
  }

  @Override
  public String interpret() {
    return value.length() + ":" + value;
  }
}

class ListElement implements BencodeElement {
  private List<? extends BencodeElement> list;

  ListElement(List<? extends BencodeElement> list) {
    this.list = list;
  }

  @Override
  public String interpret() {
    String content = "";
    for (BencodeElement e : list) {
      content += e.interpret();
    }
    return "l" + content + "e";
  }
}

class DictionaryElement implements BencodeElement {
  private Map<StringElement, BencodeElement> map;

  DictionaryElement(Map<StringElement, BencodeElement> map) {
    this.map = map;
  }

  @Override
  public String interpret() {
    String content = "";
    for (Map.Entry<StringElement, BencodeElement> kv : map.entrySet()) {
      content += kv.getKey().interpret() + kv.getValue().interpret();
    }
    return "d" + content + "e";
  }
}

Pedro: 最終,我們就可以使用平時用的數(shù)據(jù)結(jié)構(gòu)編寫程序來生成編碼后的字符串了。

// discredit user
Map<StringElement, BencodeElement> mainStructure = new HashMap<StringElement, BencodeElement>();
// our victim
mainStructure.put(new StringElement("user"), new StringElement("Bertie"));
// just downloads files
mainStructure.put(new StringElement("number_of_downloaded_torrents"), new IntegerElement(623));
// and nothing uploads
mainStructure.put(new StringElement("number_of_uploaded_torrents"), new IntegerElement(0));
// and nothing donates
mainStructure.put(new StringElement("donation_in_dollars"), new IntegerElement(0));
// prefer dirty categories
mainStructure.put(new StringElement("preffered_categories"),
                      new ListElement(Arrays.asList(
                          new StringElement("porn"),
                          new StringElement("murder"),
                          new StringElement("scala"),
                          new StringElement("pokemons")
                      )));
BencodeElement top = new DictionaryElement(mainStructure);

// let's totally discredit him
String bencodedString = top.interpret();
BitTorrent.send(bencodedString);

Eve: 很不錯哦,但是你這代碼量快要有一卡車了吧!
Pedro: 為了增強(qiáng)可讀性嘛。
Eve: 我覺得你應(yīng)該聽說過代碼即數(shù)據(jù),這在 Clojure 中特別容易實(shí)現(xiàn)

;; multimethod to handle bencode structure
(defmulti interpret class)

;; implementation of bencode handler for each type
(defmethod interpret java.lang.Long [n]
  (str "i" n "e"))

(defmethod interpret java.lang.String [s]
  (str (count s) ":" s))

(defmethod interpret clojure.lang.PersistentVector [v]
  (str "l" 
       (apply str (map interpret v))
       "e"))

(defmethod interpret clojure.lang.PersistentArrayMap [m]
  (str "d" 
       (apply str (map (fn [[k v]] 
                         (str (interpret k)
                              (interpret v))) m))
       "e"))

;; usage
(interpret {"user" "Bertie"
            "number_of_downloaded_torrents" 623
            "number_of_uploaded_torrent" 0
            "donation_in_dollars" 0
            "preffered_categories" ["porn"
                                    "murder"
                                    "scala"
                                    "pokemons"]})

Eve: 你瞅瞅使用 Clojure 定義一個特殊數(shù)據(jù)是多么的方便。
Pedro: 真的是啊,不同的 bencode 解釋器不過是一些函數(shù)而已,而不是一些類。
Eve: 回答正確,解釋器不過是一套用來處理樹形結(jié)構(gòu)的函數(shù)。

第十二集:羽量 (Flyweight)

某律師公司的管理員 Cristopher, Matton & Pharts 發(fā)現(xiàn),報表系統(tǒng)消耗了大量的內(nèi)存資源,導(dǎo)致垃圾處理程序不斷運(yùn)行,造成系統(tǒng)卡頓。修復(fù)這個問題。

Pedro: 我以前也遇見過這個問題。
Eve: 問題出在哪里呢?
Pedro: 這是個實(shí)時圖表系統(tǒng),里面有非常多的點(diǎn)。真的是占用了巨大的內(nèi)存空間。結(jié)果垃圾處理程序把系統(tǒng)拖垮了。
Eve: 嗯……那我們咋辦?
Pedro: 我也不造啊,緩存也派不上用場,因?yàn)楣?jié)點(diǎn)實(shí)在是太多了……
Eve: 等等!
Pedro: 咋了?
Eve: 這里的點(diǎn)會被重復(fù)使用多次,為什么我們不預(yù)先加載最經(jīng)常使用的點(diǎn)呢?比如 [0, 100] 范圍內(nèi)的。
Pedro: 你的意思是用*共享 *模式?
(譯注:共享 原文 Flyweight。直譯為 羽量。這個模式的思想是共享相同的元素,故又譯為 享元。)
Eve: 我的意思是復(fù)用對象。

class Point {
  int x;
  int y;

  /* some other properties*/

  // precompute 10000 point values at class loading time
  private static Point[][] CACHED;
  static {
    CACHED = new Point[100][];
    for (int i = 0; i < 100; i++) {
      CACHED[i] = new Point[100];
      for (int j = 0; j < 100; j++) {
        CACHED[i][j] = new Point(i, j);
      }
    }
  }

  Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  static Point makePoint(int x, int y) {
    if (x >= 0 && x < 100 &&
        y >= 0 && y < 100) {
      return CACHED[x][y];
    } else {
      return new Point(x, y);
    }
  }
}

Pedro: 這個模式的要點(diǎn)有兩個:一是在啟動的時候?qū)ψ畛S玫狞c(diǎn)進(jìn)行預(yù)加載,二是使用靜態(tài)工廠方法取代構(gòu)造方法,以便返回緩存的對象。
Eve: 這東西你測試過了?
Pedro: 肯定啊,系統(tǒng)像鐘表一樣精確運(yùn)行。
Eve: 你真厲害啊,來看看我寫的版本


(defn make-point [x y]
  [x y {:some "Important Properties"}])

(def CACHE
  (let [cache-keys (for [i (range 100) j (range 100)] [i j])]
      (zipmap cache-keys (map #(apply make-point %) cache-keys))))

(defn make-point-cached [x y]
  (let [result (get CACHE [x y])]
    (if result
      result
      (make-point x y))))

Eve: 我搞了一個關(guān)于 [x, y] 的扁平映射 (flat map) ,以取代二維數(shù)組。
Pedro: 沒啥區(qū)別啊。
Eve: 并不是,我這樣更靈活一些,你的二維數(shù)組并不能及時適應(yīng)三維的點(diǎn)或者非整型的點(diǎn)值。
Pedro: 哦,好吧。
Eve: 其實(shí)還能更簡單,在 Clojure 里面你可以很方便的使用 memoize 函數(shù)來給 make-point 函數(shù)增加緩存功能,這樣就可以替代手工的緩存工廠了。

(def make-point-memoize (memoize make-point))

Eve: 每次調(diào)用的時候(除了第一次),只要函數(shù)參數(shù)與之前的某次調(diào)用相同,就會返回上次緩存的值。
Pedro: 這個太牛了!
Eve: 那可不,不過需要注意的是,如果你的函數(shù)具有副作用,用緩存就不合適了。

第十三集:建造者 (Builder)

Tuck Brass 抱怨他的自動咖啡販賣機(jī)系統(tǒng)運(yùn)行起來實(shí)在是太慢了。顧客們根本沒有耐心等下去就走了。

Pedro: 首先要弄明白問題的真正原因。
Eve: 我已經(jīng)調(diào)查完畢,這是個上古系統(tǒng),竟然是用 COBOL 語言寫的,而且是建立在問-答 機(jī)制專家系統(tǒng)架構(gòu)上。這個機(jī)制在上古時期很流行的。
Pedro: “問-答” 機(jī)制是個啥?
Eve: 就好比有一個操作員坐在電腦終端面前。系統(tǒng)問:
“要加點(diǎn)水么?”
,操作員回答:“對 ”。系統(tǒng)又問:“要加點(diǎn)咖啡么?”,操作員回答:“對 ” 然后巴拉巴拉繼續(xù)下去……
Pedro: 簡直是要急死人了,我就是想要一杯咖啡加點(diǎn)牛奶嘛。為啥他們不做一些預(yù)選項(xiàng),像是:咖啡加牛奶,咖啡加糖等等等。
Eve: 因?yàn)檫@種系統(tǒng)的賣點(diǎn)就是:顧客可以*自行搭配 *各種咖啡配料。
Pedro: 好吧,我們用建造者模式進(jìn)行改進(jìn)吧。

public class Coffee {
  private String coffeeName; // required
  private double amountOfCoffee; // required
  private double water; // required
  private double milk; // optional
  private double sugar; // optional
  private double cinnamon; // optional

  private Coffee() { }

  public static class Builder {
    private String builderCoffeeName;
    private double builderAmountOfCoffee; // required
    private double builderWater; // required
    private double builderMilk; // optional
    private double builderSugar; // optional
    private double builderCinnamon; // optional

    public Builder() { }

    public Builder setCoffeeName(String name) {
      this.builderCoffeeName = name;
      return this;
    }

    public Builder setCoffee(double coffee) {
      this.builderAmountOfCoffee = coffee;
      return this;
    }

    public Builder setWater(double water) {
      this.builderWater = water;
      return this;
    }

    public Builder setMilk(double milk) {
      this.builderMilk = milk;
      return this;
    }

    public Builder setSugar(double sugar) {
      this.builderSugar = sugar;
      return this;
    }

    public Builder setCinnamon(double cinnamon) {
      this.builderCinnamon = cinnamon;
      return this;
    }

    public Coffee make() {
      Coffee c = new Coffee();
        c.coffeeName = builderCoffeeName;
        c.amountOfCoffee = builderAmountOfCoffee;
        c.water = builderWater;
        c.milk = builderMilk;
        c.sugar = builderSugar;
        c.cinnamon = builderCinnamon;

      // check required parameters and invariants
      if (c.coffeeName == null || c.coffeeName.equals("") ||
          c.amountOfCoffee <= 0 || c.water <= 0) {
        throw new IllegalArgumentException("Provide required parameters");
      }

      return c;
    }
  }
}

Pedro: 你看這樣你就不能簡單地直接實(shí)例化 Coffee 類了,必須先通過內(nèi)部類 Builder 設(shè)置參數(shù)

Coffee c = new Coffee.Builder()
        .setCoffeeName("Royale Coffee")
        .setCoffee(15)
        .setWater(100)
        .setMilk(10)
        .setCinnamon(3)
        .make();

Pedro: 調(diào)用 make 方法檢查所有必要的參數(shù),如果發(fā)現(xiàn)問題就扔出一個異常,沒問題就返回實(shí)例。
Eve: 很不錯的功能,就是有點(diǎn)啰嗦。
Pedro: 你行你上。
Eve: 小菜一碟,Clojure 支持可選參數(shù)列表,輕松實(shí)現(xiàn)建造者模式。

(defn make-coffee [name amount water
                   & {:keys [milk sugar cinnamon]
                      :or {milk 0 sugar 0 cinnamon 0}}]
  ;; definition goes here
  )

(make-coffee "Royale Coffee" 15 100
             :milk 10
             :cinnamon 3)

Pedro: 啊哈,你這有三個必選參數(shù)和三個可選參數(shù),但是必選參數(shù)依然沒有命名。
Eve: 啥子意思?
Pedro: 比如拿你這個例子來說,我并不能直接看出 15 這個數(shù)字代表什么含義。
Eve: 好像是這樣。那就把所有參數(shù)都取個名吧,然后再做一下預(yù)處理,這樣就和你的建造者一樣了。

(defn make-coffee
  [& {:keys [name amount water milk sugar cinnamon]
      :or {name "" amount 0 water 0 milk 0 sugar 0 cinnamon 0}}]
  {:pre [(not (empty? name))
         (> amount 0)
         (> water 0)]}
  ;; definition goes here        
  )

(make-coffee :name "Royale Coffee"
             :amount 15
             :water 100
             :milk 10
             :cinnamon 3)

Eve: 你看,這樣所有的參數(shù)都有名字了,而且我使用了 :pre 約束對參數(shù)進(jìn)行了預(yù)處理,如果約束不成立,就會扔出 AssertionError 異常。
Pedro: 有意思,:pre 是語言本身提供的么?
Eve: 是的,它就是一個簡單的斷言。除此之外還有 :post 斷言,功能差不多。
(譯注::post 斷言用來設(shè)置函數(shù)執(zhí)行完畢后返回值的約束條件)
Pedro: 額,好吧。不過你知道的,建造者模式通常用在易變數(shù)據(jù)結(jié)構(gòu)上,比如 StringBuilder
Eve: 可變數(shù)據(jù)類型不符合 Clojure 哲學(xué),不過如果你*真的 *需要,也沒問題。用 deftype 創(chuàng)建一個新的類就可以了,別忘了在會發(fā)生變化的屬性上加上 volatile-mutable。
Pedro: 代碼呢?
Eve: 這有一個在 Clojure 里自定義的可變類型的 StringBuilder 的實(shí)現(xiàn)的例子。雖然可變類型有一大堆的缺點(diǎn)和限制,但是沒辦法你非要用。

;; interface
(defprotocol IStringBuilder
    (append [this s])
    (to-string [this]))

;; implementation
(deftype ClojureStringBuilder [charray ^:volatile-mutable last-pos]
    IStringBuilder
    (append [this s] 
      (let [cs (char-array s)]
        (doseq [i (range (count cs))]
          (aset charray (+ last-pos i) (aget cs i))))
      (set! last-pos (+ last-pos (count s))))
    (to-string [this] (apply str (take last-pos charray))))

;; clojure binding
(defn new-string-builder []
  (ClojureStringBuilder. (char-array 100) 0))

;; usage
(def sb (new-string-builder))
(append sb "Toby Wong")
(to-string sb) => "Toby Wong"
(append sb " ")
(append sb "Toby Chung") => "Toby Wang Toby Chung"

Pedro: 并不是和我想象中的一樣麻煩。

第十四集:外觀 (Facade)

我們的新員工 Eugenio Reinn Jr. 給 servlet 程序提交了 134 行的代碼改動。其實(shí)這些代碼改動只是為了發(fā)起一個 request 請求。除此之外的代碼都是注入導(dǎo)入之類的。必須把類似的功能簡化到一行。

Pedro: 管他幾行代碼改動啊。
Eve: 某人在乎啊。
Pedro: 我看一下問題出在哪

class OldServlet {
  @Autowired
  RequestExtractorService requestExtractorService;
  @Autowired
  RequestValidatorService requestValidatorService;
  @Autowired
  TransformerService transformerService;
  @Autowired
  ResponseBuilderService responseBuilderService;

  public Response service(Request request) {
    RequestRaw rawRequest = requestExtractorService.extract(request);
    RequestRaw validated = requestValidatorService.validate(rawRequest);
    RequestRaw transformed = transformerService.transform(validated);
    Response response = responseBuilderService.buildResponse(transformed);
    return response;
  }
}

Eve: 我擦……
Pedro: 這就是我們的內(nèi)部開發(fā)者 API,每次處理 request 請求都需要注入 4 個服務(wù),導(dǎo)入所有依賴,然后就寫出了這樣的代碼。
Eve: 我們來重構(gòu)一下,就用……
Pedro: ……用外觀模式。我們把所有的依賴分解為**單一訪問點(diǎn) (single point of access) **來簡化 API 的使用。

public class FacadeService {
  @Autowired
  RequestExtractorService requestExtractorService;
  @Autowired
  RequestValidatorService requestValidatorService;
  @Autowired
  TransformerService transformerService;
  @Autowired
  ResponseBuilderService responseBuilderService;

  RequestRaw extractRequest(Request req) {
    return requestExtractorService.extract(req);
  } 

  RequestRaw validateRequest(RequestRaw raw) {
    return requestValidatorService.validate(raw);
  }

  RequestRaw transformRequest(RequestRaw raw) {
    return transformerService.transform(raw);
  }

  Response buildResponse(RequestRaw raw) {
    return responseBuilderService.buildResponse(raw);
  }
}

Pedro: 這樣如果你需要在代碼里引入任何服務(wù),只需注入 facade 到你的代碼中。

class NewServlet {
  @Autowired
  FacadeService facadeService;

  Response service(Request request) {
    RequestRaw rawRequest = facadeService.extractRequest(request);
    RequestRaw validated = facadeService.validateRequest(rawRequest);
    RequestRaw transformed = facadeService.transformRequest(validated);
    Response response = facadeService.buildResponse(transformed);
    return response;
  }
}

Eve: 打??!你這就是把所有的依賴都放在一個東西里面,每次用的時候都用這個大的,就這樣?
Pedro: 對,現(xiàn)在不管你需要哪種功能,無腦用 FacadeService。這里面啥依賴都有。
Eve: 那這東西和中介者模式一樣啊。
Pedro: 中介者模式是關(guān)于行為的模式。我們把所有的依賴都交給中介者,然后向其添加*新的行為
Eve: 那外觀模式呢?
Pedro: 外觀模式是關(guān)于組織結(jié)構(gòu)的模式,我們并沒有增加新的功能,我們只是用外觀模式
暴露出已經(jīng)存在的功能 *。
Eve: 明白了。不過貌似這個東西看起來很強(qiáng)大實(shí)際上改進(jìn)不大啊。
Pedro: 也許吧。
Eve: 這是 Clojure 版本,使用命名空間 (namespaces) 來組織結(jié)構(gòu) (structure)。

(ns application.old-servlet
  (:require [application.request-extractor :as re])
  (:require [application.request-validator :as rv])
  (:require [application.transformer :as t])
  (:require [application.response-builder :as rb]))

(defn service [request]
  (-> request
      (re/extract)
      (rv/validate)
      (t/transform)
      (rb/build)))

Eve: 通過 facade 暴露出所有的服務(wù)。

(ns application.facade
  (:require [application.request-extractor :as re])
  (:require [application.request-validator :as rv])
  (:require [application.transformer :as t])
  (:require [application.response-builder :as rb]))

(defn request-extract [request]
  (re/extract request))

(defn request-validate [request]
  (rv/validate request))

(defn request-transform [request]
  (t/transform request))

(defn response-build [request]
  (rb/build request))

Eve: 然后就可以用了。

(ns application.old-servlet
  (:use [application.facade]))

(defn service [request]
  (-> request
      (request-extract)
      (request-validate)
      (request-transform)
      (request-build)))

Pedro: :use:require 有啥區(qū)別?
Eve: 它倆基本一樣,區(qū)別是 :require 暴露出的功能必須通過命名空間全限定名 (namespace/function) 來訪問,用 :use 的時候就可以直接使用 (function)
Pedro: 也就是說,:use 更好咯。
Eve: 也不是,要小心使用 :use,因?yàn)樗赡軙甬?dāng)前命名空間沖突。
Pedro: 哦,我明白你的意思了。一旦你在某個命名空間里使用 (:use [application.facade]),就可以使用 facade 里面所有的函數(shù)功能了?
Eve: 是的。
Pedro: 嗯,是差不多。


  1. 一個不存在的公司。 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容