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

Service Objects: Finding the Right Tool For the...

Service Objects: Finding the Right Tool For the Job

From Iowa Ruby Brigade meeting on August 15th, 2017

Avatar for Zach Taylor

Zach Taylor

August 15, 2017

More Decks by Zach Taylor

Other Decks in Programming

Transcript

  1. REVIEW WHAT IS A SERVICE OBJECT? ▸ An object that

    does something in your system ▸ Encapsulates some operation (or multiple operations)
  2. REVIEW WHY WOULD YOU WANT TO USE A SERVICE OBJECT?

    ▸ Separate business logic from framework code ▸ Re-use ▸ Testing in isolation
  3. REVIEW WHEN TO USE A SERVICE OBJECT? ▸ “The action

    is complex (e.g. closing the books at the end of an accounting period) ▸ The action reaches across multiple models (e.g. an e-commerce purchase using Order, CreditCard and Customer objects) ▸ The action interacts with an external service (e.g. posting to social networks) ▸ The action is not a core concern of the underlying model (e.g. sweeping up outdated data after a certain time period). ▸ There are multiple ways of performing the action (e.g. authenticating with an access token or password). This is the Gang of Four Strategy pattern.” http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
  4. TRAILBLAZER FRAMEWORK: TRAILBLAZER ▸ http://trailblazer.to ▸ “… gives you a

    high-level architecture for web applications.” ▸ Contracts (forms), policies, operations, view models, and more
  5. TRAILBLAZER CODE class Song::Create < Trailblazer::Operation extend Contract::DSL contract do

    property :title validates :title, presence: true end step Model( Song, :new ) step :assign_current_user! step Contract::Build() step Contract::Validate( ) failure :log_error! step Contract::Persist( ) def log_error!(options) # .. end def assign_current_user!(options) options["model"].created_by = options["current_user"] end end http://trailblazer.to/gems/operation/2.0/index.html
  6. TRAILBLAZER PROS ▸ DSL ▸ A lot of functionality built-in

    ▸ Explicit ▸ Convenient bindings for Rails and Hanami ▸ Long-term support ▸ Premium support ▸ Steep learning curve ▸ A lot of dependencies ▸ Explicit ▸ Documentation could be improved CONS
  7. ACTIVE INTERACTION LIBRARY: ACTIVE INTERACTION ▸ https://github.com/orgsync/active_interaction ▸ “... gives

    you a place to put your business logic.” ▸ Provides service objects with input validation, callbacks, and error handling
  8. ACTIVE INTERACTION CODE class BuyItem < ActiveInteraction::Base object :credit_card, :item

    hash :options do boolean :gift_wrapped end def execute order = credit_card.purchase(item) notify(credit_card.account) order end private def notify(account) # ... end end https://github.com/orgsync/active_interaction
  9. ACTIVE INTERACTION CODE outcome = BuyItem.run(item: 'Thing', options: { gift_wrapped:

    'yes' }) outcome.errors.messages # => {:credit_card=>["is required"], :item=>["is not a valid object”], :options=>["has an invalid nested value (\"gift_wrapped\" => \"yes\")"]} https://github.com/orgsync/active_interaction
  10. ACTIVE INTERACTION PROS ▸ DSL ▸ A lot of functionality

    built-in ▸ Flexible parameter filtering ▸ Familiar validations (ActiveModel) ▸ Documentation has good examples ▸ Medium learning curve CONS
  11. INTERACTOR LIBRARY: INTERACTOR ▸ https://github.com/collectiveidea/interactor ▸ “… provides a common

    interface for performing complex user interactions.” ▸ Just interactors, hooks (callbacks), and context
  12. INTERACTOR CODE class AuthenticateUser include Interactor def call if user

    = User.authenticate(context.email, context.password) context.user = user context.token = user.secret_token else context.fail!(message: "authenticate_user.failure") end end end https://github.com/collectiveidea/interactor
  13. INTERACTOR CODE class SessionsController < ApplicationController def create result =

    AuthenticateUser.call(session_params) if result.success? session[:user_token] = result.token redirect_to result.user else flash.now[:message] = t(result.message) render :new end end private def session_params params.require(:session).permit(:email, :password) end end https://github.com/collectiveidea/interactor
  14. INTERACTOR PROS ▸ Simple, almost no learning curve ▸ Convenient

    bindings for Rails ▸ No dependencies ▸ Maintained? ▸ Almost too simple CONS
  15. DIY LIBRARY: BUILD YOUR OWN ▸ Define your own patterns

    and DSLs ▸ Add only the components you use ▸ Reduce risk that external dependencies create
  16. DIY CODE class AcceptInvite def self.call(*args) new(*args).call end include Virtus.model

    attribute :invite, Invite attribute :user, User attribute :account, Account attribute :time, Time, default: proc { Time.now } def call unless invite_already_accepted? accept_invite send_notification_to_inviter end end http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html
  17. DIY PROS ▸ Tweak to your style and liking ▸

    Control over behavior ▸ Depend on your own code ▸ Better naming for abstractions ▸ Have to maintain any abstractions you create CONS