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

Railsで導入してよかったデザインパターンと各クラスの役割について

3年ほどRailsを書いてきてある程度知見が溜まってきたので、忘れないためのメモとしてKPTと導入例を交えながらダラダラと書いています。

見出しの命名規則は クラス名/ディレクトリ名の単数形をupper camel caseにしたもの + KPT です。

Keepは今後も使うもの、Problemは開発規模によっては問題が発生する(した)もの、Tryは現在使用していないが使用したほうが良いと思っているものです。

これらすべてを導入すれば上手くいくというわけでもないので、開発規模に合わせて適切に採用していくと良いと思います。

DDDやデザインパターン等見聞きはしているものの詳しいわけではないので間違っている部分等あるとは思うのでその辺りはコメントでご指摘お願いします。

はてブコメント欄で頂いた指摘内容等についてはまとめの後でまとめて返答を記載しています。

Asset (Keep)

app/assetsに配置します。

最近はフロントエンド開発ではGruntを使いassetsを使わないというパターンもあるようですが、

現状ではそうする必要があるほどのフロントエンド開発をやったことがないので私は今後もassetsを使っていくと思います。

Controller (Keep)

app/controllersに配置します。

クラス名の命名規則として、接尾辞にControllerを付与します。

Controllerの仕事としては、パラメーターやセッションの状態、Validation結果による処理の分岐、ページ遷移、テンプレートの出し分けに留め、

具体的なビジネスロジックについては下記に紹介するService, Form, Factory, Validator等に任せてしまったほうが良いと思います。

Decorator (Keep)

app/decoratorsに配置します。

クラス名の命名規則として、接尾辞にDecoratorを付与します。

Draperというgemがとても便利で、READMEがわかりやすいのでそちらを読むのが良いと思います。

GitHub - drapergem/draper: Decorators/View-Models for Rails Applications

Helperの代替として、Modelの状態に応じたViewの文字列の出し分け、文字列のフォーマットを行います。

Exception (Problem)

app/exceptionsに配置します。

クラス名の命名規則として、接尾辞にExceptionを付与します。

Railsの開発ではraise "message"でRuntimeErrorを投げて済ませることが多いため必須ではないと思います。

必ず補足したい特異な例外のみExceptionクラスを実装し、それ以外ではRuntimeErrorで済ませるくらいの使い方が開発速度を落とさずに実装できて良いかと思います。

結局のところ例外設計が難しいという問題があるので導入については慎重に検討したほうが良いかなと思います。

Factory (Keep)

app/factoriesに配置します。

クラス名の命名規則として、接尾辞にFactoryを付与します。

Factoryでは主に以下の3種類の処理を行います。

複数のオブジェクトから単一のデータオブジェクトの生成

# 現在ログイン中のユーザーのユーザー名、投稿内容を持った
# comment_confirm_formインスタンスの生成
comment_confirm_form = CommentConfirmFormFactory.create!(
  current_user, 
  comment_params
)

・データ構造を持たないデータからデータオブジェクトの生成

# CSVを読み込み、在庫データオブジェクトを生成
CSV.open("/tmp/path.csv") do |row|
  stock_parameter = StockParameterFactory.create!(row.fields)
end

・共通のインターフェースを持つオブジェクトの透過的生成

# CreditCardPaymentServiceの生成
payment_service = PaymentServiceFactory.create!(:credit_card)
# CarrierPaymentServiceの生成
payment_service = PaymentServiceFactory.create!(:carrier)

上記3種類に属さないインスタンスの生成に関してはFactoryを作る必要は無いと思います。

Form (Keep)

app/formsに配置します。

クラス名の命名規則として、接尾辞にFormを付与します。

Modelの生成に必要なデータを入力するformを作る際、

ある場面ではバリデーションAを実行し、ある場面ではバリデーションBを実行するということは頻繁に発生するため、

formからのデータ送信は原則として一度Formオブジェクトにしてしまうのが良いと思います。

APIリクエストの入力値も一度Formに入れています。

Helper (Problem)

app/helpersに配置します。

クラス名の命名規則として、接尾辞にHelperを付与します。

お馴染みのHelperですがグローバルなメソッドなので、グローバル変数と同じような問題が発生するということはよく言われており、Decoratorに取って代わられようとしてします。

個人的には、Modelの状態によってCSSのクラス名を出し分けるような極めてフロントエンドよりな処理の場合に関してはDecoratorよりもHelperに処理を置いたほうが責務がはっきりするかなと思っています。

とはいえ、最近ではAngularJSやReactJS等があるためHelperで頑張らなくても良いと思います。

Job/Worker (Keep)

app/jobsまたはapp/workersに配置します。

クラス名の命名規則として、接尾辞にJobまたはWorkerを付与します。

ActiveJobやSidekiqなどでお馴染みの非同期処理を行うクラスです。

Model (Keep)

app/modelsに配置します。

Modelにはassociaion, scope, enum, データベースレベルでの制約(not null, unique等)に関するvalidation, 状態の取得、変更に関するメソッドのみ記述するのが良いかなと思います。

scopeについては後述するRepository/Finderに記述しても良いかと思っていますが、この辺りは検討中です。

Notifier (Keep)

app/notifiersに配置します。

クラス名の命名規則として、接尾辞にNotifierを付与します。

通知に関するクラス群です。

ActionMailerに関してもNotiferでラップしたほうが良いと思います。

一例として、例外の通知をHipChatに行っていたが、チャットツールの移行に伴いSlackに通知したくなったといった場合に

Notifierの中だけ修正すれば良いという状態になっているのが理想だと思います。

Parameter (Keep)

app/parametersに配置します。

クラス名の命名規則として、接尾辞にParameterを付与します。

複雑な属性を持つデータを格納するデータオブジェクトです。

CSVから読み込んだデータを格納するデータオブジェクトや、複数のオブジェクトから生成されるデータオブジェクトとして使用します。

# ログイン中のユーザー情報、カートの情報、
# 入力されたクレジットカードの情報を元に決済用データオブジェクトを生成
payment_parameter = PaymentParameterFactory.create!(
  current_user, 
  cart_form
)
# 決済処理を実行
payment_service = PaymentServiceFactory.create!(:credit_card)
payment_service.pay!(payment_parameter)

命名がParameterで良いのかという部分については悩んでいますが、こういったデータオブジェクトがあったことは便利でした。

Repository/Finder (Try)

app/repositoriesまたはapp/findersに配置します。

クラス名の命名規則として、接尾辞にRepositoryまたはFinderを付与します。

複雑な検索処理を記述するクラスです。

Model(ActiveRecord)がDDDで言うRepositoryの機能を持っているため、classとして実装するのではなくconcerns moduleとして実装し、Modelでincludeしてしまうのが良いかなと思っています。

module UserRepository
  extend ActiveSupport::Concern

  module ClassMethods
    def find_by_oauth_token(provider, token)
    end
  end
end

class User
  include UserRepository
end

Resource (Try)

app/resourcesに配置します。

クラス名の命名規則として、接尾辞にResourceを付与します。

近年マイクロサービス化が叫ばれており、各システムで使用する共通機能を提供するAPIシステムというものも増えてきていると思います。

RailsにはActiveResourceというマイクロサービスのための機能があるため、その機能を使用して実装したクラスはResourceとして定義するのが良いと思います。

サービスがRESTfulでなく、ActiveResourceを使用しない場合でも外部APIを使用するのであればResourceとして良いのかなと思っています。

Service (Keep)

app/servicesに配置します。

クラス名の命名規則として、接尾辞にServiceを付与します。

Controller, Form, Model, Repository, Resource, Task, Util, Validator以外のビジネスロジックに関する処理を記述します。

何をServiceとするかについては設計が難しい部分だとは思いますが、専門性の高い処理はほとんどここに配置することになると思います。

Task (Keep)

app/tasksに配置します。

クラス名の命名規則として、接尾辞にTaskを付与します。

rake taskの実処理を記述します。

rakeファイルにはTaskへのパラメーター受け渡しのみを記述し、実際の処理はTaskに記述しています。

Util (Keep)

app/utilsに配置します。

クラス名の命名規則として、接尾辞にUtilを付与します。

Utilは雑多なものが置かれ解りづらくなりやすいのですが、専門性の低い処理(=複数のクラスから共通で使われる処理)にのみ特化して作っていけばそれほどひどいことにはならないと思います。

ファイルの圧縮展開に関わるクラス、システムコマンドを実行するためのOpen3をラップしたクラス、ユニークなIDを払い出すクラスといったものはここに置いています。

Validator (Keep)

app/validatorsに配置します。

クラス名の命名規則として、接尾辞にValidatorを付与します。

独自バリデーションを実装する際にはここに配置します。

View (Keep)

app/viewsに配置します。

特に述べることは無いです。

ViewObject (Try)

app/view_objectsに配置します。

クラス名の命名規則として、接尾辞にViewObjectを付与します。

Viewで使用するデータオブジェクトです。

基本的にはFormやModelをそのまま使うのが良いと思っているため必須では無いと感じています。

ただし、1件目はblockAに、2件目以降はblockBに表示するといった形で表示するなど、Viewにロジックを入れたくなるようなパターンではViewObjectに1件目と2件目以降を格納してViewに渡すのが良いと思います。

まとめ

上記で述べたもの以外にもMailerやCarrierWaveを使用する際のUploaderなどありますが、そのあたりは処理が明確なので特に紹介していません。

こういった形で分かれていると責務がはっきりするのですが、Concernsに名前空間を付けないと管理が煩雑になる、定数の管理方法が定まっていないなどまだまだ問題はあるためより改善していきたいと思っています。

1年おきにやっぱりこうしたほうが良かったなと思う機会は発生するので、来年にはまた違った作りの方がよかったと言っているかもしれませんが当分はこの形で作っていこうと思います。

後出しジャンケン

Parameterが2通りの使われ方をしているので、Parameter(複雑なデータ構造を持つ引数)とEntity(CSVの行データのようなものをオブジェクト化したもの)と分けたほうがよかったかもしれない。

Exceptionについては、例外の種類を増やすよりメッセージをデバッグ可能なよう詳細に書いたほうが良いという判断です。

決済処理に失敗した、データ生成に失敗したというような重要度の高い例外については専用の例外を書くべきですが、重要度と発生頻度が低い例外はRuntimeError程度で済ませて良いと思っています。

modelsをモデル層として捉えていない、モデル==ActiveRecord::Baseを継承したものではないという意見についてはその通りなのですが、モデル層と呼ばれるものは実はかなり大きな層であると感じました。

そのため、あえてmodelsにActiveRecord::Baseを継承したもの以外を置かないようにしていると考えてもらったほうが良いかと思います。

例外的にRedisのデータを保持するクラスについては状態を保持するモノとしてmodelsに置いています。

Modelについて上記説明はちょっと曖昧な気がしました。Resource以外の永続化層のみ置くようにしたほうが良いが答です。