Decoratorパターンについて学んでみる

はいどーもこんにちは、時間も頭脳も足りない無職です。
本日はDecoratorパターンについて学んでみようと思いますよ。

勉強した結果の備忘録なので、間違ってたら土下座しま……せん!

Decoratorパターンとは

Decoratorパターンとは、いわゆるGoFのデザインパターンの一つです。
よく言われているのは

・インターフェイスが透過的
・緩やかな結合
・機能の追加が可能

あたりではないでしょーか。
これらができると何が嬉しいのかはおいおい見ていきます。

今回はとりあえずJavaScriptで実装してみたいと思います。
ただしJavaScriptにはインターフェイスがないので、その辺をちょっと工夫する必要があるかなあと思いました。

カードゲームのカードを作ってみる

今回はカードゲームで使う「1枚のカード」を作ってみたいと思います。
と言っても小難しいものを作る気はないので、今回は「名前」「攻撃力」「防御力」の3つだけを実装してみたいと思います。
まずカードのインターフェイスを決めます。

見たまんまですね。
名前と攻撃力と防御力のゲッターと、初期化処理用のメソッドだけ用意しています。
しかし見ての通り、インターフェイスだけでは役に立ちません。
というわけで具象クラスも作成します。

注*インターフェイスと言っていますが、実際にはただのクラスです。JavaScriptのベストプラクティスはわかりませんが、これはクラス+インターフェイスという考え方に縛られすぎた書き方かもしれません。また、多重継承も禁止されているので(class構文では)、インターフェイスとして使うには正直微妙です。それならいっそ単なるダックタイピングでいいかもしれません(実装してないときエラー吐かないのが嫌ですが)。

フィールドに対してプロパティのゲッターを割り当てています。
これで外部からは「値の参照はできるが値の変更はできない」という状況を作ることができました。
ただしPrivateフィールドを作るのがめんどくさいので「_」プレフィクスを付けているだけです。
this._nameとかに直接アクセスしようと思えば簡単にできるので、その点だけご注意ください。

ここまでの部分を実際に使ってみると、こんな感じ。

***通常***
名前:ゴリラ君 攻撃力:7 防御力:3

まあ普通ですね。

ロギングしてみたい

さてここまではふつーです。
別にナンチャラパターンとかないです。
問題はここからで、例えば「Game_Cardクラスを一切変更せずにロギング処理を加えたい」という場合があったとしましょう。
既存のクラスを変更せずに何かの機能を追加したいということはよくあります。
例えば既存のクラスがすでに十分複雑であり、余計な手を加えてクラスを壊したくない、とかですね(まあ壊れたことを発見するためのテストを書けっちゅー話でもあるのですが)。
あとは「本来関係ない処理をねじ込む場合」もそうでしょう。
今回のロギングもそうです。
Game_Card程度のクラスなら別に直接console.log()と書いても問題ないでしょうが、これがもっと巨大なシステムの一部だったら、余計なコードを加えたくないかもしれません。

と、こんなときにデコレータパターンが役に立ちます。
それではさっそくロギング処理を追加してみましょう。

注*console.log的な機能を追加する場合、アスペクト指向プログラミングという考え方もあります。ただ本題と関係ないのでこっちは省略。

Game_LoggingCardはコンストラクタでIGame_Cardを受け取り、this.parentに保存します。
すなわちこのクラスはIGame_Cardに依存します。
そしてまた、このクラスはIGame_Cardを保持します。
あとは見たまんま、this.parentの値をそのまま返しますが、その前にロギング処理を実行しています。

***通常***
名前:ゴリラ君 攻撃力:7 防御力:3
***ロギング***
名前はゴリラ君です。
攻撃力は7です。
防御力は3です。
ゴリラ君 7 3

以下の一行を実行するとき、ロギング処理が実行されているのがわかると思います。

元のGame_Cardを一切変更していないことに着目してくださいね。

アサーションの機能も追加してみる

特に深い意味はないですが、例として他のデコレーターも作ってみます。
今回はアサーション機能です。

initializeするときにアサーションを入れています。
具体的な使い方としてはこんな感じ。

****アサーション****
名前はゴリラ君です。
攻撃力はこーげきりょくだよです。
エラー発生でアサーション終了:atkが数値型ではありません

Game_CardをGame_LoggingCardでデコレートし、Game_LoggingCardをGame_AssertedCardでデコレートしています。
もちろんGame_CardをGame_AssertedCardで直接デコレートすることもできます。
なお

この一行はわざとエラーを起こすために直接値を変更しています。

カードの表示形式を変える

今度は攻撃力や防御力の表示形式を変えてみましょう。
これは「元のクラスを変更せずに機能を追加したい」場合にまさに活躍するパターンです。

コンストラクタでゲージの形と、名前を囲むデコレーションを注入します。
例えばHPが3のとき、「■」をゲージの形として渡したら「■■■」と表示されることになります。
こんな感じで使います。

****ビジュアライズ****
★★★チンパンジー君★★★
ATK:■ ■ ■ ■
DEF:■ ■ ■ ■ ■ ■ ■

ただ実際のプログラミングではatkやdefの型が数値型から文字列型に変わってしまうので、あんまりよろしくないかもしれません。
例えばGame_AssertedCardと組み合わせるとエラーが出ます。

もちろんロギング処理と合わせることもできます。

****ビジュアライズ+ロギング****
名前は★★★チンパンジー君★★★です。
攻撃力は■ ■ ■ ■ です。
防御力は■ ■ ■ ■ ■ ■ ■ です。
★★★チンパンジー君★★★ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■

デッキを作る

では単体のカードができたので、デッキを作ってみましょう。
と言ってもデッキは単なる配列で、おまけに総数は2枚ですw
実際にはデッキ用のクラスを作るんじゃないかなあと思います。
こんな感じ。

****デッキ****
うんちっち君の攻撃力は99で防御力は99
*うんちっちちゃん*の攻撃力は▲▲▲で防御力は▲▲▲▲▲

ご覧の通り、配列には別の型(Game_CardとGame_VisualizedCard)が入っていますが、例えばvalue.atkを呼ぶ側はそれがどちらの型であるのかを意識せずに使えます。
具体的にはIGame_Cardインターフェイスが持つプロパティやメソッドなら、クラスの利用者はその中身が何なのかを意識する必要がない、ということです。
言い換えれば「いくつデコレートしてもatkというプロパティの存在は変わらない」ということです。
これが透過的の意味です。

しかし例えばGame_VisualizedCardに独自のpublicメソッド(例えばshowImage)を追加した場合、IGame_Cardに依存している箇所でshowImageを使う、ということはできません。
例えば先ほどforEachを回している箇所で(仮にGame_VisualizedCardにshowImageというメソッドが実装されているとして)showImageを呼ぶと、Game_VisualizedCardではない型が配列に入っていた場合にエラーが投げられます。

もちろん「Game_VisualizedCard」に依存している箇所ではshowImageを使っても大丈夫です。
とはいえ具象クラスに依存し過ぎるとよろしくないので、showImageを持つクラスを複数作りそうなら、インターフェイスを用意するのがよい気がします。

注*最初にも書きましたが、インターフェイスを真似るのがJavaScriptのベストプラクティスかどうかはかなり疑わしいです。ダックタイピング的に動かす方がいいかもしれませんね……。ですがいずれにせよ、デコレータパターンの考え方は使えます。

終わりに

本日はデコレータパターンについて勉強してみました。
デザインパターンは普段から使ってこそだと思うので、積極的に使用していきたいと思いますよ(過剰に使うと不適切どうのこうのという話もありますが、そもそも「過剰に使う」レベルに達してからの話だと思うので、そのへんはあんまり難しく考えてません)。
全部終わってから思いましたが、普通にC#でサンプル書けばよかったですかねえ……。

最後にコード全体も貼っておきます。
ほなまた。

フォローする