🗡️

良いConcernとは(翻訳)

2023/02/01に公開

英語記事: Good concerns
原文公開日: 2022/10/10
原著者: Jorge Manrubia


Railsのconcernsは、長年にわたり多くの批判を受けてきました。concernsはあらゆる問題を解決するものなのでしょうか? それとも何が何でもで避けるべきものなのでしょうか? concernsの問題点は、その自由度の高さ故に、自分の足を撃つような使い方もできてしまうところだと思います。結局のところ、concernsはRubyのmixinに定型的なコードを取り除くための糖衣構文を加えたものに過ぎないのです。

37signalsは、大規模なRailsのコードベースにおいてconcernsを何年も用いてきた経験があるので、この記事で私たちが使用している設計原則をいくつか紹介したいと思います。

Concernの置き場所について

Rubyのmixinは、多重継承の代替手段、すなわちクラス間でコードを再利用するための仕組みとしてよく紹介されます。私たちもこのような使い方をすることはあります。ただ最も一般的には、単一のモデル内でコードを整理するために使います。そして、私たちはそれぞれの場合で異なる規約を使用しています。

  • モデル共通のconcern:app/models/concernsに配置
  • モデル固有のconcern:モデル名と一致するフォルダapp/models/<model_name>に配置

例えば、これはBasecampのモデル固有のconcernの例です。

app/models/recording.rb
class Recording < ApplicationRecord
  include Completable
end
app/models/recording/completable.rb
module Recording::Completable
  extend ActiveSupport::Concern
end

この規約により、concernをincludeする際に名前空間を繰り返し指定する必要がなくなります。

コントローラの場合は、状況が逆転します。ほとんどのconcernsをcontrollers/concernsフォルダに置き、特定のサブシステムにのみ適用されるconcernsを、それにちなんだ名前のサブフォルダcontroller/concerns/<subsystem>に配置します。私たちのコントローラの設計方法については、別の記事で紹介したいと思います。

可読性を向上させる

concernsに対するよくある批判は、可読性を悪化させるというものです。私はその逆だと思います。正しく用いることで、二つの点で可読性を向上させることができます。

第一に、複雑さを管理するのに役立ちます。複雑なシステムを扱う上で大事なことは、何度も小さく分割することで、一度に一つのことに集中できるようにすることです。concernはまさにそれを実現するために、あなたの道具箱に加えるべきものなのです。

ここで重要なのは、各concernはホストとなるモデルの特徴を捉えた凝集度の高い単位であるべきだということです。すなわち、それらは共に属するものだけを含むべきです。大きなモデルを小さく分割するために、振る舞いや構造を好き勝手に放り込む入れ物としてconcernsを扱うべきではありません。クラス継承が "is a" の関係を必要とするように、concernsは "has trait" や "acts as" といった本物のセマンティクスを備えていなければならないのです。そうでなければ、むしろ害になってしまうでしょう。

以前お話したHEYのスクリーナーの例をご覧ください。HEYのユーザは、メールを自分へ送りたがっている他の連絡先からの許可申請に対する審査官の役割を持ちます。

class User < ApplicationRecord
  include Examiner
end

module User::Examiner
  extend ActiveSupport::Concern

  included do
    has_many :clearances, foreign_key: "examiner_id", class_name: "Clearance", dependent: :destroy 
  end

  def approve(contacts)
    ...
  end

  def has_approved?(contact)
    ...
  end

  def has_denied?(contact)
    ...
  end

  ...
end

このconcernは、許可申請の審査官というドメインの役割に合致しており、その役割に関連するコードのみが含まれています。これは保守性を高めます。なぜなら、常に管理しなければならない概念が少ないほど、物事を理解するのが容易になるからです。

第二に、concernsはドメインの概念を反映するための追加の抽象を提供します

以下は、HEYのTopicモデルが含むconcernsです。審査官の例と同じように、ほとんどの名前が、把握しやすいドメインの概念を捉えていることに注目してください。これらは、ドメインに類似した追加の機会を提供し、読みやすさを向上させます。

class Topic < ApplicationRecord
  include Accessible, Breakoutable, Deletable, Entries, Incineratable, Indexed, Involvable, Journal, Mergeable, Named, Nettable, Notifiable, Postable, Publishable, Preapproved, Collectionable, Recycled, Redeliverable, Replyable, Restorable, Sortable, Spam, Spanning

  ...

豊富なオブジェクトモデルを置き換えるのではなく、強化する

Railsのconcernsに対するよくある誤解は、クラス継承やコンポジションといった伝統的なオブジェクト指向の技術の代替だというものです。こちらを見てください。

ビジネスロジックは、concernsではなく、抽象 (やクラス) としてモデル化するのがよいでしょう。value objects、services、repositories、aggregatesなど、より適切なアーティファクトに価値を見出すことができます。

あるいはこちらを見てください。

コンポジションが好ましい

私はすべてを一つのファイルにまとめなければならないと言っているのではありません。ぜひとも、カスタムクラスにロジックを抽出して、それを呼び出してください。

これは間違った二項対立だと思います。concernsは、システムを適切に設計する必要性を制限したり置き換えたりするものではありません。特に、concernsは、適切なシステムを構築する上でオブジェクトの責務を上手く分配するために用いるべきであり、ファットでフラットなActive Recordモデルをなんとか小綺麗に整えるために使ってはならないのです。私は、最初にconcernsを用いた時にそのような混乱に陥ったので、それがconcernsを用いる際の重要な注意点だと身に染みています。

37signalsは、古き良きオブジェクト指向設計や、継承とコンポジション、設計と実装のパターンを大切にしており、modelsフォルダにはたくさんのPORO[2]があります。concernsはこのアプローチと非常に相性が良いのです。簡単な例で説明しましょう。

HEYでは、有料会員が解約しても、そのメールアドレスは永久に予約されたままです。そのため、システムがアカウントを停止させる際、すべてのデータを完全に削除する(incineration: 焼却)か、送信転送などの最小限のセットだけを残す(purging: パージ)かを選択します。コードの関連部分をお見せしましょう。

class Account < ApplicationRecord
  include Closable
end

module Account::Closable
  def terminate
    purge_or_incinerate if terminable?
  end

  private
    def purge_or_incinerate
      eligible_for_purge? ? purge : incinerate
    end

    def purge
      Account::Closing::Purging.new(self).run
    end

    def incinerate
      Account::Closing::Incineration.new(self).run
    end
end

焼却とパージは、共通のコードを持つ関連した操作です。これはどのように設計しましょうか? 操作をカプセル化したクラスを追加し、古き良き継承によって、共通部分を再利用するのです。

私は、呼び出し側の視点からは複雑なサブシステムが隠蔽されており、またモデル上で素敵なドメイン指向のAPIを提供するためにconcernsを用いているこのアプローチが大好きです。もし、あるアカウントを停止したいのなら、こう伝えればいいのです。

account.terminate

一方で、より冗長で流れの悪いやり方にはこちらのようなものがあるでしょう。

AccountTerminationService.new(account).run

また、アカウントの焼却およびパージの全てのロジックを扱うために、ファットなAccountモデルを用いていないことに注目してください。それらを担う三つのクラスによるサブシステムがあり、Accountモデルはそのサブシステムを使用するためのドアを提供するだけです。

concernsを用いることで、システム設計の自由度を犠牲にすることなく、整理されたモデルのコードに加え、これらのより簡潔で見栄えの良いAPIを実現できるのです。

結論

concernsは道具です。切れ味が鋭いと言うべきなのか、それともオープンすぎるのかは分かりませんが、使い方を誤るとトラブルの原因になることもあります。しかし、いくつかの簡単なガイドラインに従えば、Railsプログラマーにとって素晴らしい資源になると思います。

concernsと優れたオブジェクト指向設計の組み合わせは、素晴らしいコンボです。もちろん、concernsを用いれば、ソフトウェア設計の方法はもう知らなくてよくなる、ということではありません。それでも、コードの構成を改善し、より分かりやすく保守しやすいコードを実現するための実用的な仕組みなのです。

「素のRails(vanilla Rails)ではここまでしかできない」「その上に追加の構成要素やハーネス、規約が必要だ」という話をよく聞きます。ただ、BasecampHEYは伝統的なオブジェクト指向とパターンを用いた素のRailsアプリであり、そこではconcernsをたくさん用いているということをお伝えしておきます。


この記事は、Code I likeというRailsの設計技法に関する連載記事に属しています。

jorgemanrubia.com
写真:Vardan PapikyanUnsplash

脚注
  1. その他翻訳記事:
    第一弾: Domain driven boldness
    第二弾: Fractal journeys
    第四弾(TechRachoさん): Vanilla Rails is plenty
    第五弾: Active Record, nice and blended ↩︎

  2. (訳注)Plain Old Ruby Objectの略。 ↩︎

Discussion