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

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

merguro.rb #15で話しました

35b58828e4e24c579c35529061711dfd?s=128

Kenta Suzuki

May 24, 2018
Tweet

Transcript

  1. Rails サービスクラス再考 #meguro.rb 2018/05/24 @suusan2go

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

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

    肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻 訳) • ActiveRecordのクラスから責務を分割するための考え方として登場した • 自分が初めて業務でみたのは2015年 • Rails標準ではなく、各社各々の考え方で実装されている • 最近はディスられ気味
  4. なぜ最近はRailsにおける サービスクラスの評判が 悪いのか?

  5. サービスクラス自体は 一般的な実装パターンのはずで サービスクラス = 悪ではないよね? (Spring + Kotlin + DDDプロジェクトをやった感想)

  6. というわけで Railsにおけるサービスクラスにつ いて改めて考えてみました

  7. そもそもサービスクラスとは何なのか? 個人的な認識では以下のパターンで語られる事が多い • DAO + サービスパターン • DDDにおけるアプリケーションサービス / ドメインサー

    ビスとして使うパターン
  8. DAO + サービスパターンな使い方 • ActiveRecordをDBへのアクセス層としてしか使わない ◦ => ActiveRecordにメソッド定義しない • ビジネスロジックはサービスクラスに押し込む

    • 新規Railsアプリに最初からサービスクラスを導入するとこのパ ターンになっていることがあるらしい • ロジックの重複が起こりやすく、これは自分もアンチパターンだ と思う
  9. 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
  10. pros / cons • pros ◦ ActiveRecordの肥大化は防げる • cons ◦

    ActiveRecordの肥大化は防げるが、サービスクラスは肥大化する ◦ ロジックの重複が起きやすい ▪ 「タスクを完了にするときにはXXX、XXXを更新する」みたいなビジネスルール を一箇所で守れない ▪ タスクは完了するかつ、XXXもするみたいなサービスが出来たときにどうなる だろう?一括でタスク完了するサービスが必要になったら?
  11. DDDのアプリケーション/ドメインサービス的な使い方 • DDD的に考えるとapp/models配下はEntity(= AR)、ValueObject( != AR)、Repository(=AR)と考えられると思う • 個人的には、サービスクラスをapp/servicesに於いてレイヤーを作る 構成にするならアプリケーションサービスとして扱うべきだと思う(ビジ ネスロジックはあくまでapp/modelsで担うということ)

  12. class CompleteTaskService def call(task:, user:) task.complete # あくまでTaskに定義されているメソッドを呼び出すだけ UserMailer.task_completed( user: user,

    task: task).deliver_now end end class Task < ApplicationRecord def complete # 省略... end end
  13. # タスクを一括更新する場合にメールの通知の仕方を変えたいといったのにも対応できる class BulkCompleteTaskService def call(tasks:, user:) tasks.each do |task|

    task.complete end UserMailer.tasks_completed( user: user, tasks: tasks).deliver_now end end
  14. pros / cons • pros ◦ トランザクション、セキュリティの関心事などをビジネスロジックから分離できる ◦ 今回でいうとタスクの完了と、通知という関心事を分離している •

    cons ◦ エラーハンドリング、命名規則など考えることは増える (前述の例ではエラーハンドリング全然考 えられてないw) ◦ チーム内でサービス層の役割について認識があっていないと、似たような処理が分散 し、ロジックの置き場に一貫性がなくなる (今日は主にこっちについて話します )
  15. なぜサービスクラスがアンチパターン化するのか • レイヤーとしてサービスを置くなら、サービスクラスはあくまで app/models配下のpublic methodをハンドリングするだけになるはず • これを守らないとアンチパターン化する傾向に有る気がする。過去に 見た・やってしまった経験があるのは以下 ◦ 肥大化したモデルのメソッドをただサービスクラスに移す

    ◦ 複数モデルを触るのでサービスクラスにする
  16. 肥大化したモデルのメソッドをただサービスクラスに移す コード例 # これをサービス化する 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
  17. 肥大化したモデルのメソッドをただサービスクラスに移す コード例 # 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
  18. 肥大化したモデルのメソッドをただサービスクラスに移す • DAO + サービスパターンのようなデメリットが出て来る • 本来モデルが持つべき責務までサービスクラスに漏れてしまっている • 処理としては同じことをやっているので、モデルにかけばイイのか?サー ビスにすべきか?が曖昧になっていく

    • Userモデルが肥大化したので今後はUserServiceに処理書いていく!み たいなのも同じパターン
  19. 複数モデルを触るのでサービスクラスにする コード例 # タスクへのコメント追加サービス # このコメントの生成ルールを必ず守らなければいけないとしたら、サービスにこのロジックを置くのは適切なのか? 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
  20. 複数モデルを触るのでサービスクラスにする • オブジェクトの生成条件が守れなくなる • これも本来クラスが持つべき責務が漏れ出してしまっている • 複数のモデルを触るとき〜のような画一的なルールではなくて、誰が担う 責務なのかをまず考えるべき(集約の境界を意識する)

  21. 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
  22. Fat Modelの解消にサービスクラスは有用なのか? • 前述した通り、サービスクラスはあくまで、app/models配下に定義されたビジ ネスロジック、それ以外の関心事(外部とのAPI通信、プッシュ通知、メール送 信など)をハンドリングする立場になるべき • 単純に肥大化したモデルの一部のメソッド / 特定のパターンでサービスクラス

    化というのは分かりやすいし取り組み安いけど、モデルとサービスの責務がカ オス化する • というわけで、サービスクラス = FatModel解消のためのものではないし、レイ ヤーや責務について共通認識がないチームだと厳しい (サービスに限った話ではないけど…) ※逆にそういうチームなら全然あり
  23. DHHはサービスクラスのことをどう思ってるのか あくまでCallback / Concern機能 の解説の文脈ではあるけど、以下のように言っているっぽい? Service Object/Form Object 、トランザクションスクリプトのようなもので綺麗に制御できるとは思わない https://www.youtube.com/watch?v=M3JPTOTqsnE&list=PL9wALaIpe0Py6E_oHCgTrD6FvFETwJLlx&index=2

  24. 個人的には • サービスクラス自体が悪ではないはずだけど、RailsはMVC + ActiveRecordの 世界観が強いので、導入すると考えることが増えがち。 • プロジェクトの途中から導入すると、コントローラによって全然書き方違うみた いなことになりがち。この場合どうするん?みたいな話になりがち。 •

    メソッドの置き場に困ったときに、サービスクラスに逃げるのではなく、まずは ActiveRecordで表現できていない(= DBに直接は紐付かない)概念がないか 探してみて、それを app/models に落とし込んでいくとよいのでは
  25. 非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
  26. app/modelsに非ARなクラスを作るのはRails Wayじゃない? https://www.youtube.com/watch?v=hkmrfjex7jI&t=3s DHHもやってるっぽいし多分大丈夫!

  27. None
  28. まとめ • なぜ最近になってRailsのサービスクラスがディスられがちなのか考えてみました • サービスクラス自体は悪いものではないはずですが、Railsの世界で、かつFat Modelの 解消のために使うのはあまり良い結果にならないかもしれません • サービスクラスの前に、非ARなクラスで表現ができないかを考えましょう。 •

    何かのツールやレイヤーを導入したら魔法のように設計が上手くいくなんてことは殆どな いので、まずは歯を食いしばってオブジェクト指向していきましょう!
  29. 参考文献 https://speakerdeck.com/joker1007/realworld-domain-model-on-rails https://qiita.com/joker1007/items/25de535cd8bb2857a685 https://qiita.com/klriutsa/items/8d7381f437c225c64a5f http://blog.j5ik2o.me/entry/2016/03/07/034646 Special Thanks レビューありがとう https://github.com/negito6