DCI(Data, Context and Interactions)というキーワードがRuby界で流行っているとか。
- DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien - Digital Romanticism
- DCIアーキテクチャについて語ってみるよ - uehaj's blog
まだよく消化できていないのですが(そもそもMVCだって理解できた気がしない)、PHPではどう実装すればいいかを考えてみました。
DCI概略
斜め読みしたところ、MVCのModelが肥大化しがちなところなので、じゃあModelをData、Context、Interactionに3層分割して実装すればすっきりしますよ、という概念だと読めました。実装によってはContextではなくUseCase、InteractionではなくRoleと書いていることもあるみたい。
DataとContextだけだったら、それサービス層を追加しただけじゃないの、とか思うのですが、問題はInteraction/Roleの部分です。
なんでModelが肥大化するかと言うと、「Viewのための特別メソッド」とかが増えるのがよくあるパターン。たとえばViewが「JSONとして出力したい」場合、JSON文字列を作るメソッドをModelに追加したりします。でも、そのメソッドはViewのある特定の状況下でしか使われることがない。。
責務的にはModelに持たせるのが正解なんだけど、ある文脈でのみ使われるメソッドをたくさん追加していって、その結果Modelが太って見通しが悪くなっていく。うーん、なんかこう、もやっとします。
"User"というModelクラスがあったとして、時にはAdministratorとして振る舞ってほしい、時にはViewerとして振る舞ってほしい、というように、場合によってUserの持つメソッド群を切り替えられると見通しがよくなってすっきりしそうです。
メソッドが必要かどうかなんて、『時と!!!事情に!!!よるだろ!!!!!』という感じ。
Scalaでの実装イメージ
オリジナルの記事にはScalaのtraitを使った実装例がありました。
Scalaのtraitはインスタンスの生成時にmixinできるので、「場合によってはAdministratorロールをmixinしてインスタンス化」とかが実現できるらしい。場合によってUserの振る舞いを変える。確かに自然な気がします。
Javaでの実装イメージ
DCI on Java について考えました - yojikのlog
この辺を読みました。Javaにtraitはないので、Role Objectパターンを使い、複数のDecoratorをうまいこと扱えるようにする感じか?
じゃあPHPだとどうなるの
DCIの実装方法は当然ながら言語依存性があります。PHPの場合、traitによる多重実装継承が可能ですが、Scalaのようにインスタンス生成時の編み込みはできません。宣言時に埋め込めるだけです。なのでScalaの例をそのまま移植することはできません。(それでもQIQなら…)
しかし動的言語だし、Javaよりは平易に実装できそうなもの。
ここまで読んできてふと思ったのが、「型キャストじゃだめなの?」ということ。
Userのインスタンスを、時と事情によってAdministratorクラスに変換したり、Viewerクラスに変換できれば、必要なメソッドを持ったインスタンスを取得できて、要求を満たすような気がしました。
//こういう継承構造になっていたとして
class User {}
class Admin extends User {}
class Viewer extends User {}
$user = new User;
//context内では必要に応じて$userをAdminに昇格
$admin = Admin::cast($user);
assert($admin instanceof Admin); //キャストされた
//必要に応じて$userをViewerに降格
$viewer = Viewer::cast($user);
assert($viewer instanceof Viewer); //キャストされた
//必要に応じて元に戻したり相互変換可能
$user = User::cast($admin);
思いつきだけだと説得力がないので、このインターフェースを実現する方法を考えてみます。
型キャストによるDCI(構想)
PHPそのものにクラスからクラスへのキャスト方法は用意されていませんので、自前で実装します。また、UserからAdminに変換する際に値をコピーして別のインスタンスを作ってしまうと、プロパティ値が連動せず不便です。「同じインスタンスの別の見せ方」として実装したいところ。そこでリファレンスを使い、プロパティは共有するようにキャストを実装します。
//カプセル化できてないけどこんなイメージ
class User {
var $name;
var $email;
static function cast(self $user)
{
$role = new static;
//プロパティ群をリファレンスコピー
$role->name =& $user->name;
$role->email =& $user->email;
return $role;
}
}
//Adminロール
class Admin extends User {
//Adminとしてのメソッドを定義
function greet() {
echo "Adminだよ\n";
}
}
//Viewerロール
class Viewer extends User {
//Viewerとしてのメソッドを定義
function greet() {
echo "Viewerだよ\n";
}
}
こんな風にしておけば、さっきの疑似コードが動きます。
$user = new User;
$user->name = 'タロー';
$user->email = 'taro@example.com';
$admin = Admin::cast($user);
$admin->greet();
$admin->name = 'ジロー';
echo $user->name; //$user側にも変更が入る
真面目な実装は?
上記の例だと、カプセル化できていません。で、ここからどう作るかはフレームワーク次第な気もします。
個人的にEntityをどう実装するべきか考えているのをspindle/spindle-types ・ GitHubに置いています。まとまったらまた書くかも。
雑感
型キャストでDCIを標榜してる実装は見つからなかったので、やっぱり何か問題があるのかな…。
- メンタルモデルと実装が合致してる気がする
- AdminやViewerはUserの派生クラスなので、instanceof Userであることに変わりはない。タイプヒントを通過させやすくて便利
- AdminとViewer両方のロールを兼ね備えたロールが必要になったらどうするの?
- AdminとUserとViewerは相互に変換できるので、必要に応じてキャストすればOK。もしどうしても同時に両方のメソッドが必要なら、traitで多重継承したらいいんじゃないの。
- ロールが山ほど小分けされていて、その組み合わせがたくさんある場合、多重継承で組み合わせただけのクラスがたくさん出来上がって困るかもしれない。
- キャストの際にクラス名がハードコーディングになっちゃう
- クラス名は文字列渡しできる。あとはDIコンテナを使えばいい。
//クラス名のハードコーディングをなくす $adminClass = 'Admin'; $admin = $adminClass::cast($user);
- クラス名は文字列渡しできる。あとはDIコンテナを使えばいい。
なんというか、オールドスタイルのクラスだとうまくいかない部分をtraitで強行突破した感じはあるかも。