Last active
August 29, 2015 14:11
-
-
Save CrowdHailer/ab6af39dfed88a3ccb60 to your computer and use it in GitHub Desktop.
Tell don't ask and service objects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
gist |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app.rb | |
# Success and failure actions both passed to the service object during initialization along with injecting dependencies. | |
# Then the route specific information passed during call. | |
post :new, :map => '/posts/new' do | |
# form object specific to being a webframework, can imagine different frameworks making params available in different ways | |
form = Post::DetailsForm.new(params[:post]) | |
CreatePost.new( | |
:logger => ErrorLogger, | |
:success => ->(post){ | |
redirect to("/posts/#{post.id}") | |
}, | |
:failure => ->(error){ | |
flash.error = error.message | |
redirect to('/posts') | |
} | |
).call(form, current_user) | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# some/config.rb | |
CreatePost.use :logger => ErrorLogger | |
# app.rb | |
post :new, :map => '/posts/new' do | |
form = Post::DetailsForm.new(params[:post]) | |
begin | |
post = CreatePost.call(form, current_user) | |
redirect to("/posts#{post.id}") | |
rescue UserSuspended | |
flash.error = 'Sorry you are currently unable to submit posts' | |
redirect to('/posts') | |
# Controller grows if several error cases to manage. | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# some/config.rb | |
CreatePost.use :logger => ErrorLogger | |
# app.rb | |
post :new, :map => '/posts/new' do | |
# Everything in the controller assumes success | |
form = Post::DetailsForm.new(params[:post]) | |
post = CreatePost.(form, current_user) | |
redirect("/posts/#{post.id}") | |
end | |
error UserSuspended do | |
# Looses context if different message wanted when a suspended user is trying to post vs trying to show | |
flash.error = 'Sorry you are currently unable to submit posts' | |
redirect to('/posts') | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
post :new, :map => '/posts/new' do | |
form = Post::DetailsForm.new(params[:post]) | |
CreatePost.(form, current_user) do |post| | |
redirect("/posts/#{post.id}") | |
end | |
end | |
error UserSuspended do | |
# Looses context if different message wanted when a suspended user is trying to post vs trying to show | |
flash.error = 'Sorry you are currently unable to submit posts' | |
redirect to('/posts') | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# An Implementation that best describes a use case as used in app4 | |
# This manages to have eastward travel at all points when used | |
# I still worry though this might become cumbersome when there are details of multiple failure modes that might want to be reported to the user | |
class Service | |
CallNotImplemended = Class.new(StandardError) | |
class << self | |
attr_accessor :dependencies | |
# Add a dependency to the default dependencies passed to the service when it is initialized | |
def use(hash) | |
dependencies.merge!(hash) | |
end | |
# Creates a new service instance with sane dependencies injected | |
def build | |
new dependencies | |
end | |
# Use the class call method in initialize and delegate to the instance call method | |
# The entire class can be mocked with a lambda or proc | |
def call(*args, &block) | |
build.call(*args, &block) | |
end | |
end | |
# Bit messy there might be better ways to set this up. possibly by not using attr_accessor in the class methods | |
self.dependencies = {} | |
def self.inherited(base) | |
base.dependencies = {} | |
end | |
def initialize(dependencies={}) | |
@dependencies = dependencies | |
end | |
# Zealous meta programming. The dependencies hash can be accessed just by calling the key as a method | |
# Perhaps better implemented by defining methods during the initialize step on the instance. | |
def method_missing(meth, *args, &block) | |
@dependencies.fetch(meth){ super } | |
end | |
# A general service object can have no knowledge of the domain logic you need to implement so it warns you to define that logic if not done already | |
def call(*args, &block) | |
raise CallNotImplemended.new 'Implement a call method' | |
end | |
end | |
# Simple example logger | |
class Logger | |
def self.log | |
puts 'logging' | |
end | |
end | |
class CreatePost < Service | |
# Add dummy logger to class dependencies hash | |
use :logger => Logger | |
def call(form, current_user, &block) | |
# Better buisness logic here. | |
# However some code example | |
form.merge!(:author => current_user) | |
logger.log | |
yield form | |
end | |
end | |
# Just so this gist doesn't need a framework | |
def render(post) | |
"#{post[:title]} by #{post[:author]}" | |
end | |
# Run this file and it should work | |
CreatePost.({:title => 'New Post'}, 'Nigel') do |post| | |
body = render post | |
puts body | |
end | |
# Output | |
# => logging | |
# => New Post by Nigel |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
So I have been investigating the idea of tell don't ask and how it relates to service objects. This interesting talk has a selection of rules to ensure commands and not queries. One of them is the exception that factories can return objects other than themselves. What is anyone's opinion on service objects acting as a factory.
App one shows it is possible to be purely command but I feel that particularly option three is clearest