Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

肥大化したモデルのメソッドをただサービスクラスに移す コード例 # これをサービス化する 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

Slide 17

Slide 17 text

肥大化したモデルのメソッドをただサービスクラスに移す コード例 # 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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

複数モデルを触るのでサービスクラスにする コード例 # タスクへのコメント追加サービス # このコメントの生成ルールを必ず守らなければいけないとしたら、サービスにこのロジックを置くのは適切なのか? 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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

非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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

参考文献 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