Rails等のMVC構造の中でExhibit Patternを用いる場合の一例。
Exhibit Patternを何故用いるのか。
- 問題: MVCの中でも特にViewは煩雑になりがち
- 原因: Viewの中に複数の責任が同居しているからである
- 方針: ある側面でViewを捉え、特定の責任を別の層に切り分けるべきである
- 対策: Modelを描画するという責任を、Exhibit Patternを用いて切り分けよう
Exhibit Patternとは何か。
ModelをViewのcontextの中で描画する責任を負うもので、Decoratorの一種である。 描画することに特化しており、Viewについての知識はほとんど持っていない (例えばrenderというメッセージに対応できるものである、程度の知識は持つ)。 Modelはデータをどう扱うかというビジネスロジックに関する責任を持ち、 Exhibitはデータをどう描画するかという責任を持つ。
Decorator パターンの方針は、既存のオブジェクトを新しい Decorator オブジェクトでラップすることである。
その方法として、Decorator のコンストラクタの引数でラップ対象の Component オブジェクトを読み込み、
コンストラクタの内部でそのオブジェクトをメンバに設定することが一般的である。
Decorator パターンは、既存のクラスを拡張する際にクラスの継承の代替手段として用いられる。
継承がコンパイル時に機能を拡張するのに対し、Decorator パターンはプログラムの実行時に機能追加をする点が異なる。
Decoratorは、内包するデータに対して透過的でなければならず、Exhibitについても同様である。 つまり、Modelが対応できるメッセージには、Exhibitも同様に対応できる必要がある。 この制約を守るための実装として、DecoratorはしばしばProxyとして実装されることが多い。 Rubyでは、method_missingやSimpleDelegatorを利用してこれを実現できる。
Presenterも同じくDecoratorの一種であり、これもViewとModelの両方の知識を持つが、 PresenterはViewの複雑なロジックを引き受ける責任を負う。 Exhibitに比べるとView寄りであり、Viewについての知識をより多く持つ。
ExhibitとModelが必ず1対1でなければいけないということは無い。 表現したいものにより複数のExhibitが存在するはずで、多対多の関係を持つ。 これにより、複数のModelに共通する描画ロジックを1つのExhibitで表現でき、 また1つのModelが複数のExhibitを持てる。
例えばRailsには、Viewのロジック全般に責任を持つHelperという層が存在するが、雑多な責任は全てHelperに詰め込まれることになる。 Exhibitを導入することで、Modelの描画に関する責任を取り除くことができ、Helperの肥大化を解消できると考えられる。 Exhibit導入後のHelperは、特定のModelに紐付かない描画処理のためのロジックを請け負う責任を持つ。 (例: HTML5のfigureタグを描画するロジック)
Exhibit用のファイルをどこに置くか。
Railsの場合、app/{models,views, controllers} が標準で用意されている。 Objects on Railsでは、app/exhibits/*.rbというファイルを用意している。 Exhibitはアプリケーションに特化した層なので、app/exhibits以下に置くのは妥当と言える。
Exhibitをいつ利用するか。
Modelを特定のロジックに従って描画したいという要求が発生したときに利用する。 例えば、Modelの状態によって異なるテンプレートを描画したいとき、 条件分岐によってこれを実現することになるが、 このロジックをExhibit PatternによりViewから分離することが出来る。
Exhibitをどう実現するか。
例えば、Modelの状態(persisted?メッセージを送った結果)によって、描画するテンプレートを分岐させる。 まずSimpleDelegatorを利用してコンストラクタで受け取ったmodelに全てのメソッドを委譲する。 Viewのコンテキストを受け取り、条件に応じてrenderメソッドを呼ぶ(Double Dispatch Pattern)。 form_for等に利用されたときのことを考慮して、ActiveModelとしてのAPIを壊さないように気を付ける。
class Exibit < SimpleDelegator
def initialize(model)
super(model)
end
def render(view_context)
if persisted?
view_context.render(...)
else
view_context.render(...)
end
end
# for ActiveModel API constraint
def to_model
__get_obj__
end
# for ActiveModel API constraint
def class
to_model.class
end
end
<%= Exhibit.new(@entry).render(self) %>