読者です 読者をやめる 読者になる 読者になる

Persistence

続けることに意味がある。

メディア芸術データベースのラッパーGemをリファクタリングしました

これまでの流れ

クラス設計

Before

f:id:bisque3311:20150405122900j:plain

Beforeでは、検索した結果をHashオブジェクトで返すだけという単純な機能だったので、クラス自体は多くありませんでした。それにしても、ComicがHttpBaseを継承しているのは不自然極まりないです。委譲でしょ、委譲。

After

f:id:bisque3311:20150405122912j:plain

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(サマリー情報)と詳細情報をマージします。このようにして必要になった時に情報を取得するようにすることができました。

まとめ

デザインパターンを意識することで、複雑な構造を持つクラス群をわかりやすくまとめることができました。オブジェクト指向を勉強したら、その次にデザインパターンを勉強すると、より実践的なクラス設計の方法がわかると思いますのでオススメです!

Rubyによるデザインパターン

Rubyによるデザインパターン

また、作っていて気づいたことですが、このGemは他のサイトで同じことをやろうとした時でもクラス名とかパーサ部分を変えればほとんどこのまま使えます。クラスを疎結合にしたことで再利用性が高いコードになったんじゃないかと思います。

ソースはGitHubで公開していますので、ご意見をいただけたら喜びます!ブランチは「v1.0.0」です。まだmasterにはマージしていません。

github.com

RubyGemsの方はしばらくしたらアップデートします。

次のステップ

  • Railsブロンズ試験(4/25)の勉強
  • Rspecの勉強