ServiceObjectとは

プログラミング言語と並行してフランス語の学習に精を出しているフラリモートスバルです。

21時になっても太陽が昇ったまま

そのため仕事終わりは

近くにあるプールでぷかぷか浮かび無の時間を楽しんだり、

海辺へ行って散歩をしたりしてます。

 

今の内に暇と退屈に対する感度を上書き保存しておこうと必死なのかもしれません。。

 

ServiceObject

本的な概念

<aside> 💡 A good service object is easy to test and follows the single responsibility principle.

</aside>

テストを書いたり、読み進めることが簡単であるべき。ServiceObjectには一つのビジネスロジックしか書かれていないから。

Amin Shah Gilaniは、ServiceObjectに切り出すべきか迷った時のフローチャートを示している。

Does your code handle routing, params or do other controller-y things?If so, don’t use a service object — your code belongs in the controller.

Are you trying to share your code in different controllers?In this case, don’t use a service object — use a concern.

Is your code like a model that doesn’t need persistence?If so, don’t use a service object. Use a non-ActiveRecord model instead.

Is your code a specific business action? (e.g., “Take out the trash,” “Generate a PDF using this text,” or “Calculate the customs duty using these complicated rules”)In this case, use a service object. That code probably doesn’t logically fit in either your controller or your model.

このコードってServiceObjectに切り出すべき、、?

  • routingやparams等のコントローラーっぽい処理をしている。

no, it should be in the Controller

  • 他のコントローラーと処理を共有している。

no, it should be in the Concern

  • 永続処理の不要なモデル処理をしている。

no, it’s non-ActiveRecord model (FormObject or something)

  • 詳しいビジネス処理(PDF作成、計算、ゴミ処理等)を行っている。

yes, ServiceObject should help it!!!


コントローラーって、責務を知らないと肥大になりがち。

with rendering and redirecting—normal controller concerns.

コントローラーは、renderかredirectという、コントローラの関心ごとにfocusさせたい。

class UserController < ApplicationController
  def create
    user = User.new(user_params)
    if user.save
      send_welcome_email
      notify_slack
      if @user.admin?
        log_new_admin
      else
        log_new_user
      end
      redirect_to new_user_welcome_path
    else
      render 'new'
    end
  end  # private methods
end

かといってモデルにロジックを移していくと、今度はモデルが肥大化しがち。

コントローラーとモデルの間に受け皿という形で層を設けることができたら、それぞれの処理が何をやっているのかが明確になって後で使用の把握に役立つ。

その「層」の役割を担うのがいくつかある。

その一つにService層がある。

Service objects in Rails

What are service objects?

Service objects are plain old Ruby objects (PORO’s) that do one thing.

単一責務のオブジェクト(PORO)

Serviceクラスにより詳細な機能を切り出すことで、

最終的にここまでコントローラーをスリムにすることができる。

class UserController < ApplicationController
  def create
    user = RegisterUser.new(User.new(user_params)).execute
    if user
      redirect_to new_user_welcome_path
    else
      render 'new'
    end
  end  # private methods
end

ServiceObject.new(arguments).executeの形が不恰好だと思ったら、下記のように独自にcallメソッドを定義しちゃってもいい。

class RegisterUser
  def self.call(*args, &block)
    new(*args, &block).execute
  end  def initialize(user)
@user = user
  end  def execute
    # old code
  end  # private methods
end

コントローラーはこうなる

class UserController < ApplicationController
  def create
    result = RegisterUser.call(User.new(user_params))
    if result.success?
      redirect_to new_user_welcome_path
    else
      render 'new', error: result.errors
    end
  end  # private methods
end

Serivice Classを理解する上で大事なことだから再掲

基本的な概念

<aside> 💡 A good service object is easy to test and follows the single responsibility principle.

</aside>

テストを書いたり、読み進めることが簡単であるべき。ServiceObjectには一つのビジネスロジックしか書かれていないから。

Amin Shah Gilaniは、ServiceObjectに切り出すべきか迷った時のフローチャートを示している。

Does your code handle routing, params or do other controller-y things?If so, don’t use a service object — your code belongs in the controller.

Are you trying to share your code in different controllers?In this case, don’t use a service object — use a concern.

Is your code like a model that doesn’t need persistence?If so, don’t use a service object. Use a non-ActiveRecord model instead.

Is your code a specific business action? (e.g., “Take out the trash,” “Generate a PDF using this text,” or “Calculate the customs duty using these complicated rules”)In this case, use a service object. That code probably doesn’t logically fit in either your controller or your model.

このコードってServiceObjectに切り出すべき、、?

  • routingやparams等のコントローラーっぽい処理をしている。

no, it should be in the Controller

  • 他のコントローラーと処理を共有している。

no, it should be in the Concern

  • 永続処理の不要なモデル処理をしている。

no, it’s non-ActiveRecord model (FormObject or something)

  • 詳しいビジネス処理(PDF作成、計算、ゴミ処理等)を行っている。

yes, ServiceObject should help it!!!

(*args, &block)

テンプレートのレンダリングで見かけた記法。

rails/rendering.rb at 04972d9b9ef60796dc8f0917817b5392d61fcf09 · rails/rails

スプラット(液体が地面に落ちた時などの音)がついた変数には配列が入る。

下記のように展開される。

*a = 1
a #=> [1]

a, *b = 1, 2, 3, 4
a #=> 1
b #=> [2, 3, 4]

a, *b, c = 1, 2, 3, 4
a #=> 1
b #=> [2, 3]
c #=> 4

self.call(*args, &block) vs self.call(args)

They allow you to specify a callback to pass to a method. This callback can be invoked two ways - either by capturing it by specifying a final argument prefixed with &, or by using the yield keyword:

&blockは明示しなくてもいい

その代わりに、メソッドの内部でyieldとしてあげるだけでも同じ挙動をする。

def meth_captures(arg, &block)
  block.call( arg, 0 ) + block.call( arg.reverse , 1 )
end

irb> meth_captures('pony') do |word, num|
       puts "in callback! word = #{word.inspect}, num = #{num.inspect}"
       word + num.to_s
     end
in callback! word = "pony" num = 0
in callback! word = "ynop" num = 1
#=> "pony0ynop1"
irb> def meth_yields(arg)
       yield(arg, 0) + yield(arg.upcase, 1)
     end
#=> nil
irb> meth_yields('frog') do |word, num|
       puts "in callback! word = #{word.inspect}, num = #{num.inspect}"
       word + num.to_s
     end
in callback! word = "frog", num = 0
in callback! word = "FROG", num = 1
#=> "frog0FROG1"

また、&に渡すblockは、塊として定義しておくことができる

そうするには、Procオブジェクト、→、lambdaを使用することで代用することができる。

irb> callback = lambda do |word, num|
       puts "in callback! word = #{word.inspect}, num = #{num.inspect}"
       word + num.to_s
     end
#=> #<Proc:0x0052e3d8@(irb):22>
irb> meth_captures('unicorn', &callback)
in callback! word = "unicorn", num = 0
in callback! word = "nrocinu", num = 1
#=> "unicorn0nrocinu1"
irb> meth_yields('plate', &callback)
in callback! word = "plate", num = 0
in callback! word = "PLATE", num = 1
#=> "plate0PLATE1"

What's this &block in Ruby? And how does it get passed in a method here?

エンティティ

システムでオブジェクトの同一性が重要な意味を持つもの

「中野」の体重が変わっても、変わる前と変わった後で「中野」であることに変わりは無い。

体重が変わったら「中野」じゃなくなるわけではない。

前後の同一性を判断するための識別子(id)というものを持っている。

値オブジェクト

「何であるか」が重要なもの

そして、値が同じであれば同一とみなしていいもの

神奈川県に対する思いや印象は人それぞれでいいが、神奈川県は神奈川県である。

住所は住所

そのオブジェクトが持っている値が同じかどうかで同一性が比較される。

値オブジェクトは、それだけのクラスに切り出すことができる。

例えば、住所という概念は、UserやCompany,Organizationクラスで使われるかもしれない。

そんな時は、Address(住所)という独立クラスを作成して複数のクラスから参照できるようにするとベター。

クリーンアーキテクチャ

最も柔軟なシステム

それは、ソースコードの依存関係が、具象だけではなく、抽象だけを参照しているものを指す。

安定度の高いシステムを作る

そのためには、すべての具象コンポーネントがI(Instability)の低いコンポーネント(抽象コンポーネント)を指し示すようなシステムを作る必要がある。

安定度の指標として、下記のような単語がある

ファンイン クラス内部のメソッドが外部から依存を受けている数

ファンアウト クラス内部のメソッドが外部のクラスに対して依存している数

I(Instability: 安定性

= ファンアウト➗(ファンイン+ファンアウト)

<aside> 💡 つまり、外部のクラスに一つも依存していないクラスが、一番安定しているクラスと言える。

</aside>