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