November 3, 2018
By: Kevin

面向对象的设计模式和Clojure

  1. 命令
  2. 策略
  3. 状态
  4. 访问者
  5. 模板方法
  6. 迭代器
  7. 备忘录
  8. 原型
  9. 中介者
  10. 观察者
  11. 解释器
  12. 羽量 (Flyweight)
  13. 建造者 (Builder)
  14. 外观 (Facade)
  15. 单例
  16. 责任链
  17. 组合
  18. 适配
  19. 装饰者
  20. 代理
  21. 桥接
  22. 速查表 (代替总结)
  23. 演员表

转载自 BlindingDark在简书的翻译

我们所使用的语言本身已经完犊子了. 所以我们搞出来一个叫设计模式的东西.

  • 尼古拉斯・赵四

这里有两位谦逊的程序猿 - Pedro Veel 和 Eve 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);
  }
}
(new LoginCommand("django", "unCh@1ned")).execute();
(new LogoutCommand("django")).execute();

Pedro: 用起来也很容易. 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: 好吧, 讲到访问者模式的时候再说吧.

访问者

想要搞一个可以让用户以不同格式导出他们的消息, 活动和成就的功能. 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: 运转良好. 你甚至无需修改 Message 和 Activity 的代码, 就可以增加新的导出格式. 只需增加新的访问者即可.

Pedro: 这玩意儿挺实用的. 就是实现起来有点复杂啊. 用 Clojure 实现这个是不是也很复杂啊? Eve: 并不复杂. 因为 Clojure 使用多重方法来原生支持双重分派.

Pedro: 多重 啥子 ?

Eve: 不解释, 看代码......首先我们定义一个分派函数. (译注: 分派 原文 dispatcher, 也译为 转发. )

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

Eve: 它依据接受的 item 和 format 进行分派. item 和 format 的格式如下:

;; 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-chest 和 attack, 然后把这两个函数作为参数传递给 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-chest 和 attack 函数了. 而且他们只是可选参数.

(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: 作家, 管家, 备忘 - 这么些专业词汇, 其实本质上是为了实现 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 是不可变的. 但是如果你还有一些内部状态可能会发生改变的对象, 你就必须对这些备忘录对象进行深层克隆了.

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: 嗯, 是差不多.

单例

Feverro O'Neal 抱怨我们的 UI 样式太多了. 把每个应用的 UI 配置都统一成一份.

Pedro: 等下, 我看这里要求每个用户都可以保存 UI 样式啊.

Eve: 可能需求有变吧.

Pedro: 好吧, 那我们应该使用 单例 (Singleton) 保存配置, 然后在需要用到的地方调用.

public final class UIConfiguration {
  public static final UIConfiguration INSTANCE = new UIConfiguration("ui.config");

  private String backgroundStyle;
  private String fontStyle;


  private UIConfiguration(String configFile) {
    loadConfig(configFile);
  }

  private static void loadConfig(String file) {

    INSTANCE.backgroundStyle = "black";
    INSTANCE.fontStyle = "Arial";
  }

  public String getBackgroundStyle() {
    return backgroundStyle;
  }

  public String getFontStyle() {
    return fontStyle;
  }
}

Pedro: 这样就可以在不同的 UI 之间共享配置了.

Eve: 没错是没错, 但是为啥写了这么多代码?

Pedro: 因为我们需要保证只会有一个 UIConfiguration 的实例存在.

Eve: 那我问你一个问题: 单例和全局变量之间有啥区别.

Pedro: 你说啥?

Eve: ......单例和全局变量之间的区别啊.

Pedro: Java 不支持全局变量.

Eve: 但是 UIConfiguration.INSTANCE 就是全局变量啊.

Pedro: 好吧, 就算是吧.

Eve: 单例模式在 Clojure 里面的实现就是最简单的 def.

(def ui-config (load-config "ui.config"))

(defn load-config [config-file]
    {:bg-style "black" :font-style "Arial"})

Pedro: 但是你这样怎么改变样式呢?

Eve: 你怎么在你的代码里改, 我就怎么改.

Pedro: 额......好吧, 我们增加点难度. 把 UIConfiguration.loadConfig 变成公共的, 这样当需要改配置的时候就可以调用它来改了.

Eve: 那我就把 ui-config 改成 atom 然后想改配置的时候就调用 swap! .

Pedro: 但是 atoms 只在并发环境下才有用啊.

Eve: 第一点, 虽然 atoms 在并发环境下有用, 但是并不是只能用在并发环境下. 第二点, atom 的读操作并不是你想的那么缓慢. 第三点, 这种改变 UI 配置的方式是原子性 的.

Pedro: 在这个简单例子里需要关心原子性么?

Eve: 需要啊. 考虑这种可能性, UI 配置发生了变化, 一些渲染器读取到了新的 backgroundStyle, 却读取到了老的 fontStyle.

Pedro: 好吧, 那就给 loadConfig 加上 synchronized 关键字.

Eve: 那你必须还得给 getter 也加上 synchonized, 这样会导致运行速度变慢.

Pedro: 我可以用双重检查锁定 习语啊.

Eve: 双重检查锁定是很巧妙, 但是并不总是管用.

Pedro: 好吧我认输, 你赢了.

责任链

纽约营销组织 "A Profit NY" 需要在他们的公共聊天系统上开启敏感词过滤.

Pedro: 卧槽, 他们不喜欢"槽 "这个字儿?

Eve: 他们是营利组织, 如果有人在公共聊天室说脏话会造成经济损失的.

Pedro: 那又是谁定义了脏话列表?

Eve: George Carlin . (译注: 原文给出的链接是 youtube 上 George Carlin 的演讲视频, 由于你懂的原因这里替换成一个可以访问的介绍. )

边看边笑

Pedro: 好吧, 那就加一个过滤器把这些脏字替换成星号好了.

Eve: 还要确保你的方案是可扩展的, 或许还要添加其它的过滤器呢.

Pedro: 使用责任链模式应该是一个不错的候选. 首先我们需要搞个抽象的过滤器.

public abstract class Filter {
  protected Filter nextFilter;

  abstract void process(String message);

  public void setNextFilter(Filter nextFilter) {
    this.nextFilter = nextFilter;
  }
}

Pedro: 然后, 实现你所需要的具体的过滤器

class LogFilter extends Filter {
  @Override
  void process(String message) {
    Logger.info(message);
    if (nextFilter != null) nextFilter.process(message);
  }
}

class ProfanityFilter extends Filter {
  @Override
  void process(String message) {
    String newMessage = message.replaceAll("fuck", "f*ck");
    if (nextFilter != null) nextFilter.process(newMessage);
  }
}

class RejectFilter extends Filter {
  @Override
  void process(String message) {
    System.out.println("RejectFilter");
    if (message.startsWith("[A PROFIT NY]")) {
      if (nextFilter != null) nextFilter.process(message);
    } else {

    }
  }
}

class StatisticsFilter extends Filter {
  @Override
  void process(String message) {
    Statistics.addUsedChars(message.length());
    if (nextFilter != null) nextFilter.process(message);
  }
}

Pedro: 最后, 组合成一个过滤器链, 传给它需要处理的信息.

Filter rejectFilter = new RejectFilter();
Filter logFilter = new LogFilter();
Filter profanityFilter = new ProfanityFilter();
Filter statsFilter = new StatisticsFilter();

rejectFilter.setNextFilter(logFilter);
logFilter.setNextFilter(profanityFilter);
profanityFilter.setNextFilter(statsFilter);

String message = "[A PROFIT NY] What the fuck?";
rejectFilter.process(message);

Eve: 好的, 现在轮到 Clojure了. 只需把各种过滤器定义为函数.

(defn log-filter [message]
  (logger/log message)
  message)

(defn stats-filter [message]
  (stats/add-used-chars (count message))
  message)

(defn profanity-filter [message]
  (clojure.string/replace message "fuck" "f*ck"))

(defn reject-filter [message]
  (if (.startsWith message "[A Profit NY]")
    message))

Eve: 然后使用 some-> 宏链接各个过滤器.

(defn chain [message]
  (some-> message
          reject-filter
          log-filter
          stats-filter
          profanity-filter))

Eve: 你看到有多简单了么, 不需要每次都调用 if (nextFilter != null) nextFilter.process(), 他们就自然地链接在一起. 调用链的顺序自然地依照 some-> 里从上到下填写函数的顺序, 无需手动使用 setNext.

Pedro: 这东西的可组合性真的强啊, 但是为啥这里你选择使用 some->, 而不是选择用 ->.

Eve: 是为了实现 有阻过滤器 (reject-filter). 它可以尽早地停止处理过程, 一旦有过滤器返回 nil, some-> 就会直接返回 nil.

Pedro: 可以进一步解释一下么?

Eve: 看看实际用法你就懂了

(chain "fuck") => nil
(chain "[A Profit NY] fuck") => "f*ck"

Pedro: 懂了.

Eve: 责任链模式 不过是一种函数组合.

组合

女演员 Bella Hock 投诉说, 在她的电脑上看不到我们社交网站的用户头像.

" 看谁都是黑的, 这是黑洞么? "

Pedro: 技术上来说是黑色方框.

Eve: 额, 在我的电脑上也出现了这个问题.

Pedro: 应该是最近的一次更新把头像显示给搞坏了.

Eve: 奇怪啊, 渲染头像的方式和渲染其它节点的方式是一样的啊, 但是其它节点的显示都正常啊.

Pedro: 你确定是同一种渲染方式?

Eve: 额......不确定

开始扒拉代码

Pedro: 这里™发生了什么?

Eve: 不知谁从哪复制的代码, 粘贴过来之后忘记改头像这部分了.

Pedro: 强烈谴责, 开启谴责工具 git-blame.

Eve: 谴责 虽然是好东西, 但是我们还是得修复这个问题啊.

Pedro: 修复很简单啊, 就在这加一行代码.

Eve: 我的意思是, 真正解决掉这个问题. 为啥我们要使用两段相似的代码来处理同一个模块?

Pedro: 对耶, 我觉得我们可以用组合模式来搞定整个界面的渲染问题. 我们定义最小的渲染元素是一个块 (Block).

public interface Block {
  void addBlock(Block b);
  List<Block> getChildren();

  void render();
}

Pedro: 很显然一个块里面可以嵌套着其它的块, 这是组合模式的核心所在, 首先我们可以创造出一些块的实现.

public class Page implements Block { }
public class Header implements Block { }
public class Body implements Block { }
public class HeaderTitle implements Block { }
public class UserAvatar implements Block { }

Pedro: 然后把各种具体实现依然当作 Block 来处理

Block page = new Page();
Block header = new Header();
Block body = new Body();
Block title = new HeaderTitle();
Block avatar = new UserAvatar();

page.addBlock(header);
page.addBlock(body);
header.addBlock(title);
header.addBlock(avatar);

page.render();

Pedro: 这是一种关于组织结构的模式, 是一种组合 (compose) 对象的好方式. 所以我们叫它组合结构 (composite)

Eve: 喂, 组合结构不就是个树形结构么.

Pedro: 是的.

Eve: 这种模式适用于所有的数据结构么?

Pedro: 不, 只适用于列表和树形结构.

Eve: 实际上, 树形可以用列表来表示.

Pedro: 怎么表示?

Eve: 第一个元素表示父节点, 后续元素表示子节点, 依次这样......

Pedro: 我懂了.

Eve: 为了更详细地进行说明, 假如有这样一棵树

      A
   /  |  \
  B   C   D
  |   |  / \
  E   H J   K
 / \       /|\
F   G     L M N

Eve: 然后这是这棵树的列表形式表达

(def tree
  '(A (B (E (F) (G))) (C (H)) (D (J) (K (L) (M) (N)))))

Pedro: 这括号数量有点夸张啊!

Eve: 用来明确定义结构, 你懂的.

Pedro: 但是这样理解起来很困难啊.

Eve: 但适合机器识别, 这里提供了一个十分酷炫的功能 tree-seq, 用来解析这颗树.

(map first (tree-seq next rest tree)) => (A B E F G C H D J K L M N)

Eve: 如果你需要更强大的遍历功能, 可以试试 clojure.walk

Pedro: 我看不懂, 这东西好像有点难.

Eve: 不用全部理解, 你就只需了解用一种数据结构就可以表示整棵数, 一个函数就可以操作它.

Pedro: 这个函数都会干点啥?

Eve: 它会遍历这颗树, 然后把指定的函数作用于所有的节点, 在我们的例子里就是渲染每个块.

Pedro: 我还是不懂, 可能我还是太年轻了, 我们跳过树的这个部分.

第十八集: 工厂方法

Sir Dry Bang 提议要给他们热卖的游戏增加新的关卡. 关卡多多, 圈钱多多.

Pedro: 我们要搞出来一个啥样的新关卡?

Eve: 就简单改一下道具资源然后加一点新的物体材质: 纸, 木头, 铁......

Pedro: 这么做是不是有点脑残?

Eve: 反正本身就是个脑残游戏. 如果玩家愿意砸钱给他的游戏角色买个彩色帽子, 那肯定也愿意买个木头材质的块块儿.

Pedro: 我也这么觉得, 不管咋说, 先搞一个通用的 MazeBuilder 然后为每种类型的方块创建具体的 builder. 这叫工厂模式.

class Maze { }
class WoodMaze extends Maze { }
class IronMaze extends Maze { }

interface MazeBuilder {
  Maze build();
}

class WoodMazeBuilder {
  @Override
  Maze build() {
    return new WoodMaze();
  }
}

class IronMazeBuilder {
  @Override
  Maze build() {
    return new IronMaze();
  }
}

Eve: 难道 IronMazeBuilder 还能不返回 IronMazes?

Pedro: 这不是重点, 重点是, 如果你想要生产其它材质的方块, 只需要改变具体的生产工厂.

MazeBuilder builder = new WoodMazeBuilder(); Maze maze = builder.build();

Eve: 这好像和之前的哪个模式挺像的.

Pedro: 你说哪个?

Eve: 我觉得像策略模式和状态模式.

Pedro: 怎么可能! 策略模式是关于选择哪一种合适的操作, 而工厂模式是为了生产适合的对象.

Eve: 但是生产同样可以看作一种操作.

(defn maze-builder [maze-fn])

(defn make-wood-maze [])
(defn make-iron-maze [])

(def wood-maze-builder (partial maze-builder make-wood-maze))
(def iron-maze-builder (partial maze-builder make-iron-maze))

Pedro: 嗯, 的确看起来很像.

Eve: 对吧.

Pedro: 有什么使用范例没?

Eve: 用不着, 按照你的直觉来使用就行, 你可以回到上面再看一下 策略, 状态 或 模板方法 这些章节.

第十九集: 抽象工厂

玩家不愿意购买游戏推出的新关卡. 于是 Saimank Gerr 搭了一个反馈云平台供玩家吐槽. 根据反馈结果分析, 出现最多的负面词汇是: " 丑" , " 垃圾" , " 渣" .

改进一下关卡构建系统.

Pedro: 我就说了吧这是个垃圾游戏.

Eve: 是啊, 雪地背景配木墙, 太空侵入配木墙, 啥都东西都搭配木制墙体是要闹哪样.

Pedro: 所以我们必须得把每关的游戏世界分离出来, 然后再给每种世界分配一组具体的对象.

Eve: 解释一下.

Pedro: 我们不用以前构建具体方块的工厂方法了, 取而代之的是使用抽象工厂, 以创建一组相关对象, 这样以来构建关卡的方式看起来就不会那么糟糕了.

Eve: 举个栗子.

Pedro: 看代码. 首先我们定义抽象 关卡工厂的行为

public interface LevelFactory {
  Wall buildWall();
  Back buildBack();
  Enemy buildEnemy();
}

Pedro: 然后是关卡元素的层次结构, 关卡就是由这些内容组成的

class Wall {}
class PlasmaWall extends Wall {}
class StoneWall extends Wall {}

class Back {}
class StarsBack extends Back {}
class EarthBack extends Back {}

class Enemy {}
class UFOSoldier extends Enemy {}
class WormScout extends Enemy {}

Pedro: 看到没? 我们给每个关卡都提供了具体的对象, 现在就可以给它们创建工厂了.

class SpaceLevelFactory implements LevelFactory {
  @Override
  public Wall buildWall() {
    return new PlasmaWall();
  }

  @Override
  public Back buildBack() {
    return new StarsBack();
  }

  @Override
  public Enemy buildEnemy() {
    return new UFOSoldier();
  }
}

class UndergroundLevelFactory implements LevelFactory {
  @Override
  public Wall buildWall() {
    return new StoneWall();
  }

  @Override
  public Back buildBack() {
    return new EarthBack();
  }

  @Override
  public Enemy buildEnemy() {
    return new WormScout();
  }
}

Pedro: 关卡工厂的实现类为各个关卡生产出相关的一组对象. 这样肯定比以前的关卡好看.

Eve: 让我冷静一下. 我真的看不出这和工厂方法有啥区别.

Pedro: 工厂方法把创建对象推迟到子类, 抽象工厂也一样, 只不过创建的是一组相关对象 .

Eve: 啊哈, 也就是说我需要一组相关的函数来实现抽象工厂.

(defn level-factory [wall-fn back-fn enemy-fn])

(defn make-stone-wall [])
(defn make-plasma-wall [])

(defn make-earth-back [])
(defn make-stars-back [])

(defn make-worm-scout [])
(defn make-ufo-soldier [])

(def underground-level-factory
  (partial level-factory
           make-stone-wall
           make-earth-back
           make-worm-scout))

(def space-level-factory
  (partial level-factory
           make-plasma-wall
           make-stars-back
           make-ufo-soldier))

Pedro: 很眼熟.

Eve: 就是这么直接. 你挂在嘴边的" 一组相关的东西" , 在我看来" 东西" 就是函数.

Pedro: 是的, 很清晰, 不过 partial 是干啥的.

Eve: partial 用来向函数提供参数. 所以, underground-level-factory 只需考虑构建什么样式的墙体, 背景和敌人. 其余的功能都是从抽象的 level-factory 方法继承而来的.

Pedro: 很方便.

适配

Deam Evil 举办了一场复古风格中世纪骑士对决. 奖金高达 $100.000

我分你一半奖金, 只要你能黑掉他的系统, 允许我的武装突击队加入比赛.

Pedro: 终于, 我们接到一个好玩的活了.

Eve: 我非常期待这场比赛啊. 尤其是 M16 对阵铁剑的部分.

Pedro: 但是骑士们都穿着良好的盔甲啊.

Eve: F1 手榴弹根本不在乎 什么盔甲.

Pedro: 管他呢, 只管干活拿钱.

Eve: 五万大洋, 好价钱啊.

Pedro: 可不是嘛, 瞅瞅这个, 我搞到了他们竞赛系统的源码, 虽然我们不大可能直接修改源码吧, 但是说不准能找到一些漏洞.

Eve: 我找到漏洞了

public interface Tournament {
  void accept(Knight knight);
}

Pedro: 啊哈! 系统只用了 Knight 做传入参数检查. 只需要把突击队员伪造 (to adapt) 成骑士就行了. 让我们看看骑士都长什么样子

interface Knight {
  void attackWithSword();
  void attackWithBow();
  void blockWithShield();
}

class Galahad implements Knight {
  @Override
  public void blockWithShield() {
    winkToQueen();
    take(shield);
    block();
  }

  @Override
  public void attackWithBow() {
    winkToQueen();
    take(bow);
    attack();
  }

  @Override
  public void attackWithSword() {
    winkToQueen();
    take(sword);
    attack();
  }
}

Pedro: 为了能传入突击队员, 我们先看看突击队员的原始实现

class Commando {
    void throwGrenade(String grenade) { }
    shot(String rifleType) { }
}

Pedro: 开始改造 (adapt)

class Commando implements Knight {
  @Override
  public void blockWithShield() {

  }

  @Override
  public void attackWithBow() {
    throwGrenade("F1");
  }

  @Override
  public void attackWithSword() {
    shotWithRifle("M16");
  }
}

Pedro: 这样就搞定了.

Eve: Clojure 里更简单.

Pedro: 真的?

Eve: 我们不喜欢类型, 所以根本没有类型检查.

Pedro: 那你是怎么把骑士替换成突击队员的呢?

Eve: 本质上, 骑士是什么? 就是一个由数据和行为组成的 map 而已.

{:name "Lancelot"
 :speed 1.0
 :attack-bow-fn attack-with-bow
 :attack-sword-fn attack-with-sword
 :block-fn block-with-shield}

Eve: 为了能适配突击队员, 只需把原始的函数替换为突击队员的函数

{:name "Commando"
 :speed 5.0
 :attack-bow-fn (partial throw-grenade "F1")
 :attack-sword-fn (partial shot "M16")
 :block-fn nil}

Pedro: 我们怎么分赃分钱?

Eve: 五五开.

Pedro: 我写的代码行多啊, 我要七.

Eve: 行, 七七开.

Pedro: 成交.

装饰者

Podrea Vesper 抓到我们在比赛上作弊. 现在有两条路可以走: 要么进局子, 要么就帮他的超级骑士加入比赛.

Pedro: 我不想进监狱.

Eve: 我也不想.

Pedro: 那我们就再帮他做一次弊吧.

Eve: 和上一次一样, 是吧? Pedro 不完全是. 突击队员是军队的人, 本来是不允许参加比赛的. 我们适配 (adapted) 了一下. 但是骑士本来就允许参加比赛, 不需要我们再改造了. 我们必须 给现有的对象增加新的行为.

Eve: 继承还是组合?

Pedro: 组合, 装饰者模式的主要目的就是要在运行时改变行为.

Eve: 所以, 我们要怎么造出一个超级骑士呢?

Pedro: 他们计划派出骑士 Galahad, 然后给他装饰 一下, 让他拥有超多血量 和强力盔甲 .

Eve: 嘿, 这个条子竟然还玩儿辐射[1]呢.

Pedro: 嗯哪, 让我们先写一个抽象骑士类

public class Knight {
    protected int hp;
    private Knight decorated;

    public Knight() { }

    public Knight(Knight decorated) {
        this.decorated = decorated;
    }

    public void attackWithSword() {
        if (decorated != null) decorated.attackWithSword();
    }

    public void attackWithBow() {
        if (decorated != null) decorated.attackWithBow();
    }

    public void blockWithShield() {
        if (decorated != null) decorated.blockWithShield();
    }
}

Eve: 所以我们改造了哪些功能?

Pedro: 首先我们使用 Knight 类取代原来的接口, 增加了血量属性. 然后我们提供了两个不同的构造方法, 默认无参的是标准行为, decorated 参数表示需要装饰的对象.

Eve: 用类代替接口是不是因为类更直接一些?

Pedro: 不是因为这个, 是因为这样可以避免出现两个功能相似的类, 同时不必强制对象实现所有的方法, 因为我们给每个待装饰对象提供了方法的默认实现.

Eve: 好吧, 那强力的盔甲在哪里?

Pedro: 很简单

public class KnightWithPowerArmor extends Knight {
    public KnightWithPowerArmor(Knight decorated) {
        super(decorated);
    }

    @Override
    public void blockWithShield() {
        super.blockWithShield();
        Armor armor = new PowerArmor();
        armor.block();
    }
}

public class KnightWithAdditionalHP extends Knight {
    public KnightWithAdditionalHP(Knight decorated) {
        super(decorated);
        this.hp += 50;
    }
}

Pedro: 两个装饰者就可以满足 FBI 的要求, 然后我们就可以着手制造看起来和 Galahad 差不多, 但是拥有强力盔甲和额外 50 点血量的超级骑士了.

Knight superKnight =
     new KnightWithAdditionalHP(
     new KnightWithPowerArmor(
     new Galahad()));

Eve: 这个特技加的可以.

Pedro: 接下来有请你来展示一下 Clojure 是怎么实现类似功能的.

Eve: 好的

(def galahad {:name "Galahad"
              :speed 1.0
              :hp 100
              :attack-bow-fn attack-with-bow
              :attack-sword-fn attack-with-sword
              :block-fn block-with-shield})

(defn make-knight-with-more-hp [knight]
  (update-in knight [:hp] + 50))

(defn make-knight-with-power-armor [knight]
  (update-in knight [:block-fn]
             (fn [block-fn]
               (fn []
                 (block-fn)
                 (block-with-power-armor)))))


(def superknight (-> galahad
                     make-knight-with-power-armor
                     make-knight-with-more-hp)

Pedro: 的确也可以满足要求.

Eve: 是的, 这里要提一下, 强力盔甲装饰器是个亮点. (译注: 亮点可能是使用了闭包. )

代理

Deren Bart 是一个调酒制造系统的管理员. 这个系统非常地死板难用, 因为每次调制完毕之后, Bart 都必须手动的从酒吧库存中扣除已使用的原材料. 把它改成自动的.

Pedro: 能搞到他代码库的权限么?

Eve: 不能, 但是他给了一些 API.

interface IBar {
    void makeDrink(Drink drink);
}

interface Drink {
    List<Ingredient> getIngredients();
}

interface Ingredient {
    String getName();
    double getAmount();
}

Pedro: Bart 不想让我们修改源码, 所以我们得通过实现 IBar 接口来提供一些额外的功能 - 自动扣除已用原料.

Eve: 怎么搞啊?

Pedro: 用代理模式 , 前几天我还看这个模式来着.

Eve: 讲给我听听呗.

Pedro: 基本思路就是, 所有已有功能依然调用之前标准的 IBar 实现来执行, 然后在 ProxiedBar 里提供新的功能

class ProxiedBar implements IBar {
    BarDatabase bar;
    IBar standardBar;

    public void makeDrink(Drink drink) {
       standardBar.makeDrink(drink);
       for (Ingredient i : drink.getIngredients()) {
           bar.subtract(i);
       }
    }
}

Pedro:

Pedro: 他们只需要把老的 StandardBar 实现类替换成我们的 ProxiedBar.

Eve: 看起来超级简单啊.

Pedro: 是的, 额外加入的功能并不会破坏已有功能.

Eve: 你确定? 我们还没有做回归测试呢.

Pedro: 所有的功能都是委派给已经通过测试的 StandardBar 去执行的啊.

Eve: 但是同时你还调用了 BarDatabase 扣除了已用原材料啊.

Pedro: 我们可以认为他们是解耦的 (decoupled) .

Eve: 哦......

Pedro: Clojure 里有什么替代方案么?

Eve: 这个, 我也不清楚. 在我看来你只是在用函数组合 (function composition).

Pedro: 怎么说.

Eve: IBar 的实现类是一组函数, 其它什么的各种 IBar 都不过是一组函数. 你所谓的一切额外加入的功能都可以通过函数组合来实现. 就好比在 make-drink 之后对酒吧库存进行 subtract-ingredients 操作不就行了.

Pedro: 可能用代码描述会更清晰一点?

Eve: 嗯, 不过我并不觉得这有啥特别的

(defprotocol IBar
  (make-drink [this drink]))


(deftype StandardBar []
  IBar
  (make-drink [this drink]
    (println "Making drink " drink)
    :ok))


(deftype ProxiedBar [db ibar]
  IBar
  (make-drink [this drink]
    (make-drink ibar drink)
    (subtract-ingredients db drink)))


(make-drink (StandardBar.)
    {:name "Manhattan"
     :ingredients [["Bourbon" 75] ["Sweet Vermouth" 25] ["Angostura" 5]]})


(make-drink (ProxiedBar. {:db 1} (StandardBar.))
    {:name "Manhattan"
     :ingredients [["Bourbon" 75] ["Sweet Vermouth" 25] ["Angostura" 5]]})

Eve: 我们可以利用协议 (protocol) 和类型 (types) 把一组函数聚合在一个对象里.

Pedro: 看起来 Clojure 也有着面向对象的能力啊.

Eve: 没错, 不仅如此, 我们还可以使用 reify 功能, 它可以允许我们在运行时创建代理.

Pedro: 就好比在运行时创建类?

Eve: 差不多.

(reify IBar
  (make-drink [this drink]))

Pedro: 感觉挺好用的.

Eve: 是啊, 但是我还是没有理解它和装饰者的区别在哪.

Pedro: 完全不一样啊.

Eve: 装饰者给接口增加功能, 代理也是给接口增加功能.

Pedro: 好吧, 但是代理......

Eve: 甚至, 适配器看起来也没啥区别嘛.

Pedro: 适配器用了另一个接口.

Eve: 但是从实现的角度来说, 这些模式都是一样的道理, 把一些东西包装起来, 然后把调用委派给包装者. 我觉得叫它们" 包装者 (Wrapper)" 模式更好一些.

桥接

一位来自人力资源代理机构 "Hurece's Sour Man" 的女孩需要审核应征者是否满足职位要求. 问题在于, 一般来说工作岗位是顾客设计的, 但是职位要求却是人力部门设计的. 给他们提供一个灵活的方式来协调这个问题.

(译注: " 工作岗位是顾客设计的" , 人力资源代理机构的顾客就是用人单位了. 也就是说工作岗位是用人公司设计的, 职位要求是人力资源代理机构设计的. )

Eve: 说实话我没看明白这个问题.

Pedro: 我倒是有点这方面背景. 他们的系统非常的奇怪, 职位要求是用一个接口来描述的.

interface JobRequirement {
    boolean accept(Candidate c);
}

Pedro: 通过实现这个接口, 来表示每一个具体的职位要求.

class JavaRequirement implements JobRequirement {
    public boolean accept(Candidate c) {
        return c.hasSkill("Java");
    }
}

class Experience10YearsRequirement implements JobRequirement {
    public boolean accept(Candidate c) {
        return c.getExperience() >= 10;
    }
}

Eve: 我好像明白了点.

Pedro: 你要谅解, 毕竟这个层次结构是人力部设计的.

Eve: 好的.

Pedro: 然后还有一个 Job 层级, 用来描述岗位, 和职位要求一样, 每个具体岗位都要实现 Job.

Eve: 为啥他们要把每种岗位都用一个类来表示? 明明一个对象就可以了啊.

Pedro: 这个系统设计的时候就是类要比对象还多, 所以你就先凑合着.

Eve: 类比对象还多? !

Pedro: 是的, 好好听别打岔. 岗位和职位要求是两个完全分离开的层级, 而且岗位是由用人单位设计的. 现在我们有请 桥接 (Bridge) 模式来关联这两个分离的层级, 并允许两者继续独立运转.

abstract class Job {
    protected List<? extends JobRequirement> requirements;

    public Job(List<? extends JobRequirement> requirements) {
        this.requirements = requirements;
    }

    protected boolean accept(Candidate c) {
        for (JobRequirement j : requirements) {
            if (!j.accept(c)) {
                return false;
            }
        }
        return true;
    }
}

class CognitectClojureDeveloper extends Job {
    public CognitectClojureDeveloper() {
        super(Arrays.asList(
                  new ClojureJobRequirement(),
                  new Experience10YearsRequirement()
        ));
    }
}

Eve: 桥呢?

Pedro: JobRequirement, JavaRequirement, ExperienceRequirement 是一个层级, 是吧?

Eve: 是啊.

Pedro: Job, CongnitectClojureDeveloperJob, OracleJavaDeveloperJob 是另一个层级.

Eve: 哦, 我明白了. 职位和职位要求之间的联系就是那个桥.

Pedro: 非常对! 这样以来人事部的人员就可以像这样来进行审核了.

Candidate joshuaBloch = new Candidate();
(new CognitectClojureDeveloper()).accept(joshuaBloch);
(new OracleSeniorJavaDeveloper()).accept(joshuaBloch);

Pedro: 总结一下要点. 用人单位使用抽象的 Job 以及 JobRequirement 的实现. 他们只需要大概描述一下岗位的情况就行了, 然后人力资源部门负责把描述转换成一组 JobRequirement 对象.

Eve: 明白了.

Pedro: 据我了解, Clojure 可以用 defprotocol 和 defrecord 来模拟这个模式?

Eve: 是的, 不过我想重温一下这个问题.

Pedro: 为啥啊?

Eve: 我们先整理一下套路: 顾客描述岗位, 人力资源部把它转换成一组职位要求, 然后在求职数据库里跑一段脚本去逐一尝试看没有没有符合要求的人员?

Pedro: 没错.

Eve: 所以这里还是存在依赖关系啊, 没有职位空缺的话 HR 啥也干不了.

Pedro: 这个, 算是吧. 但是他们还是可以在没有职位空缺的情况下设计出一组职位要求.

Eve: 目的何在?

Pedro: 提前搞出来, 留着以后碰见一样的要求就可以直接拿来用了啊.

Eve: 行吧, 但是这不就是自找麻烦了. 本来我们只是想要找到一种在抽象与实现之间协调的方式而已.

Pedro: 也许吧, 我想看看你是怎么在 Clojure 里用桥接模式解决这个特定问题的.

Eve: 简单. 用专设层级 (adhoc hierarchies).

Pedro: 要给抽象设置层级?

Eve: 是的, 岗位是抽象 层级, 然后我们只需要对其进行扩展.

(derive ::clojure-job ::job)
(derive ::java-job ::job)
(derive ::senior-clojure-job ::clojure-job)
(derive ::senior-java-job    ::java-job)

Eve: HR 部门就好比开发者 , 他们提供抽象的具体实现.

(defmulti accept :job)

(defmethod accept :java [candidate]
  (and (some #{:java} (:skills candidate))
       (> (:experience candidate) 1)))

Eve: 如果以后有新岗位出现, 但是岗位需求还没有被确认, 当然也没有与之对应的 accept 方法, 这个时候就会回退到上个层级.

Pedro: 蛤?

Eve: 假如某人创建了一个新的下属于 ::java 岗位的 ::senior-java 岗位.

Pedro: 哦! 如果 HR 没有给委派值 ::senior-java 提供 accept 实现, 多重方法就会委派给 ::java 对应的方法, 对吧?

Eve: 小伙子学的挺快嘛.

Pedro: 但是这还是桥接模式么?

Eve: 这里本来就没有什么桥 , 但是同样得以让抽象与实现可以独立地运转.

剧终.

速查表 (代替总结)

模式非常难以理解, 关于它们的介绍, 通常都是使用面向对象的方式, 再配上一堆 UML 图表和花哨的名词, 而且还是为了解决特定语言下的问题, 所以这里提供了一张迷你复习速查表, 希望能用类比的方式帮助你理解模式的本质.

  • 命令 (Command) - 函数
  • 策略 (Strategy) - 接受函数的函数
  • 状态 (State) - 依据状态的策略
  • 访问者 (Visitor) - 多重分派
  • 模板方法 (Template Method) - 默认策略
  • 迭代器 (Iterator) - 序列
  • 备忘录 (Memento) - 保存和恢复
  • 原型 (Prototype) - 不可变值
  • 中介者 (Mediator) - 解耦和
  • 观察者 (Observer) - 在函数后调用函数
  • 解释器 (Interpreter) - 一组解析树形结构的函数
  • 羽量 (Flyweight) - 缓存
  • 建造者 (Builder) - 可选参数列表
  • 外观 (Facade) - 单一访问点
  • 单例 (Singleton) - 全局变量
  • 责任链 (Chain of Responsibility) - 函数组合
  • 组合 (Composite) - 树形结构
  • 工厂方法 (Factory Method) - 制造对象的策略
  • 抽象工厂 (Abstract Factory) - 制造一组相关对象的策略
  • 适配 (Adapter) - 包装, 功能相同, 类型不同
  • 装饰者 (Decorator) - 包装, 类型相同, 但增加了新功能
  • 代理 (Proxy) - 包装, 函数组合
  • 桥接 (Bridge) - 分离抽象和实现

演员表

很久很久以前, 在一个很远很远的星系......

由于思维匮乏, 所有登场的名字都是字母倒置游戏.

  • Pedro Veel - Developer
  • Eve Dopler - Developer
  • Serpent Hill & R.E.E. - Enterprise Hell
  • Sven Tori - Investor
  • Karmen Git - Marketing
  • Natanius S. Selbys - Business Analyst
  • Mech Dominore Fight Saga - Heroes of Might and Magic
  • Kent Podiololis - I don't like loops
  • Chad Bogue - Douchebag
  • Dex Ringeus - UX Designer
  • Veerco Wierde - Code Review
  • Dartee Hebl - Heartbleed
  • Bertie Prayc - Cyber Pirate
  • Cristopher, Matton & Pharts - Important Charts & Reports
  • Tuck Brass - Starbucks
  • Eugenio Reinn Jr. - Junior Engineer
  • Feverro O'Neal - Forever Alone
  • A Profit NY - Profanity
  • Bella Hock - Black Hole
  • Sir Dry Bang - Angry Birds
  • Saimank Gerr - Risk Manager
  • Deam Evil - Medieval
  • Podrea Vesper - Eavesdropper
  • Deren Bart - Bartender
  • Hurece's Sour Man - Human Resources

P.S. 从刚开始提笔距今早已超过 两年. 时间飞逝, 物换星移, Java 8 也已经发布了.

clojure programming java story patterns

18 December 2015

辐射 (Fallout) 是 Bethesda 出品的游戏. 在游戏中有一种可驾驶的重型盔甲机器人. ↩

A long time ago in a galaxy far, far away... 出自电影<星球大战>. ↩

Tags: clojure