-
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
endWith 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
endAnd 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
endThose 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.