ザネリは列車を見送った

ブログという名の備忘録

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

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

単独のタスクと、それらが集まった複合タスクを同一に扱う

タスクにはそれのみで所要時間を持つ最小単位のもの(単独タスク)と、
いくつかのタスクをまとめたもの(複合タスク)があるとする。
まず、単独タスクをふたつ、それを自身にサブタスクとして登録できる複合タスクをひとつ作る。

第一引数に畳み込みに使用する関数を、題二引数に初期値を取る。
名前を返す関数getName, 所要時間を返す関数getTimeRequired は
全てのタスクの親クラスであるTaskに定義し、
子クラスのコンストラクタでそれぞれの名前、所要時間を設定するようにした。
複合タスクの所要時間は自身のサブタスクの所要時間を合算したものとしており、
畳み込み関数「@subTasks.reduce ((sum, time) -> sum + time.getTimeRequired()), 0)」を使用して
計算している。

var task = new MakeBatterTask();
console.log(task.getTimeRequired()); // 「4」
var subTask = new AddDryIngredientsTask();
task.addSubTask(subTask);
console.log(task.getTimeRequired()); // 「5」
task.removeSubTask(subTask);
console.log(task.getTimeRequired()); // 「4」
複合タスクを表すクラスを定義する

composite1.coffee では単独タスクも複合タスクもTaskを継承し、
個別の複合タスクでサブタスクを扱うメソッドを用意していたが、
複合タスクごとにいちいち個別に定義していては辛いので複合タスクを表すクラスを定義し、
個別の複合タスク(今回の例では MakeBatterTask)がそれを継承する形に修正する。

MakeBatterTask(どうでもいいけどバターではなく Batter。こねもの、みたいな意味らしい。
今回は小麦粉と砂糖をこねたものを指すようだ。
しっくりくる訳が見当たらなかったので"ケーキの生地"くらいに捉えておこう)自体は、
その複合タスクを構成するサブタスクの登録のみでシンプルになった。
サブタスクの追加、削除は親クラスである CompositeTask でまかなうようにしている。
(実行例は composite1.coffee と同一のため割愛)

タスクを JavaScript の配列として扱う

原著では Ruby で配列要素を操作する演算子をオーバーロードすることにより
配列のように扱う例を示しているが、
JavaScript の場合配列操作に演算子ではなく push, pop, shift, unshift などのメソッドを使用しており、
また演算子オーバーロードも容易ではないため(beyond.js というライブラリを使えば実現可能らしい)
今回は割愛する。
CompositeTask が Array を継承したパターンはこちら。

Arrayクラスのメソッドをそのまま使用して、サブタスクの追加・削除(取り出し)・参照などができる。

var task = new MakeBatterTask();
console.log(task.getTimeRequired()); // 「4」
var subTask = new AddDryIngredientsTask();
task.push(subTask);
console.log(task.getTimeRequired()); // 「5」
task.pop();
console.log(task.getTimeRequired()); // 「4」
console.log(task[0].constructor.name); // 「AddDryIngredientsTask」
サブタスクに自身の親タスクを持たせる

これは原書のRubyとそんなに代わり映えしないが、
サブタスクの追加時にサブタスクの親タスクとして自身を設定し、
サブタスクの削除時にサブタスクの親タスクを null に設定するバージョン。

サブタスクから、自身の親を参照できている。

var task = new MakeBatterTask();
console.log(task.getTimeRequired()); // 「4」
var subTask = new AddDryIngredientsTask();
task.addSubTask(subTask);
console.log(task.getTimeRequired()); // 「5」
console.log(subTask.parent === task); // 「true」
task.removeSubTask(subTask);
console.log(task.getTimeRequired()); // 「4」
console.log(subTask.parent); // 「null」
specs2で単体テスト

最後に、いつも通りテストを書…こうとしたらテストケースで配列の reduce が実行できなかったため、
単体テストでのみ読み込む JavaScript で実装することにした。
Array.prototype.map の自前実装を参考に。

25行目以降が今回追加分。
で、テストケースはこのように。