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

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

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. Rails サービスクラス再考
    #meguro.rb
    2018/05/24
    @suusan2go

    View Slide

  2. 自己紹介
    ● Kenta Suzuki / @suusan2go
    ● M3,inc / Software Engineer
    ● 経験値で言うとこんな感じ
    ○ Ruby > JavaScript > Kotlin(ServerSide) > Golang
    ● 直近はKotlinでAPIサーバ + Nuxt.js書いてました

    View Slide

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

    View Slide

  4. なぜ最近はRailsにおける
    サービスクラスの評判が
    悪いのか?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. DAO + サービスパターンな使い方
    ● ActiveRecordをDBへのアクセス層としてしか使わない
    ○ => ActiveRecordにメソッド定義しない
    ● ビジネスロジックはサービスクラスに押し込む
    ● 新規Railsアプリに最初からサービスクラスを導入するとこのパ
    ターンになっていることがあるらしい
    ● ロジックの重複が起こりやすく、これは自分もアンチパターンだ
    と思う

    View Slide

  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

    View Slide

  10. pros / cons
    ● pros
    ○ ActiveRecordの肥大化は防げる
    ● cons
    ○ ActiveRecordの肥大化は防げるが、サービスクラスは肥大化する
    ○ ロジックの重複が起きやすい
    ■ 「タスクを完了にするときにはXXX、XXXを更新する」みたいなビジネスルール
    を一箇所で守れない
    ■ タスクは完了するかつ、XXXもするみたいなサービスが出来たときにどうなる
    だろう?一括でタスク完了するサービスが必要になったら?

    View Slide

  11. DDDのアプリケーション/ドメインサービス的な使い方
    ● DDD的に考えるとapp/models配下はEntity(= AR)、ValueObject( !=
    AR)、Repository(=AR)と考えられると思う
    ● 個人的には、サービスクラスをapp/servicesに於いてレイヤーを作る
    構成にするならアプリケーションサービスとして扱うべきだと思う(ビジ
    ネスロジックはあくまでapp/modelsで担うということ)

    View Slide

  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

    View Slide

  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

    View Slide

  14. pros / cons
    ● pros
    ○ トランザクション、セキュリティの関心事などをビジネスロジックから分離できる
    ○ 今回でいうとタスクの完了と、通知という関心事を分離している
    ● cons
    ○ エラーハンドリング、命名規則など考えることは増える (前述の例ではエラーハンドリング全然考
    えられてないw)
    ○ チーム内でサービス層の役割について認識があっていないと、似たような処理が分散
    し、ロジックの置き場に一貫性がなくなる
    (今日は主にこっちについて話します )

    View Slide

  15. なぜサービスクラスがアンチパターン化するのか
    ● レイヤーとしてサービスを置くなら、サービスクラスはあくまで
    app/models配下のpublic methodをハンドリングするだけになるはず
    ● これを守らないとアンチパターン化する傾向に有る気がする。過去に
    見た・やってしまった経験があるのは以下
    ○ 肥大化したモデルのメソッドをただサービスクラスに移す
    ○ 複数モデルを触るのでサービスクラスにする

    View Slide

  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

    View Slide

  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

    View Slide

  18. 肥大化したモデルのメソッドをただサービスクラスに移す
    ● DAO + サービスパターンのようなデメリットが出て来る
    ● 本来モデルが持つべき責務までサービスクラスに漏れてしまっている
    ● 処理としては同じことをやっているので、モデルにかけばイイのか?サー
    ビスにすべきか?が曖昧になっていく
    ● Userモデルが肥大化したので今後はUserServiceに処理書いていく!み
    たいなのも同じパターン

    View Slide

  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

    View Slide

  20. 複数モデルを触るのでサービスクラスにする
    ● オブジェクトの生成条件が守れなくなる
    ● これも本来クラスが持つべき責務が漏れ出してしまっている
    ● 複数のモデルを触るとき〜のような画一的なルールではなくて、誰が担う
    責務なのかをまず考えるべき(集約の境界を意識する)

    View Slide

  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

    View Slide

  22. Fat Modelの解消にサービスクラスは有用なのか?
    ● 前述した通り、サービスクラスはあくまで、app/models配下に定義されたビジ
    ネスロジック、それ以外の関心事(外部とのAPI通信、プッシュ通知、メール送
    信など)をハンドリングする立場になるべき
    ● 単純に肥大化したモデルの一部のメソッド / 特定のパターンでサービスクラス
    化というのは分かりやすいし取り組み安いけど、モデルとサービスの責務がカ
    オス化する
    ● というわけで、サービスクラス = FatModel解消のためのものではないし、レイ
    ヤーや責務について共通認識がないチームだと厳しい
    (サービスに限った話ではないけど…) ※逆にそういうチームなら全然あり

    View Slide

  23. DHHはサービスクラスのことをどう思ってるのか
    あくまでCallback / Concern機能 の解説の文脈ではあるけど、以下のように言っているっぽい?
    Service Object/Form Object 、トランザクションスクリプトのようなもので綺麗に制御できるとは思わない
    https://www.youtube.com/watch?v=M3JPTOTqsnE&list=PL9wALaIpe0Py6E_oHCgTrD6FvFETwJLlx&index=2

    View Slide

  24. 個人的には
    ● サービスクラス自体が悪ではないはずだけど、RailsはMVC + ActiveRecordの
    世界観が強いので、導入すると考えることが増えがち。
    ● プロジェクトの途中から導入すると、コントローラによって全然書き方違うみた
    いなことになりがち。この場合どうするん?みたいな話になりがち。
    ● メソッドの置き場に困ったときに、サービスクラスに逃げるのではなく、まずは
    ActiveRecordで表現できていない(= DBに直接は紐付かない)概念がないか
    探してみて、それを app/models に落とし込んでいくとよいのでは

    View Slide

  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

    View Slide

  26. app/modelsに非ARなクラスを作るのはRails Wayじゃない?
    https://www.youtube.com/watch?v=hkmrfjex7jI&t=3s
    DHHもやってるっぽいし多分大丈夫!

    View Slide

  27. View Slide

  28. まとめ
    ● なぜ最近になってRailsのサービスクラスがディスられがちなのか考えてみました
    ● サービスクラス自体は悪いものではないはずですが、Railsの世界で、かつFat Modelの
    解消のために使うのはあまり良い結果にならないかもしれません
    ● サービスクラスの前に、非ARなクラスで表現ができないかを考えましょう。
    ● 何かのツールやレイヤーを導入したら魔法のように設計が上手くいくなんてことは殆どな
    いので、まずは歯を食いしばってオブジェクト指向していきましょう!

    View Slide

  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

    View Slide