Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Active RecordモデルをDecoratorで気持ちよく整頓しよう(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails: Active RecordモデルをDecoratorで気持ちよく整頓しよう(翻訳)

SingletonパターンやServiceパターン、Commandパターンなど、プログラミングでパターンとお付き合いしない日はありません。パターン同士が似すぎて結局何が違うのかわからなくなってくることすらあります。

そんなこんなで、私が自分用に構築したアプリや請負で構築したアプリで使っているのは、結局のところService(それともCommandだったかな?)パターンと、Decoratorパターンぐらいです。

ただし私の場合、app/ディレクトリ以下にservices/decorators/といったそれ専用のディレクトリは設けていません。少々風変わりだなと自分でも思います(私のアプリ編成の秘訣については、そのうち記事にするかもしれません: 何しろ複雑なことはやっていないので)。

今回はDecoratorパターンについて注目してみたいと思います。Decoratorを使うことで、(Active Record)オブジェクトの構造に手を加えることなく、代わりに「デコレーション」する、言い換えればオブジェクトをラッピングする🎁形で責務を追加できるようになります。

しかし私は、この手のパターンにはむやみに手を出しません。
多くのデータ処理は普通にActive Recordモデル内で行いますし、たいていはResource::Publishableなどのオブジェクトに関連付けられています。
ただし私は、Active Recordオブジェクトには画面表示用の「プレゼンテーションロジック」は置かないようにしています。私にはViewComponentという強い味方がいるので、プレゼンテーションロジックはそちらに配置しています。

それでは、最近手掛けたアプリを例として見てみることにしましょう。

class Resource < ApplicationRecord
  include Event::History, Identifiable, Lockable, Sluggable
  include Filterable, Renderable, Publishable

  belongs_to :owner

  delegated_type :resourceable, types: [Page, Chapter], dependent: :destroy

  def primary_field
    settings.find_by(key: "title").value.presence || "Untitled"
  end
end

このResourceモデルは、RailsのDelegatedType APIで使われる標準的な「親モデル」です。
上のprimary_fieldメソッドの配置は、私にとってはアンチパターンになっているので、以下のようにコンポーネントで定義すべきです。

class ResourceComponent < ApplicationComponent
  def initialize(resource)
    @resource = resource
  end

  def primary_field
    @resource.settings.find_by(key: "title").value.presence || "Untitled"
  end
end

これで、Resourceモデルからprimary_fieldを問題なく削除できるようになりました。

class Resource < ApplicationRecord
  include Event::History, Identifiable, Lockable, Sluggable
  include Filterable, Renderable, Publishable

  belongs_to :owner

  delegated_type :resourceable, types: [Page, Chapter], dependent: :destroy
end

モデルが常にきちんと片付いている方がずっと気持ちがいいですね!

しかし今度は、このアプリでリソースを何らかの形でリストアップするためのAPIエンドポイントが必要になったとしたら、primary_fieldメソッドをResourceモデルに戻すべきでしょうか?

そうすることも一応可能ですし、フィールドがたった1個なら私も無理に止めません。しかしそうしたフィールドが1個で済むことはめったにありません。
そんなときはDecoratorパターンを使うようにしています。

早速Decoratorを追加してみましょう。

class Resource < ApplicationRecord
- include Event::History, Identifiable, Lockable, Sluggable
+ include Decoration, Event::History, Identifiable, Lockable, Sluggable
  include Filterable, Renderable, Publishable

  belongs_to :owner

  delegated_type :resourceable, types: [Page, Chapter], dependent: :destroy
end

このDecorationというconcernは、Active Recordの任意のモデルに手軽に追加できる、実にシンプルなコードです。

# app/models/concerns/decoration.rb
module Decoration
  def decorate(with: nil)
    decorator_class = with || "#{self.class}::Decorator".constantize

    decorator_class.new(self)
  end
end

このメソッドは、そのコードが使われるクラス名(Resource::Decoratorなど)を利用しているクラスが存在するかどうかを探索します。

このクラスはresource/ディレクトリの下、つまりapp/models/resource/decorator.rbに配置することになります。それではこのクラスを作ってみましょう。

class Resource::Decorator < SimpleDelegator
  def primary_field
    settings.find_by(key: "title").value.presence || "Untitled"
  end
end

これで、Resourceに関連するすべてのクラスがここにまとめられます。クリーンでメンテナンスもしやすいですね!

これはどのように使えばよいでしょうか?

  • Resourceモデルにdecorateをチェインできます。
    例: Resource.find(1).decorate
  • コレクションの場合は以下のようにチェインできます。
    例: Resource.all.map(&:decorate)
  • オプションのwith属性を使うと、必要に応じて別のDecoratorも設定できます。
    例: Resource.find(1).decorate(with: AnotherDecorator)

🔗 SimpleDelegatorとは何か

上のコードにSimpleDelegatorというクラスがしれっと登場していることにお気づきでしょうか?これはRubyの標準ライブラリクラスであり、Decoratorパターンを実装するときのあらゆる「マジック」の源です。
SimpleDelegatorでオブジェクトをラップすると、特定の振る舞いだけを選択的にオーバーライド・拡張しながら、他のメソッド呼び出しを元のオブジェクトに委譲できるようになります。decoratorに存在しないメソッドは、それをラップしているオブジェクトに渡されるようになります。つまり、Resource.find(1).decorate.idと書いたとしても引き続き1が返されるようになります。

Ruby標準のツールを使った、ささやかで有用なテクニックを紹介いたしました。

関連記事

Rails: ActiveRecord::DelegatedType APIドキュメント(翻訳)

Rails: 私の好きなコード(1)大胆かつ的確なドメイン駆動開発(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。