ザネリは列車を見送った

ブログという名の備忘録

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

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

HTML/プレーンテキストでレポート出力再び

まずは原書通りに、Strategyクラス2つ(HTMLFormatter, PlainTextFormatter)を作成し、
Contextクラス(Report)に設定/差し替えができるようにしたものを書く。
ContextからStrategyメソッドの引数に必要な値(title, text)を渡すバージョン。

Strategyの引数にContext自身を渡し、必要な値をStrategyメソッド内で取得するバージョン。

こんな感じでformatterを差し替えて出力できる。

var x = new Report(new PlainTextFormatter())
x.outputReport() // プレーンテキスト形式のレポートをコンソールログ出力
x.formatter = new HTMLFormatter()
x.outputReport() // HTML形式のレポートをコンソールログ出力
Strategyを関数オブジェクトとして扱う

ReportクラスにStrategyクラスではなく、関数オブジェクトを渡すバージョンを書いてみる。

this の formatter の引数に this を渡している…。あれ、こんなことする必要はないのでは?
ということで、Strategy関数から引数を除去し、
関数内の this から必要な title, text を取得するようにしたバージョンがこちら。

実行例はこんな感じ。

var x = new Report(outputHTML)
x.outputReport() // HTML形式のレポートをコンソールログ出力
x.formatter = outputPlainText
x.outputReport() // プレーンテキスト形式のレポートをコンソールログ出力

ところで、formatter には何でも渡せてしまうので、
例えば文字列や数値など関数オブジェクト以外を渡すと
ReportオブジェクトのoutputReportを呼んだ時点でエラーになってしまう。

x.formatter = "hoge"
x.outputReport() // 「TypeError: Property 'formatter' of object #<Report> is not a function」が発生

これを何とかケアできないものか。

Strategyの妥当性チェック(蛇足)

試行錯誤した結論として、ここからの書き換えは労力に見合う効果は無いのでは?
と思うようになったが、せっかく色々やってみたので一応残しておく。
以下がoutputReportを呼び出したときに、
関数オブジェクトでなければワーニングを出すようにしたバージョン。

この変更の目的が「不正な値を設定したときにでも実行時エラーにならないようにする」
であれば概ね目的を満たせているようには思う。

var x = new Report(outputHTML)
x.outputReport() // HTML形式のレポートをコンソールログ出力
x.formatter = "hoge"
x.outputReport() // 「String is not a function」をワーニング出力
x.formatter = 1
x.outputReport() // 「Number is not a function」をワーニング出力
x.formatter = outputPlainText
x.outputReport() // プレーンテキスト形式のレポートをコンソールログ出力

さらに、そもそもoutputReport実行時にチェックするのではなくて、
formatter設定時に検知できたほうが親切では、と思い手を加えたバージョンがこちら。

一気にゴチャゴチャしたような感じがする。
setFormatter時にチェックするのがメインの目的で、
それならコンストラクタでもチェックしないと、とコンストラクタ関数でsetFormatterを呼ぶようにし、
getFormatter関数自体を上書きされてしまえばsetFormatterのチェックが行われずにoutputReportが呼ばれてしまう、と思い
結局outputReportでもチェックするようにした。
複数箇所から同一のチェックがしたいので、safeExecuteという関数にまとめた。
第一引数が関数オブジェクトであれば第二引数の関数を実行する。
コンストラクタで関数オブジェクトを渡さない場合、ワーニングメッセージを出してもいいのだけれど、
その状態で outputReport を呼んでもワーニングメッセージ「undefined is not a function」が出続けるオブジェクトができてしまい、
それならいっそエラーを投げてあげたほうが親切では、と思いこの形になった。
ただ、よくよく考えてみるとこの修正の出発点が「不正な値を設定したときにでも実行時エラーにならないようにする」だったはずなので、
本末転倒では、という考えに至った。
Report オブジェクトを this として扱わせるために「->」ではなく「=>」を使用したり、
セッターで渡された関数オブジェクトに bind(this)したりと、
色々工夫してそれなりに勉強にはなったと思うが、
ここは妥当性チェックを行わない strategy.coffee を採用することにしよう。
(CoffeeScript Cookbook にも Strategy パターンの例があるが、これもすごくシンプルに見える。)

specs2で単体テスト

最後にテストケース。
formatterを設定し直して出力形式を変えるテストも書いた。