リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳)

こんにちは、hachi8833です。今回はRailsのリファクタリングについての翻訳記事をお送りいたします。 概要 原著者より許諾をいただいて翻訳・公開いたします。 元記事: Refactoring Ruby: From Subclass to Registry 著者: Thiago Araújo Silva リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳) あるとき私のチームに、レガシーなRailsアプリ全体にレスポンシブな画像を実装するという面倒なタスクが舞い降りてきました。そのアプリはそれまで、クライアントのデバイスやviewport属性などおかまいなしに、縦横比率の合わない巨大な画像ファイルをWebブラウザに送信していたのです。ページ読み込みでユーザーが待たされるためにユーザーエクスペリエンスが損なわれ、コンバージョン率も低下していました。 この問題を解決するために、画像をリアルタイムで変換するオンザフライ画像サーバーをプロビジョニングしてCDNレイヤーに配置しました。最初のタスクは、このコンセプトでうまくいくかどうかを本番サーバーで小規模に試すことでした。配置した場所は本番サーバーですが、ホームページから離して配置したのでトラフィックはそれほどありませんでした。 最初のバージョン 使われているすべての基本インフラに合うレスポンシブ画像用のgemを探しましたが、今回の目的に合う既存のgemは残念ながら見当たりませんでした。そこで、以下の要件を満たすソリューションの開発をスタートさせました。 1つの画像(ActiveRecordモデル)を、1つのレスポンシブ画像に1対1対応させる レスポンシブ画像が複数フォーマット(HTML/CSS/JSON)に対応する フォーマットはアプリの内部外部ともに共有でき、プリロードやJS統合などに利用できる 解説: レスポンシブ画像とは、異なる画像サイズやデバイス/ピクセル比に応じて複数の画像ファイルをまとめたものです。 しばらくかかって、次のようなResponsiveImageクラスをこしらえました(実物とは異なります)。 class ResponsiveImage class << self attr_reader :versions def inherited(subclass) subclass.version ImageVersion.new(:original) end def version(*args) new_version = ImageVersion.new(*args) (@versions ||= {}).merge!(new_version.id => new_version) end end version ImageVersion.new(:original) def initiliaze(image) @image = image end def url(version_id) # version_idに対応するURLを生成 end # 表現形式を展開するメソッドをここに置く… end ご覧のとおり、このクラスは次のようなサブクラスに画像のバージョンの定義と収集機能を提供するためのものです。 class FreebieMainImage < ResponsiveImage version :sm, 173, 130, [nil, 599] version :md, 241, 181, [600, 991] # その他のバージョン… end # 属性は記事用に簡略化されています 上の2行目で宣言されている173×130ピクセルの画像は、viewportが0〜599ピクセルの場合に使われます。同様に、継承したフックで自動的にすべてのサブクラスの:originalバージョンが1つ宣言されます。宣言されたサイズに応じた画像をリクエストできるよう、次のコードで取得する画像サーバーへのURLはバージョンに応じて変わります。 # :sm バージョンのURLを返す responsive_image.url(:sm) ResponsiveImageオブジェクトは、最終的には表現形式の異なる画像を取得するときの単なるプロキシになりますので、特殊化したオブジェクトに次のように委譲されます。 class ResponsiveImage # … def to_picture PictureTag.new(self) end # … end # PictureTagなどのオブジェクトは、ResponsiveImageのバージョンとURLを読み出してHTML/CSS/JSON出力を生成する このときの最終的なビューは次のとおりです。 <%= FreebieMainImage.new(freebie.main_image).to_picture %> 上のERBは<picture>タグを出力します。FreebieMainImageで宣言された全バージョンの表示ルールとURLがこのタグに含まれます。 よくなった点 上の実装は、私たちの問題を解決するのにうってつけでしたが、いくつか問題が残りました。中でも、1つのエンドポイントに対するレスポンシブ画像を使いまわす必要があったことと、そのためにオブジェクトのインスタンス生成を何度も行わなければならないのがよくないと思えました。 画像使い回しのためにコントローラを使うのはいかにも悪手です。他の全コントローラで同じことをするはめになりがちだからです。 class FreebiesController def show freebie = Freebie.find(params[:id]) @freebie = FreebiePresenter.new(freebie) @main_image = FreebieResponsiveMainImage.new(@freebie.main_image) end end 何らかの形でコードを抽象化して、再利用しやすくする必要がありました。しばらく考えた末、レスポンス画像のファクトリーをモデルでグループ化するのが適切であると思えてきました。 このときのざっくりした設計は「Freebieモデルには1つのmain_imageがあり、コレクションでFreebieMainImage用の適切なプレゼンターを自動的に決定して、結果をハッシュ的なインターフェイス経由で渡す」というものです。 ざっくりとですが、以下のような青写真的コードを作ってみました。 # “freebie”モデルを読み取って、対応する正しい画像に対応付ける collection = ResponsiveImageCollection.new(freebie) # 最終的にレスポンシブ画像を出力する puts collection[:main_image].to_picture このときは最終的に以下のような実装になりました。 class ResponsiveImageCollection def initialize(model) @model = model @images = {} end def [](image_id) @images[image_id] ||= begin image = @model.public_send(image_id) klass = find_responsive_image_class(image_id) klass.new(image) end end private def find_responsive_image_class(image_id) “#{@model.class.model_name.name}#{image_id.to_s.camelize}”.constantize end end ご覧のように、画像のレスポンシブなクラスの探索はRailsらしい書き方になっています。モデル名がFreebieで画像名がmain_imageの場合は、FreebieMainImageというクラスを探索します。 FreebiePresenterの以下のメソッドによって、コードでのアクセスが容易になり、コントローラも汚さずに済むようになりました。 class FreebiePresenter # … … Continue reading リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳)