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. Richer Services with Hanami Interactors

    View Slide

  2. 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)

    View Slide

  3. Hanami Interactors
    Hanami implementation of Service Objects
    » Part of hanami-utils gem
    » Small API
    » Configurable Result
    » Internal State Exposure

    View Slide

  4. 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

    View Slide

  5. Successful Example
    result = Signup.new(name: 'Krombopulos Michael').call
    result.failure? # => false
    result.successful? # => true
    result.user # => #
    result.params # => { :name=>"Krombopulos Michael" }
    result.foo # => raises NoMethodError

    View Slide

  6. 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

    View Slide

  7. Failing Example
    result = Signup.new(name: "Krombopulous Michael").call
    result.successful? # => false
    result.failure? # => true
    result.user # => #

    View Slide

  8. 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.

    View Slide

  9. 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

    View Slide

  10. 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

    View Slide

  11. 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

    View Slide

  12. This
    action
    is doing
    too much

    View Slide

  13. 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

    View Slide

  14. Where should
    the creation
    code live?

    View Slide

  15. Enter
    Hanami
    Interactors

    View Slide

  16. 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

    View Slide

  17. 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

    View Slide

  18. 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

    View Slide

  19. 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

    View Slide

  20. 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

    View Slide

  21. And that's
    How Interactors
    Can help you
    Organise your code

    View Slide

  22. Questions?

    View Slide