[译] Clojure 中的设计模式(上)

Clojure 设计模式 Design Patterns


译自 Clojure Design Patterns
Author: Mykhailo Kozik

快速 了解如何在 Clojure 中运用最基本的设计模式

免责声明 绝大多数模式非常易于实现,因为我们可以使用动态类型、函数式和……嗯,和 Clojure。 文中的个别模式实现看起来很烂。好吧我承认,这里写的每个字儿都有可能出错,有时候老司机也难免翻车。

(译注:译者水平有限,使用本文代码所导致的损失概不负责。
如果你在文中发现错误,或有任何意见或建议,请直接在文后留言。2333)


引子

我们所使用的语言本身已经完犊子了。所以我们搞出来一个叫设计模式的东西。
--- 尼古拉斯・赵四

这里有两位谦逊的程序猿 --- Pedro VeelEve Dopler 正在运用设计模式,来着手解决一些通用而常见的软件工程问题。

第一集:命令

IT 外包商大佬 "Serpent Hill & R.E.E" 新接了一个来自美国的大单子。这次的首要交付任务是完成该品牌官网的用户注册、登录、注销功能。

Pedro: 这还不简单,只需要搞一个像这样的 Command 接口……

interface Command {
  void execute();
}

Pedro: 然后让每个具体的功能去实现这个接口,定义自己的 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: 为啥你费那么大老劲在 LoginCommand 里面包裹了一层,为啥不直接调用 DB.login
Pedro: 这个包裹可重要了,因为这样就可以用同一个方法来操作任意实现 Command 的对象了。
Eve: 有啥子用呢?
Pedro: 延时调用、登陆、历史跟踪、缓存……等等一火车应用。
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: 那你怎么延后方法执行做延时调用呢?
(译注:延时调用指的是先进行参数设置,最后再调用方法的意思么?求教。)
Eve: 你自己琢磨一下。当你需要函数执行的时候,只需准备好什么呢?
Pedro: 方法名……
Eve: 还有?
Pedro: ……参数。
Eve: Bingo。你只需记得 (函数名, 参数列表) ,就可以在任何你需要的位置像这样来让函数执行 (apply function-name arguments)
Pedro: 嗯…… 看起来好像挺简单的样子。
Eve: 那可不, 命令模式只需一个函数而已

第二集:策略

Sven Tori 花大价钱雇人制作一张用户表单。但是有几个要求,用户必须按照姓名排序,而且呢,会员用户必须排在所有普通用户之。这不废话么,因为人家掏钱了。倒序排序依然要保持付费用户在上面。

Pedro: 蛤,自定义一个比较器,再调用一下 Collections.sort(users, comparator) 就能搞定了。
Eve: 那要怎么搞才能实现 自定义比较器 呢?
Pedro: 首先要实现 Comparator 接口、实现 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: 当然,差不多像是这样。

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

Pedro: 和我写的挺像的。
Eve: 不过我还有一个改进版。

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

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

Pedro: 哦我的⑦舅老爷哦,这什么可怕的一行代码。
Eve: 函数,你懂的。
Pedro: 管他什么鬼,总之这也太难理解了吧。

Eve 正在解释 juxt、complement 和 sort-by 函数的功能
10 分钟后

Pedro: 这真的是一种非常玄学的策略模式实现。
Eve: 反正对我来说,实现策略模式只需 函数传递与组合

第三集:状态

销售员 Karmen Git 调查了市场情况之后,决定要给不同用户提供专属功能。

Pedro: 很合理的需求嘛。
Eve: 我们来仔细研究一下。

  • 如果是用户是付费用户,则可以看到所有的消息记录。
  • 普通用户则只能看到最近的 10 条消息。
  • 如果用户进行了充值,要记录用户当前总余额。
  • 如果普通用户当前总余额已经足够购买会员,那就让他(自动)升级为……

Pedro: 状态!这模式特别带劲。首先我们要搞一个表示用户状态的枚举。

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: 开始调用吧。

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: 你就是把有关于那些值的逻辑藏在 User 类里面而已。我们可以直接像这样使用策略模式啊 user.newsFeed(subscriptionType)
Pedro: 同意。状态和策略非常相似。甚至连它俩的 UML 表示形式都是一样的。 但是我们把余额信息封装了起来,这样用户接触不到啊。
Eve: 我觉得用另一套方案也能实现相同的功能。无需显式地说明使用哪个策略,而是可以依据某些状态来决定所使用的策略。在 Clojure 里面,这东西和策略模式做的事儿差不多。
Pedro: 但是(在状态模式里)如果成功调用,是可以改变对象的状态哦。
Eve: 话是这样没错,不过这和是不是策略模式没啥关系吧,只是细节实现有些不同而已。
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 函数的任务就是改变对象的状态。虽然 Clojure 不喜欢修改对象的状态,但是非要改的话还是可以的。

(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: 使用多重方法来转发,比使用枚举更好么?
Eve: 也许在上面的例子中并不是,不过通常来说用多重方法更好。
Pedro: 为啥,给解释一下。
Eve: 你知道啥是 双重分派 么?
(译注:双重分派* 原文为 double dispatch,也译为双重转发,双重分发。)*
Pedro: 不造啊。
Eve: 好吧,讲到访问者模式的时候再说吧。

第四集:访问者

Natanius S. Selbys 想要搞一个可以让用户以不同格式导出他们的消息、活动和成就的功能。

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: 还不错,不过你怎么处理参数类型的分发呢?
Pedro: 啥子意思?
Eve: 瞅一下这样一段代码:

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

Pedro: 没瞅出来啥毛病啊。
Eve: 其实,如果执行这段代码会产生 UnknownFormatException
Pedro: 蛤?真的?!
Eve: 在 Java 里只有 单一分派。这也就是说,如果你调用 i.export(f),只有 i 能被分派到具体实现类,而 f 无法被找到具体的实现类别。
(译注1:C++ / Java / C# 等都只支持单一分派,也就是 i 的分派,也就是我们熟悉的 多态 概念。如在上面的例子中,选择使用哪个 export 方法,取决于 i 的运行时类型。而双重分派不仅根据 i 的运行时类型,同时还取决于参数 f 的运行时类型。访问者模式实际上提供了对于支持单分派语言的双分派策略。
(译注2:关于单一分派、双重分派与访问者模式的更多细节可以阅读一下这篇文章。)
Pedro: 我懵逼了。所以你的意思是说,这里没有根据参数类型进行分派?
Eve: 这时候就需要祭出访问者模式了。在依据 i 分派之后,紧接着手工使用 f.someMethod(i) 进行 f 的分派。
Pedro: 代码长啥样给看看呗。
Eve: 你需要在 Visitor 里给每一种类型都定义自己的导出操作。

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 实现。

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: 像这样调用就可以了。

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

Eve: 运转良好。你甚至无需修改 MessageActivity 的代码,就可以增加新的导出格式。只需增加新的访问者即可。
Pedro: 这玩意儿挺实用的。就是实现起来有点复杂啊。 用 Clojure 实现这个是不是也很复杂啊?
Eve: 并不复杂。因为 Clojure 使用多重方法来原生支持双重分派。
Pedro: 多重 啥子
Eve: 不解释,看代码……首先我们定义一个分派函数
(译注:分派 原文 dispatcher,也译为 转发。)

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

Eve: 它依据接受的 itemformat 进行分派。itemformat 的格式如下:

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

Eve: 现在你只需提供一系列函数,来接收各种不同的分派,分派器会自动决定使用最合适的函数进行处理。

(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: 如果遇见未知的导出格式该怎么进行处理呢?
Eve: 我们可以定义默认情况下的分派处理。

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

Pedro: 好吧,但是 :pdf:xml 没有层次继承什么之类的关系啊。就只是关键字而已?
Eve: 答对了,简单问题简单处理嘛。如果你的确需要高级特性,可以使用专设层级 或者依据 class 进行转发。
(译注:专设层级 原文 adhoc hierarchies。暂未发现准确的翻译。)

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

Pedro: 双重冒号?!
Eve: 你就假装它就是个关键字。
Pedro: 好吧就当是吧。
Eve: 接下来就可以把接收分派的类型换成 ::pdf::xml 或者 ::format 了。

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

Eve: 如果系统中出现了新的格式(比如 csv):

(derive ::csv ::format)

Eve: 接受 ::csv 的函数还没有出现时,:csv 会被分派到接受 ::format 的函数那里。
Pedro: 看起来挺棒的。
Eve: 那可不,简单多了。
Pedro: 所以也就是说,如果语言本身支持多重分派,就无需访问者模式
Eve: 完全正确。

第五集:模板方法

MMORPG 游戏 机械多米诺尔大战撒加 需要给 VIP 玩家们(单独)调整电脑难度。破坏平衡性。

Pedro: 首先,我们要搞清楚自动机器角色都应该有些什么行为。
Eve: 你以前玩过 RPG 游戏没?
Pedro: 很庆幸,没。
Eve: 我类乖……来,让你长长见识……

两星期后

Pedro: ……我去,我刚找到一把 +100 攻击力的史诗级大保健大宝剑。
Eve: 这么叼。不过……该起来干活了。
Pedro: 好啦好啦,淡定,这还不手到擒来。我们应该实现这些事件:

  • 战斗
  • 探索
  • 开宝箱

Pedro: 不同的人物会根据不同的事件作出不同的行为,比如法师在战斗中喜欢使用远程法术,但是盗贼更偏爱安静地进行近战刺杀;绝大多数玩家对上锁的箱子束手无策,但是盗贼却可以打开它们,等等……
Eve: 看起来 模板方法 是个合适的选择?
Pedro: 嗯呢。我们先定义抽象的规则,然后在子类里实现不同的具体方法。

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: 我们已经分离出了 Character 类中所有通用的方法,提供给所有的角色。接下来就可以创造子类了,定义属于他们自己的行为以适应具体情景。在我们的游戏里面就是:处理上锁的箱子攻击敌人
Eve: 那我们先来写一个法师类吧。
Pedro: 法师?好的。首先他不能打开上锁的箱子,所以重写的方法里面啥都不干 就行了。然后是攻击模式,如果遇见十个以上的敌人,就施放冰冻术把他们全冻住,然后开传送逃跑。如果遇见十个或者更少的敌人,就对敌人依次使用火球术。

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: 感觉很不错,盗贼类应该怎么写呢?
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: 做的不错。但是这个东西和策略模式有啥区别呢?
Pedro: 啥子意思?
Eve: 我的意思是说,你用子类来重新定义行为,但是策略模式也是重新定义行为啊,只不过是使用函数来实现的。
Pedro: 那个,那个的确是另一种实现方式。
Eve: 同理,状态模式也是另一种实现方式咯。
Pedro: 你想表达什么?
Eve: 明明是同一类问题,你却使用了不同的方式去解决。
Pedro: 那 Clojure 里是怎么用策略模式来解决这个游戏角色问题的?
Eve: 只需要通过给每个角色搞一些专属函数。你看,你写的抽象 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: 角色需要实现函数 handle-chestattack,然后把这两个函数作为参数传递给 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: 我的太上老君呐。这发生了什么?我要报警了。
Eve: 就是改了一下 move-to 所接受的参数啊,这样就可以接受 handle-chestattack 函数了。
而且他们只是可选参数。

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

Eve: 这里要提一下,如果没有传进来这些函数的时候,会自动使用我们提供的默认值:handle-chest 里面什么也不做,然后 attack 里面写的是,见了敌人就跑。
Pedro: 好吧,但是好像用子类继承更好一些吧?你看你这多次调用 move-to 的时候就会产生很多重复的代码。
Eve: 这个可以改进,比如给它起个名,把它定义成一个函数。

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

Eve: 用多重方法也行,这样更强大一些。

(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: 因为这样可以动态的改变他们的行为。假设你的法师魔法值耗尽了,就别扔火球了,他大可以开一个传送门逃跑,只需提供一个新的函数就能实现了。
Pedro: 说的对啊。函数随处可用

第六集:迭代器

技术顾问 Kent Podiololis 正在吐槽 C 语言风格的循环。
“活在 1980 年还是咋地?” --- Kent

Pedro: 肯定要用 Java 里面的迭代器模式啊。
Eve: 别犯傻了,根本没人用 java.util.Iterator
Pedro: 但是大家都在 for-each 循环里隐式地使用它啊。用它来遍历容器感觉特别爽。
Eve: “遍历容器 ”是个什么意思?
Pedro: 专业点来说就是,一个容器需要提供这两个方法:
next(),用来返回下一个元素。hasNext(),如果容器中还存在元素就返回真。
Eve: 那个,你知道啥是链表么?
Pedro: 你说单链表?
Eve: 是的,单链表。
Pedro: 肯定知道啊。它也算一种容器,是由一系列节点组成的。每个节点包括数据部分和指向下一个节点的部分。如果是最后一个节点,那么它下个节点就是 null。
Eve: 很懂行啊。那你给我说说遍历链表和使用迭代器遍历有啥区别?
Pedro: 呃……

Pedro 写了两段遍历的代码:

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

Pedro: 你别说还真是挺像的……那 Clojure 里面有什么类似 Iterator 的东西么?
Eve: seq 函数。

(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: 准确来说是 序列,因为(在 Clojure 里) 序列代替了迭代器
Pedro: seq 可以操作自定义数据结构么?
Eve: 实现 clojure.lang.Seqable 接口就可以了:

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

Pedro: 好吧好吧。但是我听说迭代器通常用来实现惰性,比如等到 getNext() 被调用的时候才会进行求值,用列表能解决这类问题么?
Eve: 能啊,Clojure 里管它叫“惰性序列 ”

(def natural-numbers (iterate inc 1))

Eve: 我们刚才定义了一个表示 全体 自然数的东西,但是并没有 OutOfMemory,因为我们还没有从里面取任何值,它是惰性的。
(译注:0 是否属于自然数仍有争议。目前国际标准和中国国家标准都把 0 算作自然数。)
Pedro: 能仔细解释一下么?
Eve: 对不起哦,我好像也 “惰性” 起来了。(跑咯)
Pedro: 你给我等着我记住你了!

第七集:备忘录

一位名叫 Chad Bogue 的用户丢失了他已经写了两天的消息。给他一个保存按钮吧。

Pedro: 我简直不敢相信有人会在那个输入框里面打字打了两天,整整两天!
Eve: 让我们来拯救 他吧。
(译注:拯救 原文 save,有保存之意。双关。)
Pedro: 我刚才在 Google [1] 上查了一下。实现保存按钮的通常做法是使用备忘录模式。 需要三个东西,
创作者 (originator)
管理者 (caretaker)备忘录 (memento)
Eve: 这些都是干啥用的?
Pedro: 创作者 就是我们需要保存的对象或者状态(例如输入框里面的文本就是创作者),管理者 的功能就是保存需要保存的状态(例如那个保存按钮就是管理者),最后 备忘录 就是用来存储状态的对象。

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: 作家, 管家, 备忘 - 这么些专业词汇,其实本质上是为了实现 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 是不可变的。但是如果你还有一些内部状态可能会发生改变的对象,你就必须对这些备忘录对象进行深层克隆了。
Pedro: 哦,谢谢提醒。所以这里还需要对得到的原型递归使用 clone() 方法。
Eve: 过一会儿我们就会见到原型模式,但是一定要搞清楚,备忘录模式 的本质不是 管理者创作者,而是 保存恢复

第八集:原型

经过分析之后 Dex Ringeus 发现,用户并不喜欢填写登记表。要想办法提升易用性才行。

Pedro: 所以,那个登记表问题出在哪里?
Eve: 因为烦人的表项实在是太多了啊。
Pedro: 比如说?
Eve: 比如说,体重。这项吓跑了 90% 的女性用户。
Pedro: 但是这项对我们的分析系统来说很重要啊,推荐食品和衣服的时候要用到这一项。
Eve: 那就,把它改成非必填项吧,如果用户没有填这一项,就随便取个默认值。
Pedro: 60 千克 咋样。
Eve: 行。
Pedro: 好的,给我两分钟。

两小时后

Pedro: 我建议先建立一张*原型 *登记表,所有表项都预先填上默认值。等用户把内容填进来的时候我们再去改这些值。
Eve: 不错的建议。
Pedro: 这里就是我们的标准注册表原型了,它实现了 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: 需要创建一个新表格的时候,调用 clone() 就可以得到一个同样的表格了,然后就可以对这个新表格进行修改了。
Eve: 哎呦我去太吓人了!在可变的世界里,想复制一个对象就必须依赖 clone() 方法。之所以吓人就在于复制必须要足够深,也就是说,如果你想复制一个引用,就必须递归地调用 clone(),万一引用对象不支持 clone()……
Pedro: 这个模式就是用来解决这个问题的。
Eve: 我不觉得每次加入新对象都必须费老劲实现新对象的 clone 方法是一个很好的解决方案。
Pedro: 那 Clojure 有啥灵丹妙药么?
Eve: Clojure 的数据结构是不可变的。就这样。
Pedro: 这样就能解决原型问题了?
Eve: 每次修改数据,你都会得到一个全新的不可变的原数据的拷贝,原来的数据不会发生任何变化。 不可变数据类型的世界里不需要原型模式

(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: 厉害了!但是这东西性能咋样?复制上百万行的数据也要返回一个全新的?好像要消耗巨大的运算资源啊。
Eve: 并不是你想的那样。你可以去搜一下可持久化数据结构 (persistent data structures)结构共享 (structural sharing) 的相关资料。
Pedro: 谢了啊。

第九集:中介者

最近,公司对当前代码库进行了外部代码审查,暴露出许多问题。Veerco Wierde 强调,这个聊天应用耦合度太高。

Eve: 耦合度太高是啥意思。
Pedro: 就是说,对象彼此之间的关系太过紧密。对象之间彼此知道的太多就会出问题。
Eve: 能详细说明一下么?
Pedro: 直接看一下目前的聊天代码实现:

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: 问题就在于用户必须知道其它所有的用户。这样维护起来非常的麻烦。每当有新的用户加入聊天,你必须通过 addUser 方法给所有已存在的用户的添加这个新用户的引用。
Eve: 所以,我们就把这个添加新用户的职能移动到另一个类里面?
Pedro: 是的,基本上就是这样。我们创造一个*超限の觉醒 * 类,其名为终结者(误)中介者,它把众生绑定在一起。很显然,这样每个用户只会感应到中介者的存在。

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: 貌似就是简单的重构了一下啊。
Pedro: 看起来貌似没啥改进,但是如果你有上百个组建需要互相关联(比如 UI),伟大的救世主,中介者,就出现了。
Eve: 这倒是。
Pedro: 下面该 Clojure 出招了。
Eve: 行吧……我看看……你的中介者所拥有的能力就是保存用户列表 和*发送消息 *。

(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: 不吹不黑,不就是 减小耦合 么,轻轻松松。

第十集:观察者

经探查,某第三方安全机构在黑客 Dartee Hebl 账户上发现了高达数十亿美元的不明资金。你的任务是追踪这个账户的大额资金流动。

Pedro: 我们是福尔摩斯 么?
Eve: 不是,但是这个系统里并没有日志记录,所以要想个法子去追踪这个账户上所有的资金流动。
Pedro: 我们需要增加点观察者。每当有资金变化,如果流动金额*足够大 *,就发出通知,然后追踪源头。首先我们需要一个 Observer 接口:

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

Pedro: 然后实现两个具体观察者。

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 类的职责就是用来管理这些观察者。

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 方法做点手脚。如果账户的流动金额高于 100$,通知 FBI,冻结他的账户。

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: 单一职责原则。
Eve: 哦,对。
Pedro: 这样就可以动态地对观察者的功能进行搭配组合了。
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: 对啊,实际上观察者就是把一些函数记录下来,然后这些函数就可以等着被其它函数调用了。
Pedro: 这不还是一种模式啊。
Eve: 对,不过我们可以借助 Clojure 自带的观察者功能对其进行进一步地改进。

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

Pedro: 这样写有啥优点呢。
Eve: 首先是,我们的 add-money 方法更干净了,只负责增加金额。然后是,这种方式可以监听到*所有 * 的状态改变,不仅仅是那个我们做了手脚的 add-money 方法。
Pedro: 解释一下呗。
Eve: 假如这里提供了一个隐藏的秘密方法 secret-add-money 也可以改动资金,那么我这种观察者一样可以很好地处理它。
Pedro: 这个有点酷炫啊!

第十一集:解释器

Bertie Prayc 从我们的的服务器上偷走了重要的数据,而且还做了 BT 种子上传到了网上。搞一个叫 Bertie 的假帐户整一下他。

Pedro: BT 系统建立在 .torrent 文件之上。我们需要进行 Bencode 编码。
Eve: 是的,不过我们首先要了解它的编码格式

Bencode 编码规范:

  • 支持以下两种数据类型:

    • 整形 N 被编码为 i<N>e。 (42 = i42e)
    • 字符串 S 被编码为 <长度>:<内容> (hello = 5:hello)
  • 支持以下两种容器类型:

    • 列表类型被编码为 l<内容>e ([1, "Bye"] = li1e3:Byee)
    • 键值类型被编码为 d<内容>e ({"R" 2, "D" 2} = d1:Ri2e1:Di2ee)
      • 键必须是字符串,值可以是任何允许的 bencode 元素节点

Pedro: 看上去不难。
Eve: 但愿吧,考虑到值是可以进行嵌套的,列表套列表之类的。
Pedro: 好的。我认为我们可以使用*解释器 *模式来对付 bencode 编码问题。
Eve: 试试看。
Pedro: 我们先把所有 bencode 元素抽象为一个接口

interface BencodeElement {
  String interpret();
}

Pedro: 然后我们再依次搞出数据类型和容器类型的实现

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: 最终,我们就可以使用平时用的数据结构编写程序来生成编码后的字符串了。

// 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: 为了增强可读性嘛。
Eve: 我觉得你应该听说过代码即数据,这在 Clojure 中特别容易实现

;; 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 定义一个特殊数据是多么的方便。
Pedro: 真的是啊,不同的 bencode 解释器不过是一些函数而已,而不是一些类。
Eve: 回答正确,解释器不过是一套用来处理树形结构的函数

第十二集:羽量 (Flyweight)

某律师公司的管理员 Cristopher, Matton & Pharts 发现,报表系统消耗了大量的内存资源,导致垃圾处理程序不断运行,造成系统卡顿。修复这个问题。

Pedro: 我以前也遇见过这个问题。
Eve: 问题出在哪里呢?
Pedro: 这是个实时图表系统,里面有非常多的点。真的是占用了巨大的内存空间。结果垃圾处理程序把系统拖垮了。
Eve: 嗯……那我们咋办?
Pedro: 我也不造啊,缓存也派不上用场,因为节点实在是太多了……
Eve: 等等!
Pedro: 咋了?
Eve: 这里的点会被重复使用多次,为什么我们不预先加载最经常使用的点呢?比如 [0, 100] 范围内的。
Pedro: 你的意思是用*共享 *模式?
(译注:共享 原文 Flyweight。直译为 羽量。这个模式的思想是共享相同的元素,故又译为 享元。)
Eve: 我的意思是复用对象。

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: 这个模式的要点有两个:一是在启动的时候对最常用的点进行预加载,二是使用静态工厂方法取代构造方法,以便返回缓存的对象。
Eve: 这东西你测试过了?
Pedro: 肯定啊,系统像钟表一样精确运行。
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: 我搞了一个关于 [x, y] 的扁平映射 (flat map) ,以取代二维数组。
Pedro: 没啥区别啊。
Eve: 并不是,我这样更灵活一些,你的二维数组并不能及时适应三维的点或者非整型的点值。
Pedro: 哦,好吧。
Eve: 其实还能更简单,在 Clojure 里面你可以很方便的使用 memoize 函数来给 make-point 函数增加缓存功能,这样就可以替代手工的缓存工厂了。

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

Eve: 每次调用的时候(除了第一次),只要函数参数与之前的某次调用相同,就会返回上次缓存的值。
Pedro: 这个太牛了!
Eve: 那可不,不过需要注意的是,如果你的函数具有副作用,用缓存就不合适了

第十三集:建造者 (Builder)

Tuck Brass 抱怨他的自动咖啡贩卖机系统运行起来实在是太慢了。顾客们根本没有耐心等下去就走了。

Pedro: 首先要弄明白问题的真正原因。
Eve: 我已经调查完毕,这是个上古系统,竟然是用 COBOL 语言写的,而且是建立在问-答 机制专家系统架构上。这个机制在上古时期很流行的。
Pedro: “问-答” 机制是个啥?
Eve: 就好比有一个操作员坐在电脑终端面前。系统问:
“要加点水么?”
,操作员回答:“对 ”。系统又问:“要加点咖啡么?”,操作员回答:“对 ” 然后巴拉巴拉继续下去……
Pedro: 简直是要急死人了,我就是想要一杯咖啡加点牛奶嘛。为啥他们不做一些预选项,像是:咖啡加牛奶,咖啡加糖等等等。
Eve: 因为这种系统的卖点就是:顾客可以*自行搭配 *各种咖啡配料。
Pedro: 好吧,我们用建造者模式进行改进吧。

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: 你看这样你就不能简单地直接实例化 Coffee 类了,必须先通过内部类 Builder 设置参数

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

Pedro: 调用 make 方法检查所有必要的参数,如果发现问题就扔出一个异常,没问题就返回实例。
Eve: 很不错的功能,就是有点啰嗦。
Pedro: 你行你上。
Eve: 小菜一碟,Clojure 支持可选参数列表,轻松实现建造者模式。

(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: 啊哈,你这有三个必选参数和三个可选参数,但是必选参数依然没有命名。
Eve: 啥子意思?
Pedro: 比如拿你这个例子来说,我并不能直接看出 15 这个数字代表什么含义。
Eve: 好像是这样。那就把所有参数都取个名吧,然后再做一下预处理,这样就和你的建造者一样了。

(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: 你看,这样所有的参数都有名字了,而且我使用了 :pre 约束对参数进行了预处理,如果约束不成立,就会扔出 AssertionError 异常。
Pedro: 有意思,:pre 是语言本身提供的么?
Eve: 是的,它就是一个简单的断言。除此之外还有 :post 断言,功能差不多。
(译注::post 断言用来设置函数执行完毕后返回值的约束条件)
Pedro: 额,好吧。不过你知道的,建造者模式通常用在易变数据结构上,比如 StringBuilder
Eve: 可变数据类型不符合 Clojure 哲学,不过如果你*真的 *需要,也没问题。用 deftype 创建一个新的类就可以了,别忘了在会发生变化的属性上加上 volatile-mutable
Pedro: 代码呢?
Eve: 这有一个在 Clojure 里自定义的可变类型的 StringBuilder 的实现的例子。虽然可变类型有一大堆的缺点和限制,但是没办法你非要用。

;; 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 行的代码改动。其实这些代码改动只是为了发起一个 request 请求。除此之外的代码都是注入导入之类的。必须把类似的功能简化到一行。

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: 这就是我们的内部开发者 API,每次处理 request 请求都需要注入 4 个服务,导入所有依赖,然后就写出了这样的代码。
Eve: 我们来重构一下,就用……
Pedro: ……用外观模式。我们把所有的依赖分解为**单一访问点 (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: 这样如果你需要在代码里引入任何服务,只需注入 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: 对,现在不管你需要哪种功能,无脑用 FacadeService。这里面啥依赖都有。
Eve: 那这东西和中介者模式一样啊。
Pedro: 中介者模式是关于行为的模式。我们把所有的依赖都交给中介者,然后向其添加*新的行为
Eve: 那外观模式呢?
Pedro: 外观模式是关于组织结构的模式,我们并没有增加新的功能,我们只是用外观模式
暴露出已经存在的功能 *。
Eve: 明白了。不过貌似这个东西看起来很强大实际上改进不大啊。
Pedro: 也许吧。
Eve: 这是 Clojure 版本,使用命名空间 (namespaces) 来组织结构 (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 暴露出所有的服务。

(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 有啥区别?
Eve: 它俩基本一样,区别是 :require 暴露出的功能必须通过命名空间全限定名 (namespace/function) 来访问,用 :use 的时候就可以直接使用 (function)
Pedro: 也就是说,:use 更好咯。
Eve: 也不是,要小心使用 :use,因为它可能会引起当前命名空间冲突。
Pedro: 哦,我明白你的意思了。一旦你在某个命名空间里使用 (:use [application.facade]),就可以使用 facade 里面所有的函数功能了?
Eve: 是的。
Pedro: 嗯,是差不多。


  1. 一个不存在的公司。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容