この記事はRails Advent Calendar 2020の24日目の記事です。今月中旬に見てみたら24日だけ空いていたので参加を申し込みました。よろしくお願いいたします。
Ruby on Railsアプリケーションのディレクトリ構成としては、大きくmodelsとcontrollers、viewsがあります。ここにコードを書いていくことになります。実装が進むにしたがって問題になりやすいのが、Fat ModelやFat Controllerといったコードの肥大化に関する問題です。これは適切にデザインパターンを導入することで防ぐことができます。
この記事では、Railsにおけるデザインパターンとはなにか、その必要性やデザインパターンの一覧について書いていきます。なお、Railsアプリケーションの開発についてより詳しく学びたいときに参考になる本を次の記事でまとめています。あわせてご覧ください。
テクニカルライター。元エンジニア。共著で「現場で使えるRuby on Rails 5」を書きました。プログラミング教室を作るのが目標です。
Ruby on Railsにおけるデザインパターンとは
Railsにおけるデザインパターンとは、モデルやコントローラ、ビューに頻出する実装パターンを、オブジェクト設計の原則にもとづいて抽象化したパターンのことです。デザインパターンにしたがうことで、設計上の問題を防ぐことができます。
例:Formオブジェクト
たとえばFormオブジェクトというデザインパターンがあります。これはユーザーからの入力を整形・検証して永続化するという役割を持ちます。この処理はコントローラに書かれがちですが、このことはFat Controllerにつながりやすいです。
Formオブジェクトを導入することで、Fat Controllerの問題を防げます。またFormオブジェクト単体として再利用性が上がったり、テストが書きやすくなるといった利点もあります。
なぜデザインパターンが必要か
デザインパターンは、アプリケーションが大きくなるにしたがって起こりがちな設計上の問題を防ぐために必要になります。
代表的な問題がFat ModelやFat Controller、Fat Viewです。Railsはデフォルトでモデルやコントローラ、ビューを用意しています。用意されたこのファイル群だけにコードを記述していくと、オブジェクト指向設計の原則を守るのは難しいです。つまりコードが肥大化してしまいます。
こうなると拡張性や再利用性がなく、またテストも書きづらくなります。これを防ぐために、オブジェクト指向設計の原則に基づいてクラスを分割していく必要があります。
このクラス分割について、これまで開発者によって頻出パターンを抽象化されたのがデザインパターンです。Railsアプリケーションにデザインパターンを導入することで、Fat Modelをはじめとする設計上の問題を防ぐことができます。
前提条件
まず、デザインパターンを導入するときの前提として、オブジェクト指向設計の原則を忠実に守る必要があります。たとえばSOLIDの原則などです。原則を守りつつ、まずはRailsが用意したモデルやコントローラ、ビューのレイヤーにコードを書いていきます。このとき純粋はRubyオブジェクトを積極的に活用します。
こうしてコードを書いていた結果、パターンが現れたときに適切なデザインパターンの導入を検討します。デザインパターンの導入ありきにならず、まずはオブジェクト指向設計の原則と向きあうことが前提となります。
これについては@joker1007氏の「俺が悪かった。素直に間違いを認めるから、もうサービスクラスとか作るのは止めてくれ」でも詳しく書かれていますのであわせてお読みください。
Railsのデザインパターン一覧
Railsのデザインパターンについて、各責務とディレクトリ名、関連Gemがあればそれを書いています。このブログに別途記事を書いているパターンについては、記事へのリンクも貼っています。
- Decoratorオブジェクト
- Delivryオブジェクト
- Formオブジェクト
- Interactorオブジェクト
- Policyオブジェクト
- Queryオブジェクト
- Validatorオブジェクト
- Valueオブジェクト
- View Componentオブジェクト
Decoratorオブジェクト
モデルに対するビューのロジックをカプセル化する責務をもちます。ビューにはモデルに関連するロジックがよく登場しますが、ビューからモデルにアクセスすると肥大化の原因になり、また再利用性がなくテストも書きづらくなります。Decoratorオブジェクトでこの問題を防ぐことができます。
項目 | 値 |
---|---|
ディレクトリ | app/decorators |
関連するGem | active_decorator |
Deliveryオブジェクト
通知に関するロジックをカプセル化する責務をもちます。RailsにはデフォルトでActionMailerというしくみがあります。ただ、最近はメール以外にもSlackやLINEなどのチャットツールに通知することもよくあります。こういった通知に関する処理をラップするのがDeliveryオブジェクトです。
項目 | 値 |
---|---|
ディレクトリ | app/deliveries |
関連するGem | active_delivery |
Formオブジェクト
ユーザーからの入力を整形・検証して永続化する責務をもちます。ユーザーからの入力を処理するのはコントローラの役割です。ただ、複雑な処理や複数の場所で行われる処理をコントローラに書くと、コードの肥大化といった問題の原因になります。Formオブジェクトで、この処理をカプセル化することができます。
項目 | 値 |
---|---|
ディレクトリ | app/forms |
関連するGem | - |
関連記事 | Formオブジェクト |
Interactorオブジェクト
ビジネスロジックをカプセル化する責務をもちます。Interactorオブジェクトは、アプリケーションの一つのみのビジネスロジックをもちます。ビジネスロジックを一つしかもたないため、再利用性があり、テストも書きやすいです。二つ以上の処理はInteractorオブジェクトを組み合わせることで実現します。
同じような役割としてServiceオブジェクトがあります。ただServiceオブジェクトは定義があいまいで、オブジェクト指向設計の原則が守られづらいという問題があります。Interactorは役割が明確で、またGemによるルールのおかげもあり設計の原則を守りやすいという利点があります。
項目 | 値 |
---|---|
ディレクトリ | app/interactors |
関連するGem | interactor |
関連記事 | Interactorオブジェクト |
Policyオブジェクト
ビジネスルールをカプセル化する責務をもちます。たとえばユーザーの役割に応じて処理を実行する権限をもつかどうかを判断したりします。Policyオブジェクトがないと、コントローラやビューがルールに関するロジックであふれてしまうことになります。
項目 | 値 |
---|---|
ディレクトリ | app/policies |
関連するGem | pundit |
Queryオブジェクト
ActiveRecord::Relationに対して結合や絞り込み、ソートなどの操作を行い、Relationを返す責務をもちます。Relationに対する複雑な操作や再利用性のある操作を分割することで、再利用ができるなどの利点を得られます。ActiveRecord::Base継承クラスのスコープ経由で呼び出すことで、依存関係が明確になり、また返却されるクラスも自明になるという利点があります。
項目 | 値 |
---|---|
ディレクトリ | app/queries |
関連するGem | - |
関連記事 | Queryオブジェクト |
Validatorオブジェクト
ActiveRecord::Base継承クラスのレコードを検証する責務をもちます。複数の場所で利用される検証ロジックを書くことで、再利用性などの利点を得られます。
項目 | 値 |
---|---|
ディレクトリ | app/validators |
関連するGem | - |
Valueオブジェクト
値オブジェクトをカプセル化する責務をもちます。住所やメールアドレス、あるいは商品のレビューにおける星の数といった値オブジェクトを書くことで、再利用性などの利点を得られます。
項目 | 値 |
---|---|
ディレクトリ | app/values |
関連するGem | - |
関連記事 | Valueオブジェクト |
View Componentオブジェクト
ビューをコンポーネント単位でカプセル化する責務をもちます。再利用性のあるビューを、ビジネスロジックごとカプセル化することで、再利用性などの面で利点を得られます。
項目 | 値 |
---|---|
ディレクトリ | app/view_components |
関連するGem | view_component |
まとめ
RailsアプリケーションのFat ModelやFat Controllerといった設計上の問題は、デザインパターンを導入することで防ぐことができます。ただ、その前提としてオブジェクト指向設計について学び、その原則にしたがって設計する必要があります。
原則を守った上でデザインパターンを導入することで、アプリケーションが大きくなっても秩序のあるコードベースを保つことができます。