Use one service object per endpoint
Service objects can be used to share code, leading to nested service objects:
Problems π
This makes service objects difficult to reason about.
Result objects also add extra complexity.
Solution π
Use exactly one service object per endpoint.
Donβt nest service objects.
Use domain objects.
What is a domain object? π
- Has data and behaviour
- Uses business language
- Can be a plain old Ruby object or an Active Record object (POROs preferred)
Where are domain objects stored? π
- ActiveRecord objects -
app/models
- POROs -
lib
Examples π
Bad π
class SearchesController < ApplicationController
def update
search = Search.find(params[:id])
if params[:action] == "back"
result = BackSearchStepService.new.call(search:)
updated_search = result.data[:search]
redirect_to search_step_path(updated_search.current_step)
elsif search.current_step.final?
result = CompleteSearchService.new.call(search:)
updated_search = result.data[:search]
redirect_to search_step_path(updated_search.current_step)
else
result = AdvanceSearchStepService.new.call(search:)
if result.success?
updated_search = result.data[:search]
redirect_to search_step_path(updated_search.current_step)
else
redirect_to search_step_path(params[:step_id]), alert: I18n.t(result.errors.first)
end
end
end
end
Good π
class SearchesController < ApplicationController
def update
result = UpdateSearchService.new.call(request:)
if result.success?
redirect_to search_step_path(result.search.current_step)
else
redirect_to search_step_path(params[:step_id]), alert: I18n.t(result.errors.first)
end
end
end
class UpdateSearchService
def call(request:)
search_params = SearchParams.from_request(request)
return ServiceObject::Error.new(["agent_finder.searches.invalid"]) if search_params.invalid?
search = Search.find(search_params[:id])
if search_params.back_pressed?
search.go_back_a_step!
elsif search.current_step.final?
search.run!
search.show_results!
else
search.advance_step!
end
ServiceObject::Success.new(search: search)
end
end