Share
Embed
Transcript
22 slides
-
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) -
Hanami Interactors Hanami implementation of Service Objects » Part of
hanami-utils gem » Small API » Configurable Result » Internal State Exposure -
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 -
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 -
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 -
Failing Example result = Signup.new(name: "Krombopulous Michael").call result.successful? # =>
false result.failure? # => true result.user # => #<User:0x007fa311105778 @id=nil @name="Krombopulos Michael"> -
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. -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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