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

Playing with Service Objects

Playing with Service Objects

Michał Poczwardowski

July 06, 2016
Tweet

More Decks by Michał Poczwardowski

Other Decks in Programming

Transcript

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

    6th July 2016 Michal Poczwardowski michal.poczwardowski@netguru.co @dmp
  2. Service objects are just a plain old ruby objects (PORO)

    responsible for ONE thing. Just one public method to execute
  3. 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
  4. 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’)
  5. Why we like them? • handling legacy • decoupling logic

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

    invalidation and naming things. Phil Karlton
  7. 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.
  8. 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
  9. 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.”
  10. 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
  11. 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
  12. 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)
  13. 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
  14. 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
  15. 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 } }
  16. 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
  17. 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...
  18. dry-rb is a collection of next-generation Ruby libraries, each intended

    to encapsulate a common task dry-pipeline brings >> operator
  19. 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
  20. 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
  21. 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">
  22. 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"