メディア芸術データベースのラッパーGemをリファクタリングしました
これまでの流れ
- メディア芸術データベースのラッパーGemを作ってみました - Persistence
- 俺、Rubyについて全然分かってないじゃん
- 『Rubyによるデザインパターン』を読み終えて - Persistence
- イマココ
クラス設計
Before
Beforeでは、検索した結果をHashオブジェクトで返すだけという単純な機能だったので、クラス自体は多くありませんでした。それにしても、ComicがHttpBaseを継承しているのは不自然極まりないです。委譲でしょ、委譲。
After
Afterでは、検索した結果をその要素のオブジェクト(例えば、Comicを検索した場合はComicオブジェクト)の配列で返すようにしました。これにより、関連する項目(例えば、Comicの著者の詳細情報)を取りたい時に、利用者はidを使って再度検索する必要がなくなり、メソッドチェーンで取り出せるようになりました。
パターン
次に、意識したパターンについて解説します。
Builderパターン
Builderパターンは、次のようなパターンでした。
複雑なコンポーネントの組み立てを簡単に使えるようにするとともに、内部ロジックを隠蔽する。また、コンポーネントの組み立て方に誤りはないか、不足はないかなどのチェックを設けることもできる。
このGemではコンポーネント(クラス)の組み立てはありませんが、検索条件がたくさんありますので、検索条件を設定するためのクラス(SearchOptionBuilder)を作りました。これにより、利用者はどんな検索条件があるかは.methodsを見れば分かりますし、.buildメソッドは検索条件の必須項目をチェックしてから検索クラス(Search)が使うオブジェクトに整形して渡すことができます。
Template Methodパターン
Template Methodパターンは、次のようなパターンでした。
処理全体の流れは同じだが、一部が異なる処理が複数ある場合に用いる。
このGemでは、検索(Search)系の機能と、取得(Find)系の機能があります。FindはSearchで得た各要素のidを用いて詳細情報を取得するための機能です。これらはどちらも「HTTPリクエストをする」「結果を解析する」という同じ流れの処理をしており、これらをまとめた抽象クラスをRetrieveTemplateクラスとしました。Findの方はさらに共通処理が加わるため、FindTemplateを追加しました。XXXとYYYはそれぞれの機能や要素が入りますので複数あります。
Strategyパターン
Strategyパターンは、次のようなパターンでした。
全体の流れは同じである複数の処理があり、一部の処理を変更する必要がある場合。委譲にすることでクラス間の結びつきを疎結合にしたい場合。
このGemでの使い方が厳密にStrategyパターンかと言われるとちょっと自信がないですけど、委譲により解析処理を分けて必要なところで呼び出すようにしています。SearchやFindのクラスに解析処理を入れてしまうと、本来の検索するという目的とは関係のないメソッドがたくさんあって気持ち悪いんですよね。なんでFindクラスにHTML解析のメソッドがあるのって思ってしまいます。
Proxyパターン(仮想プロキシー)
Proxyパターン(仮想プロキシー)は、次のようなパターンでした。
本物のオブジェクトと同じ振る舞いをし、オブジェクトの実体が生成させるのをできるかぎり遅らせるためのプロキシーを仮想プロキシーという。
このGemでは、Searchの検索結果にその要素のオブジェクト(例えば、Comicオブジェクト)が返されますが、検索した時点ではサマリー情報しかありません。全部の情報を取るには、Findを使って詳細ページを取得する必要があります。そこで、Proxyパターンを使って、必要になった時に詳細情報を取得するようにしました。
class Component attr_reader :id def initialize(id, content = {}, retrieved = false) @id = id @content = content @retrieved = retrieved end def [](key) if @content.has_key?(key) @content[key] else unless retrieved? @content.merge!(@retriever.execute.content) @retrieved = true @content.has_key?(key) ? @content[key] : nil end end end def method_missing(name, *args) self[name.to_sym] end def content unless retrieved? @content.merge!(@retriever.execute.content) @retrieved = true end @content end def content_cache @content end private def retrieved? @retrieved end end class Comic < Component def initialize(id, content = {}, retrieved = false) super(id, content, retrieved) @retriever = FindComic.new(@id) end end # 以下同様に続く... #
例えば、SeachComicがComicオブジェクトを作るときには、idとcontent(サマリー情報)を渡します。idとcontentは検索結果を解析して取得しています。また、Comicのinitializeでは@retriverに詳細情報を検索するためのクラスを設定します。
Componentクラスの[]メソッドとmethod_missingメソッドはいずれもcontentへのアクセサです。[]メソッドの場合はcomic[:title]で取り出せますし、method_missingメソッドの場合はcomic.titleで取り出せます。method_missingは存在しないメソッドが指定された時に呼ばれるメソッドで、それをオーバーライドしています。
また、まだ取得していない詳細情報を取得しようとすると、@retrieved(詳細情報を取得したかどうか)がfalseであれば一度だけ@retriverを実行して詳細情報を取得してcontent(サマリー情報)と詳細情報をマージします。このようにして必要になった時に情報を取得するようにすることができました。
まとめ
デザインパターンを意識することで、複雑な構造を持つクラス群をわかりやすくまとめることができました。オブジェクト指向を勉強したら、その次にデザインパターンを勉強すると、より実践的なクラス設計の方法がわかると思いますのでオススメです!
- 作者: Russ Olsen,ラス・オルセン,小林健一,菅野裕,吉野雅人,山岸夢人,小島努
- 出版社/メーカー: ピアソン桐原
- 発売日: 2009/04/01
- メディア: 単行本
- 購入: 13人 クリック: 220回
- この商品を含むブログ (64件) を見る
また、作っていて気づいたことですが、このGemは他のサイトで同じことをやろうとした時でもクラス名とかパーサ部分を変えればほとんどこのまま使えます。クラスを疎結合にしたことで再利用性が高いコードになったんじゃないかと思います。
ソースはGitHubで公開していますので、ご意見をいただけたら喜びます!ブランチは「v1.0.0」です。まだmasterにはマージしていません。
RubyGemsの方はしばらくしたらアップデートします。