ザネリは列車を見送った

ブログという名の備忘録

CoffeeScript によるデザインパターン(Observer)

Rubyによるデザインパターン』をCoffeeScriptで書く試み。
目次

給料の変更を通知する

最初に、従業員を表す Employee クラスに
給料の変更を通知したいクラスのオブジェクトを直接渡すバージョン。

var e = new Employee("zaneli", "プログラマ", 100, new Payroll());
e.setSalary(200); // 「zaneliのために小切手を切ります! 」「彼の給料はいま200です! 」
e.setSalary(300); // 「zaneliのために小切手を切ります! 」「彼の給料はいま300です! 」
console.log(e.getSalary()); // 「300」

setSalary するたびに、経理部門にその変更が通知される。
変更を通知したい対象が増えた場合、この形では柔軟に対応できないのでさぁどうしよう、
ということで、次に Employee クラスにオブザーバを追加できるメソッドを用意し、
そこに変更を通知したいクラスを追加していくバージョン。

ここでは、update メソッドを持ったクラスのみ受け入れるように
addObserver で存在演算子「?」を使用してチェックしている。

var e = new Employee("zaneli", "プログラマ", 100);
var p = new Payroll();
var t = new TaxMan();
e.addObserver(p, t, null, 1, "hoge");
e.setSalary(200); // 「zaneliのために小切手を切ります! 」「彼の給料はいま200です! 」「zaneliに新しい税金の請求書を送ります!」
e.deleteObserver(t);
e.setSalary(300); // 「zaneliのために小切手を切ります! 」「彼の給料はいま300です! 」
console.log(e.getSalary()); // 「300」

上記のように update メソッドを持ったクラス以外(null, 1, "hoge")を渡すと、

undefined has no method 'update'
Number has no method 'update'
String has no method 'update' 

とワーニングメッセージを出力するようにした。
また、「e.observers = [null, 1, "hoge"]」とオブザーバを直接上書きすることもできてしまうため、
notifyObservers でもupdate メソッドを持っている場合のみ実行するようにした。

Observerを関数オブジェクトとして扱う

Strategy パターンのときと同様の流れだが、
オブザーバである Payroll と TaxMan をクラスではなく関数オブジェクトとして表してみる。

var e = new Employee("zaneli", "プログラマ", 100);
e.addObserver(payroll, taxMan, null, 1, "hoge");
e.setSalary(200); // 「zaneliのために小切手を切ります! 」「彼の給料はいま200です! 」「zaneliに新しい税金の請求書を送ります!」
e.deleteObserver(taxMan);
e.setSalary(300); // 「zaneliのために小切手を切ります! 」「彼の給料はいま300です! 」
console.log(e.getSalary()); // 「300」

addObserver でのチェック方法、notifyObservers での実行方法が少し変わっている。
notifyObservers 時に「observer?.call?(@)」と Employee を this として呼ぶのではなく、
addObserver 時に「@observers.push observer.bind(@)」と
Employee を bind しても意味的には同じだと思っていたが、
こうすると deleteObserver できなくなる(observer.bind(@) と observer は別物なので)。

オブザーバに対する責務を分離する(継承)

Employee クラスは本来従業員を表すためのもので、
notifyObservers, addObserver, deleteObserver を直接持っているのはあまりよろしくない。
オブザーバを取り扱う Subject クラスを用意し、それを Employee クラスが継承する形にしてみる。

これ以降、実行例は observer3.coffee と同じため割愛。

オブザーバに対する責務を分離する(関数オブジェクト単位のミックスイン)

次に、ミックスインにより Subject の機能を Emplayee にオブザーバに対する責務を持たせてみる。
CoffeeScript(JavaScript)でのミックスインの実現はいくつか方法があるようだ。
まずは、The Little Book on CoffeeScript に紹介されているミックスインを参考に、
関数をそのままミックスする手法を試してみる。

うーん…これはこれで面白いやり方だけど、
ひとまとまりの機能であるはずの notifyObserversFunc, addObserverFunc, deleteObserverFunc を
それぞれ独立して定義しているのがイマイチ。
やはり Subject クラスのように、ひとまとまりの機能を表すクラスをミックスしたい。
The Little Book on CoffeeScript には「クラスの拡張」としてこれの実現方法を紹介しているが、
ミックスインの実現のために Module クラスを継承しなくてはいけないのがどうも本末転倒な気がする。
例えば本当に継承関係にしたい Person クラスなどがあった場合、Employee は Person を継承しつつ、
Subject もミックスインするようにしたい。
The Little Book on CoffeeScript での方法では、
Module を継承したうえで、Person も Subject も同列でミックスインする、
という形で書くことになりそうだ。

オブザーバに対する責務を分離する(クラス単位のミックスイン)

CoffeeScript Cookbook にも Mixins for classes の紹介があるが、
こちらのほうが前述の希望を満たせそうだ。
「class Employee extends mixOf Person, Subject」とすれば、
mixOf の第一引数が本来継承したいクラス、
カンマ以降の第二引数以降がミックスインするクラスとして表すことができる。
さっそくこの形で実装を、と思ったのだが、
CoffeeScript サイトの TRY COFFEESCRIPT では問題なく JavaScript に変換できたものの、
Play framework ではパースエラーになってしまった。
どうも「class Employee extends (mixOf Person, Subject)」と
extends 以下をカッコで囲まないと上手く解釈できないようだ。
個人的には、カッコの有無では結構見栄えに影響すると思うのだが仕方ない。

specs2で単体テスト

テストケースでは、例によってこれを利用するのだが、
今回、単体テストでのみ読み込む JavaScript をいくつか手直しする必要があった。

まず、 console.log だけでなく console.warn で警告メッセージも出すようにしたので、
これも定義することにした。
(debug, info, error なども一緒に定義しておいたほうが良かったかもしれないが、今回は手抜き…)
また、これは specs2 で使用している selenium の都合か、配列の indexOf が実行できず
deleteObserver でエラーになってしまったので、
IE8 を Array:indexOf に対応させる」を参考に回避コードを入れた。
(IE8 に対応させるためには for-unittest.coffee ではなく
プロダクトコードでも同様の対応が必要そうだが、今回は手抜き…)
で、テストケースはこちら。

  • payroll を追加
  • taxMan を追加
  • payroll, taxMan を追加
  • payroll, taxMan を追加し、taxMan を削除
  • 不正なオブザーバ(null、文字列、数値) を追加
  • payroll を追加し、taxMan を削除(追加していないオブザーバを削除するケース)
  • 不正なオブザーバ(null、文字列、数値) を直接上書き

のテストを実行している。