Playing with Service Objects

Playing with Service Objects

4cf300db4be834dfb4ddd76337205946?s=128

Michał Poczwardowski

July 06, 2016
Tweet

Transcript

  1. Playing with Service Objects Netguru Rails Meeting #10 Gdansk -

    6th July 2016 Michal Poczwardowski michal.poczwardowski@netguru.co @dmp
  2. Service Objects

  3. Service objects are just a plain old ruby objects (PORO)

    responsible for ONE thing. Just one public method to execute
  4. 01: module RebelServices 02: class EmpireDestroyer 03: def initialize(jedi:) 04:

    @jedi = jedi 05: end 06: 07: def call 08: jedi.use_the_force 09: end 10: 11: private 12: 13: attr_reader :jedi 14: end 15: end 16: 17: ### 18: 19: RebelServices::EmpireDestroyer.new(jedi: luke).call
  5. 01: # more reusable version - dry-rb 02: 03: class

    JediCreator 04: def initialize(repository) 05: @repository = repository 06: end 07: 08: def call(input) 09: repository.create(input) 10: end 11: 12: private 13: 14: attr_reader :repository 15: end 16: 17: ### 18: 19: jedi_creator = JediCreator.new(jedi_repository) 20: jedi_creator.(name: ‘Anakin’) 21: jedi_creator.(name: ’Vader’)
  6. Why we like them? • handling legacy • decoupling logic

    • one thing, just one method to test
  7. There are only two hard things in Computer Science: cache

    invalidation and naming things. Phil Karlton
  8. Naming • The most important thing is how to name

    a service object properly. • Make name short but include context, environment and its behaviour • Keep it unambiguous • Ask yourself what is's doing and ask your coworker what does he/she think after just hearing its name.
  9. Naming things developers have a tendency to stick to a

    class name given when the class was born… because in life we don’t rename things
  10. Back to service objects... Gems for everything

  11. Gem # 1 Interactor

  12. interactor • “An interactor is a simple, single-purpose object” •

    “Interactors are used to encapsulate your application's business logic. Each interactor represents one thing that your application does.”
  13. built-in stuff • shared context • failing context • before/after/around

    hooks • organizers • rollback
  14. 01: # example interactor: 02: 03: class AuthenticateUser 04: include

    Interactor 05: 06: def call 07: if user = User.authenticate(context.email, context.password) 08: context.user = user 09: context.token = user.secret_token 10: else 11: context.fail!(message: "authenticate_user.failure") 12: end 13: end 014: end
  15. 01: # example interactor usage: 02: 03: class SessionsController <

    ApplicationController 04: def create 05: result = AuthenticateUser.call(session_params) 06: 07: if result.success? 08: session[:user_token] = result.token 09: redirect_to root_path 10: else 11: flash.now[:message] = t(result.message) 12: render :new 13: end 14: end 15: 16: private 17: 18: def session_params 19: params.require(:session).permit(:email, :password) 20: end 21: end
  16. 01: # example organizer 02: 03: 04: class PlaceOrder 05:

    include Interactor::Organizer 06: 07: organize CreateOrder, ChargeCard, SendThankYou 08: end 09: 010: 011: ### 012: 013: 014: result = PlaceOrder.call(order_params: order_params)
  17. Gem # 2 Waterfall

  18. waterfall goals • “Be able to chain ruby commands, and

    treat them like a flow.” • “When logic is complicated, waterfalls show their true power and let you write intention revealing code.” http://slides.com/apneadiving/code-ruby-like-you-build-legos
  19. None
  20. None
  21. None
  22. None
  23. Available waterfall predicates • .chain(name_or_mapping = nil, &block) | block

    signature: (outflow, waterfall) • .when_falsy(&block) | block signature: (error_pool, waterfall) • .when_truthy(&block) | block signature: (error_pool, waterfall) • .dam - for .when_falsy/truthy • .on_dam(&block) | block signature: (error_pool, outflow, waterfall) • + Each waterfall has its own outflow and error_pool
  24. 01: Wf.new 02: .when_falsy { @user.update(user_params) } 03: .dam {

    @user.errors } 04: .chain { render json: @user } 05: .on_dam { |errors| render json: { errors: errors.full_messages } }
  25. Gem # 3 Services

  26. 01: module Services 02: module Users 03: class Delete <

    Services::Base 04: def call(ids_or_objects) 05: users = find_objects(ids_or_objects) 06: users.each do |user| 07: if user.posts.any? 08: raise Error, "User #{user.id} refusing to delete." 09: end 10: user.destroy 11: Mailer.user_deleted(user).deliver 12: end 13: users 14: end 15: end 16: end 17: end 18: 19: ### 20: Services::Users::Delete.call 1 21: Services::Users::Delete.perform_async 1
  27. but… (copied from README.md) Logging You can choose between logging

    to Redis or to a file, or turn logging off. By default logging is turned off. Redis to be described... File to be described... Exception wrapping to be described... Uniqueness checking to be described...
  28. # 4 dry-rb/dry-pipeline

  29. dry-rb is a collection of next-generation Ruby libraries, each intended

    to encapsulate a common task dry-pipeline brings >> operator
  30. dry-pipeline - #1 - transform_user_attributes 01: USERS = [] 02:

    User = Struct.new(:id, :first_name, :last_name, :email) 03: 04: transform_user_attributes = Dry::Pipeline.new do |user_attributes| 05: allowed_keys = [:id, :first_name, :last_name, :email] 06: 07: user_attributes.each_with_object({}) do |(key, value), hash| 08: next unless allowed_keys.include?(key.to_sym) 09: hash[key.to_sym] = value 10: end 11: end
  31. dry-pipeline - #2 - validate_user_attributes 01: validate_user_attributes = Dry::Pipeline.new do

    |user_attributes| 02: required_keys = [:first_name, :last_name, :email] 03: 04: if (required_keys - user_attributes.keys).empty? 05: user_attributes 06: else 07: raise ':first_name, :last_name and :email must be present' 08: end 09: end
  32. dry-pipeline - #3 - create_user 01: create_user = Dry::Pipeline.new do

    |user_attributes| 02: User.new( 03: USERS.length.next, *user_attributes.values_at(:first_name, :last_name, :email) 04: ).tap { |user| USERS << user } 05: end 06: 07: (transform_user_attributes >> validate_user_attributes >> create_user)[ 08: first_name: 'Jane', 09: last_name: 'Doe', 10: email: 'jane.doe@gmail.com' 11: ] 12: #<struct User id=1,first_name="Jane",last_name="Doe",email="jane.doe@gmail.com">
  33. dry-pipeline - extra short example 01: upcase = Dry::Pipeline.new {

    |s| s.upcase } 02: reverse = Dry::Pipeline.new { |s| s.reverse } 03: 04: (upcase >> reverse)[“Michał Poczwardowski”] 05: (upcase >> reverse).(“Michał Poczwardowski”) 06: (upcase >> reverse).call(“Michał Poczwardowski”) # => "IKSWODRAWZCOP łAHCIM"
  34. Resources https://www.netguru.co/blog/service-objects-in-rails-will-help GEMs: - interactor: https://github.com/collectiveidea/interactor - waterfall: https://github.com/apneadiving/waterfall -

    services: https://github.com/krautcomputing/services dry-web: http://dry-rb.org/resources/reddotrubyconf-2016/
  35. Thanks!