-
Notifications
You must be signed in to change notification settings - Fork 5
Home
The idea is to encapsulate User Stories in Service Objects.
The product team of a given sports website wants a new feature:
As a team manager
In order to manage my team
I want to be able to add new players to my team’s roster
- The user must exist
- The team must exist
- The team must be accepting new players
This could be a PORO implementation:
class AddPlayerToTeamRoster
attr_reader :player_id, :team_id, :player_query, :team_query
def self.call(team_query: nil, player_query: nil, player_id:, team_id:)
player_query ||= UserQuery.new
team_query ||= TeamQuery.new
new(
team_query: team_query,
player_query: player_query
).call(player_id: player_id, team_id: team_id)
end
def initialize(team_query:, player_query:)
@team_query = team_query
@player_query = player_query
end
def call(player_id:, team_id:)
@team_id = team_id
@player_id = player_id
player_must_exist!
team_must_exist!
team_must_accept_players!
team.add_to_roster(player)
end
private
def player
@player ||= player_query.call(player_id)
end
def team
@team ||= team_query.call(team_id)
end
def player_must_exist!
player.present? or raise UserNotFoundException
end
def team_must_exist!
team.present? or raise TeamNotFoundException
end
def team_must_accept_player!
team.accepts_players? or raise FullTeamException
end
end
With Injectable
you can avoid lots of boileplate:
class AddPlayerToTeamRoster
include Injectable
dependency :team_query
dependency :player_query, class: UserQuery
argument :player_id
argument :team_id
def call
player_must_exist!
team_must_exist!
team_must_accept_players!
team.add_to_roster(player)
end
private
def player
@player ||= player_query.call(player_id)
end
def team
@team ||= team_query.call(team_id)
end
def player_must_exist!
player.present? or raise UserNotFoundException
end
def team_must_exist!
team.present? or raise TeamNotFoundException
end
def team_must_accept_player!
team.accepts_players? or raise FullTeamException
end
end
And we are using just a couple of Injectable
's features.
There are some tips for you:
As you can see, we raise
meaningful exceptions using the domain's language.
Before operating, we call several private methods that follow this idiom:
def check_something! # notice the bang
some_precondition? or raise MeaningfulException
end
Those map 1:1 with the User Story acceptance criteria.
When possible, validate on your Service Objects. If you need to reuse validations, you can extract those into further POROs or use libraries like dry-validation.
If you find that you have conditional validations on your models based on the record state, you are probably in need of this approach. It's more code, but it's explicit.