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 Veel 和 Eve 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)良好。你甚至無需修改 Message 和 Activity 的代碼,就可以增加新的導(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ù)接受的 item 和 format 進(jìn)行分派。item 和 format 的格式如下:
;; 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-chest 和 attack,然后把這兩個函數(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-chest 和 attack 函數(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) save 和 restore 這兩個功能。
(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: 嗯,是差不多。
-
一個不存在的公司。 ?