Pinocoでシンプルに正しく(&ぶっちゃけで)DIを理解する
Pinocoだって実はすごいんだぞ、Pimpleになんて負けないもん、というわけで、
PHPメンターズ -> Pimpleでシンプルに正しくDIを理解する
をPinocoで理解してみようというネタをやります。先にこれを見ておいてください。
あ、「Pinocoってなんやねん、あっちょんぶりけかよ」って思った人すみません。Pinocoは拙作のマイクロフレームワークです。いわゆるオレオレの一種ですが、オレオレにしてはかなりよくできたほうだと思います。RESTなAPIを作る他のと比べて、どっちかといえばWebサイトを作るほうが強い感じのヤツ。
https://github.com/tanakahisateru/pinoco
で、DIですよ。依存性注入ですよ。楽しんご的なアレじゃないですよ。オブジェクト指向ですよ。
Pinocoの第一印象はみんな「プレーンPHPっぽいね」で、それはそれで狙い通りなんだけど、いかんせん、そのせいで「この子、やればデキる子...」というのがわかってもらえないかわいそうな所があります。まあ作者がドキュメントを書かずにソース読んで一人で使ってるからダメなんですけどね。
以前、Pinocoで遅延評価がおいしいという話を、Pinoco0.4はクロージャですごくなる - なんたらノート 第二期 に書いています。Pinoco_Vars を PHP5.3 で使うと、遅延評価でいろいろやるとき超クールとかそんな話です。しかもこれがランタイム依存なしで、単独で使えるクラスだからこれだけ使ってもいいよ、とかそのあたりに言及しました。今回、これ前提でいくので、事前に読んでおくとわかりやすいですよ。
じゃ、やりましょう。
まず、PHPメンターズのお題を引用:
アプリケーションからニュースレターを送信する機能を実装しているとします。ニュースレターを送信するという責務を持つニュースレタークラスと、具体的にメールシステムを使ってメッセージの送信を行う責務を持つメール送信インフラクラスの2つに着目しましょう。
あくまで説明用のサンプルモデルなので、細かい設計の是非は論じません。ニュースレターオブジェクトの送信メソッドを呼び出すと、メール送信インフラを使ってニュースレターが送信されることとします。
というわけで、こっちでも全く同じことをやりましょう。メールを送るためのインフラになるクラスを作りますね。
おまけで、同じインターフェースの別のメーラー実装も書きました。
じゃあつぎ、こいつらをPimpleではなくPinocoで依存性として管理するにはどうするか。
だいたい似たようなもんですね。
Pinoco_VarsはDIコンテナのために設計されたわけではなく、汎用的なオブジェクトとして使うためにあります。なので、ただの代入はインスタンスへのプロパティアクセスになっちゃいます。遅延評価されるプロパティ専用のメソッドを使うあたりが、ちょっと見た目、DI専用に作られたPimpleより愚直な感じですが、まあ問題ない範囲ですよ。
さて、ここからがDIを理解しましょうという話になります。ここまでは、あくまで「DIコンテナの使い方」の話。
PHPメンターズ版と違って、まず先に答えをいうと、次のコードはDIがうまくできなくなるダメな設計です。
ここまでは、何も問題ないように思えます。コンテナで管理してるのは、まだあくまで、メール送信をするインフラのほうで、アプリケーション固有の機能は独自に new して使っています。NewsletterTransfer::send() の中で完結して動くようにベタに書いた、という段階ですね。これでも一応、単体テストは通るだろうからTDDしてもいいでしょう。
これだけのコードをカタカタと書いてる時間があれば、みなさんそろそろ、
「ニュースレター送信はメール送信インフラに依存してるじゃん」
という本質に気付くと思います。イエース、そのとおり。
「じゃあさ、NewsletterTransferのほうもDIコンテナに入れようぜ」... カタカタカタ
おやおやおや〜、慌ててそんなことしていいのかな〜?
「あ、そうだ、SendmailMailer を EchoMailer にすり替えたほうがテストしやすいじゃん、オレ頭いい〜。えーと、スタブって言うんだけっけ? こういうの。」... カタカタカタ
はい、ここ、「依存性」の「注入」じゃないですね。こういう編集作業をした時点で、依存性を手動でセットしているということになります。
単にDIコンテナと呼ばれるものを使うってことがDIなんじゃない。みたいなことを、「(ご)」←こんな感じのアイコンの偉い人が、PHPカンファレンスで言ってました。せっかく入れ替え差し替えでうまくできるオブジェクト指向なのに、その差し替えをロジック中に残したら意味ないですね。
で、このまま慌ててリファクタせずに作業を進めるとどうなるでしょうか?
「ステージングに入りまーす。本番モードようそろ〜」
_人人人人人人人人人人人人人人人人人_
> 突然の NewsletterTransfer 修正 <
 ̄^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
「ごめんなさい、モジュールのコードをフリーズしたというのは嘘でした」
一同「エー」
はいこれが依存性を注入しなかった代償です。
おっと、さらに焦った文系PG上がりのプロマネからこんな案が...
...こんなとこ辞めてやるフラグへまた一歩ですね。あるあるすぎますね。いったどれだけの、そして何年の間、 ->testmailer が偽り続けるのだろうか...
ちゃんとDIしましょう。まず、依存関係に対して、正直に正しくクラスを設計します。
大きな問題の1つは、NewsletterTransferクラスをパッと見て、具体的にどのクラスに依存しているのか、関連を持っているのか分からなくなっていることです。
というわけで、関係をちゃんとすると、
$container が $mailer になり、assert が消えてタイプヒントだけになりました。
NewsletterTransfer が本当に依存しているものだけ、DIコンテナから取り出して、個々に与えましょう。コンテナまるごと与えて好きにしろというのは、間違いだったのです。こうすることで、どのメール送信インフラを使うかを切り替える権限は、オブジェクトの設計から、DIの設定の方に逃がすことができました。
こういう部分部分のまとまりの良さを、OOP用語で凝集度といいます。一般的に、凝集度が高いほうが、テストも保守もレビューもしやすいです。
こうしちゃうと、たしかに、外から依存性を注入しないといけないので、すぐには動かせないかもしれません。でも、個々の部品から全体へのアクセス権を奪うことで、各実装が好き勝手できないようになります。どのモジュールが何に依存するのかという関係が、いつの間にか誰にも知られずに変わってる、なんて、ちょっとしたホラーですよね。現場ではよくありますが。
それにほら、システムのコンテナを渡してしまうのは、みんな大嫌いな、あのグローバル変数と同じことですよね。いつでもなんでもグローバル変数という、あのプログラムセンスを許せないなら、それと同じ意味のことをやっちゃダメ。
なぜグローバル変数がダメなのかは、いつどこで書き換えられるかわからない、ということ以外に、仕様を変えたいのに誰が使っているかわからない、というのも問題でしょ。スコープが閉じてなくて広すぎ、っていう問題。
以上、Pinocoでちゃんと動いてますよという証明のために、リポジトリ作りました。
https://github.com/tanakahisateru/pinoco-as-di-sample
ComposerがあるとPinocoを入れるのに便利ですが、なくてもエラーメッセージでだいたいわかります。PHPUnit で
phpunit --bootstrap bootstrap.php test/NewsletterTransferTest.php