スマートモデルがアンチパターンなケースもある
http://d.hatena.ne.jp/zetta1985/20091014/1255620654 の「1. 参照」例の、より良い実装より。
この実装だと、hogeListプロパティのカプセル化がいないので、内部を好き勝手にいじられてしまいます。
hogeListに意図する型以外の要素を格納されてしまったり、独自のList実装クラスを渡されてしまったり、nullを渡されてしまったり・・・
http://d.hatena.ne.jp/zetta1985/20091014/1255620654
意図せず、よくやってしまうパターンですね。Beanのあずかり知らぬところで、hogeListが好きに変更できてしまいます。
これを防ぐには、まずsetHogeListで防御コピーしてフィールドに保持しつつ、getHogeListでもコピーを返すようにする。実際にhogeListを操作したい時は、必ずBeanクラスに実装したメソッド(例えばaddHoge)を経由するようにする。配列でも同じ。cloneを保持して、cloneを返す。
add(element), add(index, element), remove(element), remove(index), get(index) くらいの基本的なメソッドを用意しておけば、大抵の用は足りる…かな?
ひとまず、こうすれば、意図しないhogeListの変更は防ぐことができるでしょう。
問題点1「不便」
ここで問題点。操作メソッドが、結構限定される。Listが本来持つ便利メソッドを利用することができない。
これで用は足りるかと思っていたけど、Iterator回しながらIterator#remove()したくなった。どうしよう? とか、ありがち。List#iterator()に委譲するメソッドもBeanに追加しよう、とか。clear()したいとかaddAll()したいとか。あれ、set(index,element)は?とか。
仕方ないので、Beanにメソッド足しますか?
問題点2「組み合わせ爆発」
さらに、モデルがもう少し複雑だった時。例えばBeanのメンバとしてfoo,bar,baz,quxと4つのリストを持っていた場合*1、4つのListとそれぞれに対する5つ6つの操作メソッドをBeanに実装…したくないw
使うモノだけ実装する、というYAGNIに従うのも手だけど、「そもそも誰がどんな使い方をするか完全には分からない、公開クラスライブラリのAPIを設計している」場合はどうしよう? fooにはclear()が提供されてて、barにはclear()が提供されない理由は? と聞かれて「俺は今のところ使わないから。使うことになったら追加するよw」では説得力があまりない*2。
というわけで、本来単純なJavaBeanだった(命名もBeanだし)はずのクラスが、このように肥大化していきます。
スマートモデル
このパターンは、スマートモデル(賢いモデル)と言えば良いんでしょうか*3。モデル構造が簡単なうちは良いのですが、複雑なモデルを構築する際には結構な足かせになってしまいがちです。
本来のBeanの責務は「値を保持して必要な時に提供すること」ですが、スマートモデルパターンでは、「バリデーション」の責務もBeanに負わせてしまっていることになります。
話は逸れますが、スマートUIというアンチパターンもありますね。UI、例えばボタンオブジェクトがビジネスロジックやデータアクセスの方法を知っているパターンです。これに似ているといえば似ているかもしれない。
http://www.ogis-ri.co.jp/otc/hiroba/technical/DDDEssence/chap2.html#SmartUI
Jiemamyでは
で、なんでこの話題に食いついたかというと。Jiemamyの設計でぶつかった問題だからです。v0.2.0をスクラッチで書き直すことにした当初、超絶スマートモデル方針でした。整合性のとれていない状態のモデルは存在できないようにしよう、と。
例えば、USERテーブルがGROUPテーブルを外部キー参照しているけど、キーカラム(USER#GROUP_ID)と参照先カラム(GROUP#ID)の型が違う等、DBに反映できない状態を一切モデルが保持できないように作っていこう、と。
テーブルに外部キーを設定する際にバリデーションをかければ、ひとまずOK? しかし、外部キーを設定した後にGROUP#IDの型を変更したら? GROUP#IDカラムを削除したら? そもそもGROUPテーブル自体を削除したら…?
等、様々な難しい問題が生じました。GROUP#IDやGROUPを削除したら、カスケードでUSER#GROUP_IDに対する外部キー制約も削除するのか、「FKで参照してるから消せません。先にFK消してね」っていうエラーを出すのか。どちらにせよ、モデルにこの処理を実装していくとなると…。
副作用のあるメソッドにコールバックを仕込んで、リスナを登録してもらって、イベント通知受けた方では状況を判断して…。あーー。TableModelクラスがどれだけデカくなるか、大体想像がつきますw
さらに、JiemamyはXMLでモデルを永続化します。さらにそのXMLはリポジトリにコミットし、コンフリクトがあった場合はマージすることを想定しています。つまり、Jiemamyのロジックが関与しないところでXMLが手編集される可能性があり、そこで整合性が崩れることも予想されます。
超絶スマートモデルですと、手編集で整合性の崩れたXMLは、デシリアライズ時にエラーとなり、Jiemamyで開く事ができなくなります。そりゃ困る。整合性がおかしくても、ひとまず最低限の体裁が整っていれば、とにかくエディタで開くことができ、エラーメッセージを出してくれる。そして編集によって整合性を取り戻す事ができなければなりません。
以上の理由から、Jiemamyにおいてはスマートモデルはアンチパターンでした。ちなみに、強調した通り、全てのケースでスマートモデルがアンチパであるとは言いません。使うべきシーンもあるかもしれないっす。
そんな経緯で、Jiemamyのコアモデルは「頭の悪いモデル」で作ってあります。コレクションは生で外に晒されているので、外部から編集し放題ですw
でもそうすると、整合性は簡単に崩れてしまう。なら、どうしたらいいのか、ということで、「モデルを直接触るのはprimitiveな手段」と位置付け、直触りの時は気をつけてネ( ̄ー+ ̄)と切り捨てました。
そして、安全に、整合性を維持しつつモデルを触るレイヤとしてJiemamyFacadeというインターフェイスを用意しました。(これがファサードパターンなのか、自分でもちょいと疑問に残ってますがw)
簡単に primitive API からモデルを操作することもできる。ちょっと一手間かかるけど、整合性を崩さず、なおかつ便利なメソッドを提供する facade API もある。という状態です。
さらに、facade API からモデルを操作すれば、セーブポイントとロールバック、というオトクな機能つきw どんな編集をしても、セーブしておけばUNDOできる。ロールバック前にsaveしておけば、REDOもラクラク。この機能をモデルに実装しようとした日にゃ、手がつけられなくなること請け合いです。
JiemamyFacade coreFacade = ...; TableModel tableModel = ...; String originalName = tableModel.getName(); SavePoint save1 = coreFacade.save(); coreFacade.changeModelProperty(tableModel, EntityProperty.name, "USER"); // テーブル名をUSERに変更 SavePoint save2 = coreFacade.save(); assertThat(tableModel.getName(), is("USER")); coreFacade.rollback(save1); assertThat(tableModel.getName(), is(originalName)); coreFacade.rollback(save2); assertThat(tableModel.getName(), is("USER"));
上記は、テーブルの名前を"USER"に変えてから、UNDO/REDOしてる例ですが、確かに分かり難い。単純に tableModel.setName("USER")ってしたい。俺もしたいw ただ、このようなメカニズムになっているが故に守られているメンテナンス性があるのも事実。
直感的な操作で、なおかつ便利で、保守性も高い。これがベストだけど、どうにもならないトレードオフだった。エディタでの操作を提供しようとしていたので、UNDO/REDOは普通に出来なきゃいけなかったしね。
っと。話が飛んだが。とにかくまぁ、DB要素(例えばTable)を抽象化したオブジェクトであり、その情報を保持し、必要な時に提供する、という純粋な責務を持つ「モデルレイヤ」。それに対して、整合性を維持しつつモデルを編集する*4、という責務を持つ「ファサードレイヤ」をかぶせた感じです。
ちょっと複雑になってくると、こういう工夫も必要なのかな、と思った次第です。