Skip to content

Instantly share code, notes, and snippets.

@CrowdHailer
Last active August 29, 2015 14:11
Show Gist options
  • Save CrowdHailer/ab6af39dfed88a3ccb60 to your computer and use it in GitHub Desktop.
Save CrowdHailer/ab6af39dfed88a3ccb60 to your computer and use it in GitHub Desktop.
Tell don't ask and service objects
# 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
# 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
# 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
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
# 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
@CrowdHailer
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment