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

Richer Service Objects with Hanami Interactors

Richer Service Objects with Hanami Interactors

Gabriel Gizotti

May 24, 2017
Tweet

More Decks by Gabriel Gizotti

Other Decks in Programming

Transcript

  1. Services Objects used to orchestrate the business logic between your

    controllers (or any other business logic) and the persistence layer » Service Objects (Rails) » Use Case Objects/Interactor (Hanami) » Operations (Trailblazer)
  2. Hanami Interactors Hanami implementation of Service Objects » Part of

    hanami-utils gem » Small API » Configurable Result » Internal State Exposure
  3. Successful Example require 'hanami/interactor' class Signup include Hanami::Interactor expose :user,

    :params def initialize(params) @params = params @user = User.new(@params) @foo = 'bar' end def call @user = UserRepository.persist(@user) end end
  4. Successful Example result = Signup.new(name: 'Krombopulos Michael').call result.failure? # =>

    false result.successful? # => true result.user # => #<User:0x007fa311105778 @id=1 @name="Krombopulos Michael"> result.params # => { :name=>"Krombopulos Michael" } result.foo # => raises NoMethodError
  5. Failing Example require 'hanami/interactor' class Signup include Hanami::Interactor expose :user

    def initialize(params) @params = params @user = User.new(@params) end # THIS WON'T BE INVOKED BECAUSE #valid? WILL RETURN false def call @user = UserRepository.persist(@user) end private # super always return true # It's desined to be overriden in each child class def valid? false end end
  6. Failing Example result = Signup.new(name: "Krombopulous Michael").call result.successful? # =>

    false result.failure? # => true result.user # => #<User:0x007fa311105778 @id=nil @name="Krombopulos Michael">
  7. Caveats As per current hanami-utils version (1.0.0), parameters cannot be

    passed to the #call method, only to #new.1 » cause of very limited Dependecy Injection » Interactor instance cannot be reused with other parameters 1 This will be changed in hanami-utils 1.1.0. See this PR for more details.
  8. Refactoring an Action with Interactor class TasksController < ApplicationController def

    assign_checklist checklist_details = params[:task][:checklist] checklist = current_user.organisation.checklists.find(checklist_details[:id]) tasks = Task.transaction do checklist.items.map do |description| task = scope.new(resource_params.merge(description: description)) authorize! :create, task unless task.save render_errors(task) raise ActiveRecord::Rollback.new end task end end if tasks.present? render json: TaskMapper.all_as_json(tasks, scope: current_user) end end end
  9. What we need to know about the application? » The

    application has a Cheklist and a Task model » A collection of Task can be created from a Checklist
  10. What does the action do? » Creates one Task per

    Checklist item » Authorize creation of each task » Task resources are all the same (passed on resource_params), except for description that is set to the Checklist item » Handles database transaction rollback if any Task fails to be saved » Handles rendering of errors or tasks depending if any error happened or not
  11. What should this action be doing? » Check user authorization

    for whole action » Pass params, and any other argument necessary, to a service that handle the actual creation work » Handle rendering of errors or tasks based on service result
  12. Implementing the Interactor Class require 'hanami/interactor' class ChecklistTasksFactory include Hanami::Interactor

    def initialize(checklist_params, task_params, current_user) @checklist = current_user.organisation.checklists.find(checklist_params[:id]) @task_params = task_params @scope = current_user.organisation.tasks @tasks = [] end # ... end
  13. Refactoring the Controller Action class TasksController < ApplicationController def assign_checklist

    authorize! :create_from_checklist, Task # resource_params comes from our private API interactor = ChecklistTasksFactory.new(checklist_params, resource_params, current_user) # ... end private def checklist_params params.require(:task).permit(checklist: [:id]) end end
  14. Implementing the Interactor Class require 'hanami/interactor' class ChecklistTasksFactory include Hanami::Interactor

    def initialize(checklist_params, task_params, current_user) @checklist = current_user.organisation.checklists.find(checklist_params[:id]) @task_params = task_params @scope = current_user.organisation.tasks @tasks = [] end def call @tasks = Task.transaction do @checklist.items.map do |description| build_task(description) save_task! @task end end end private def build_task(description) @task = @scope.new(@task_params.merge(description: description)) end def save_task! return if @task.save add_error(@task) raise ActiveRecord::Rollback end end
  15. Refactoring the Controller Action class TasksController < ApplicationController def assign_checklist

    authorize! :create_from_checklist, Task # resource_params comes from our private API interactor = ChecklistTasksFactory.new(checklist_params, resource_params, current_user) result = interactor.call if result.successful? # result.tasks will raise NoMethodError as tasks is not exposed by the interactor render json: TaskMapper.all_as_json(result.tasks, scope: current_user) else # Interactor::Result exposes #errors by default render_errors(result.errors) end end private def checklist_params params.require(:task).permit(checklist: [:id]) end end
  16. The complete Interactor require 'hanami/interactor' class ChecklistTasksFactory include Hanami::Interactor expose

    :tasks def initialize(checklist_params, task_params, current_user) @checklist = current_user.organisation.checklists.find(checklist_params[:id]) @task_params = task_params @scope = current_user.organisation.tasks @tasks = [] end def call @tasks = Task.transaction do @checklist.items.map do |description| build_task(description) save_task! @task end end end private def build_task(description) @task = @scope.new(@task_params.merge(description: description)) end def save_task! return if @task.save add_error(@task) raise ActiveRecord::Rollback end end