Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails サービスクラス再考 / have a rethink on Rails servi...

Rails サービスクラス再考 / have a rethink on Rails service class

merguro.rb #15で話しました

Kenta Suzuki

May 24, 2018
Tweet

More Decks by Kenta Suzuki

Other Decks in Programming

Transcript

  1. 自己紹介 • Kenta Suzuki / @suusan2go • M3,inc / Software

    Engineer • 経験値で言うとこんな感じ ◦ Ruby > JavaScript > Kotlin(ServerSide) > Golang • 直近はKotlinでAPIサーバ + Nuxt.js書いてました
  2. (Railsの)サービスクラスとは • 7 Patterns to Refactor Fat ActiveRecord Models ◦

    肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻 訳) • ActiveRecordのクラスから責務を分割するための考え方として登場した • 自分が初めて業務でみたのは2015年 • Rails標準ではなく、各社各々の考え方で実装されている • 最近はディスられ気味
  3. DAO + サービスパターンな使い方 • ActiveRecordをDBへのアクセス層としてしか使わない ◦ => ActiveRecordにメソッド定義しない • ビジネスロジックはサービスクラスに押し込む

    • 新規Railsアプリに最初からサービスクラスを導入するとこのパ ターンになっていることがあるらしい • ロジックの重複が起こりやすく、これは自分もアンチパターンだ と思う
  4. class CompleteTaskService def call(task:, user:) if task.completed_at.nil? task.update( hoge: "piyo",

    completed_at: Time.current) else task.update( piyo: "fuga") end UserMailer.task_completed(user: user, task: task).deliver_now end end
  5. pros / cons • pros ◦ ActiveRecordの肥大化は防げる • cons ◦

    ActiveRecordの肥大化は防げるが、サービスクラスは肥大化する ◦ ロジックの重複が起きやすい ▪ 「タスクを完了にするときにはXXX、XXXを更新する」みたいなビジネスルール を一箇所で守れない ▪ タスクは完了するかつ、XXXもするみたいなサービスが出来たときにどうなる だろう?一括でタスク完了するサービスが必要になったら?
  6. pros / cons • pros ◦ トランザクション、セキュリティの関心事などをビジネスロジックから分離できる ◦ 今回でいうとタスクの完了と、通知という関心事を分離している •

    cons ◦ エラーハンドリング、命名規則など考えることは増える (前述の例ではエラーハンドリング全然考 えられてないw) ◦ チーム内でサービス層の役割について認識があっていないと、似たような処理が分散 し、ロジックの置き場に一貫性がなくなる (今日は主にこっちについて話します )
  7. 肥大化したモデルのメソッドをただサービスクラスに移す コード例 # これをサービス化する class Task < ApplicationRecord def complete(user:)

    update(hoge: "piyo", completed_at: Time.current)    task.comments.each do |comment| comment.update( hoge: fuga) end task.owner.update( "piyo") if task.piyopiyo?    UserMailer.send_hoge_mail(task).deliver_later if task.ponyo? # みたいな処理が100行 end end
  8. 肥大化したモデルのメソッドをただサービスクラスに移す コード例 # DAO + サービスパターンと同じく、本来 Taskが持っているべき知識が漏れてしまっている # Task クラスの中に他にも

    `completed_at` を更新するメソッドがいたりすると責務が曖昧になる class CompleteTaskService def call(task:, user:) update(hoge: "piyo", completed_at: Time.current)    task.comments.each do |comment| comment.update( hoge: fuga) end task.owner.update( piyo: "piyo") if task.piyopiyo?    UserMailer.send_hoge_mail(task).deliver_later if task.ponyo? # みたいな処理が100行 end end
  9. 複数モデルを触るのでサービスクラスにする コード例 # タスクへのコメント追加サービス # このコメントの生成ルールを必ず守らなければいけないとしたら、サービスにこのロジックを置くのは適切なのか? class CreateTaskCommentService def call(task:,

    content:, author:) # タスクが完了してたらコメントは追加できない if task.completed? raise "This task is already completed" end # タスクの作者はコメントできない。変な仕様だけど例なので許してください :bow: if task.owner?(author)     raise "Task owner can’t add comment" end Comment.create!(body: content, task_id: task.id, author_id: author.id) end end
  10. TaskクラスがCommentの生成の責務を保つ場合の例 class Task < ApplicationRecord def add_comment(body:, author:) # タスクが完了してたらコメントは追加できない

    if completed? raise "This task is already completed" end # タスクの作者はコメントできない。変な仕様だけど例なので許してください :bow: if owner?(author) raise "Task owner can't add comment" end task.comments.create!( body: content, task_id: id, author_id: author.id) end end
  11. Fat Modelの解消にサービスクラスは有用なのか? • 前述した通り、サービスクラスはあくまで、app/models配下に定義されたビジ ネスロジック、それ以外の関心事(外部とのAPI通信、プッシュ通知、メール送 信など)をハンドリングする立場になるべき • 単純に肥大化したモデルの一部のメソッド / 特定のパターンでサービスクラス

    化というのは分かりやすいし取り組み安いけど、モデルとサービスの責務がカ オス化する • というわけで、サービスクラス = FatModel解消のためのものではないし、レイ ヤーや責務について共通認識がないチームだと厳しい (サービスに限った話ではないけど…) ※逆にそういうチームなら全然あり
  12. 個人的には • サービスクラス自体が悪ではないはずだけど、RailsはMVC + ActiveRecordの 世界観が強いので、導入すると考えることが増えがち。 • プロジェクトの途中から導入すると、コントローラによって全然書き方違うみた いなことになりがち。この場合どうするん?みたいな話になりがち。 •

    メソッドの置き場に困ったときに、サービスクラスに逃げるのではなく、まずは ActiveRecordで表現できていない(= DBに直接は紐付かない)概念がないか 探してみて、それを app/models に落とし込んでいくとよいのでは
  13. 非ARなクラスを作る例 # 実装は適当ですが、タスクのリストを扱う場合はこんな概念を検討してみてもいいかもしれない class UserTaskList def initialize(tasks:, user:) @tasks =

    tasks @user = user end def complete_all @tasks.each do |task| task.complete end UserMailer.tasks_completed(user: @user, tasks: @tasks).deliver_now end  # 省略 def expired_tasks end end