Benjamin Roth
credit @ Christy Justice
credit videobash
GORO (from Mortal Kombat)
also named:
- Application Service
- Operation
- Command/Strategy pattern
- Method Object
(- sigh, naming ...)
Its a design pattern
many articles in the ruby/rails community
naming (!)
test
Named out of a verb: it's an action
(LaunchProject, CreateContact)
As few as possible:
- one to trigger the object
- ... then it depends on what shape you choose, examples to come later in the slides
- reusable (ex: Builder Service as factory)
- since Service Objects represent user real actions, you can setup you tests by chaining them to create a realistic context
# INSTANCE BASED
# args in initializer
ExtractPictures.new(docx_file).call
ExtractPictures.new(docx_file).run
ExtractPictures.new(docx_file).perform
# args to performing method
ExtractPictures.new.call(docx_file)
ExtractPictures.new.run(docx_file)
ExtractPictures.new.perform(docx_file)
# CLASS BASED
ExtractPictures.call(docx_file)
ExtractPictures.run(docx_file)
ExtractPictures.perform(docx_file)
ExtractPictures.(docx_file)
Virtus and the like
is it even necessary?
(in Rails...)
app
├── services
│ ├── create_account_service.rb => ::CreateAccountService
app
├── services
│ ├── create_account.rb => ::CreateAccount
app
├── concepts
│ ├── account
│ │ ├── operation
│ │ │ ├── create.rb => ::Account::Create (load trick involved)
app
├── services
│ ├── services
│ │ ├── create_account.rb => ::Services::CreateAccount
Plenty of options here:
- dedicated response object
- boolean
- the service itself with public methods
- none: event based
How to get proper flow control?
class Service
def initialize(user_id)
@user_id = user_id
end
def call
response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}")
response.body
end
end
useful_data = Service.new(1).call
puts(useful_data)
# and hope for the best
class ExceptionService
class RequestError < StandardError; end
def initialize(user_id)
@user_id = user_id
end
def call
response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}")
if response.success?
response.body
else
raise RequestError.new("Error status #{response.code}")
end
end
end
begin
useful_data = ExceptionService.new(1).call
puts(useful_data)
rescue ExceptionService::RequestError => e
puts(e.message)
end
class BooleanService
attr_reader :body, :error
def initialize(user_id)
@user_id = user_id
end
def call
response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}")
if response.success?
@body = response.body
@success = true
else
@error = "Error status #{response.code}"
@success = false
end
end
def success?; !!@success; end
end
service = BooleanService.new(1)
service.call
if service.success?
puts(service.body)
else
puts(service.error)
end
class EventService
include Wisper::Publisher
def initialize(user_id)
@user_id = user_id
end
def call
response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}")
if response.success?
broadcast(:success, response.body)
else
broadcast(:error, "Error status #{response.code}")
end
end
end
service = EventService.new(1)
service.on(:success) {|res| puts res }
service.on(:error) {|err| puts err }
service.call
class MonadicService
def initialize(user_id)
@user_id = user_id
end
def call
response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}")
if response.success?
Success(response.body)
else
Failure("Error status #{response.code}")
end
end
end
Success({})
.bind { MonadicService.new(1).call) }
.bind {|res| puts(res); true }
.or {|err| puts(error); false }
class WfService
include Waterfall
def initialize(user_id)
@user_id = user_id
end
def call
chain do
@response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}")
end
when_falsy { @response.success? }
.dam { "Error status #{@response.code}" }
chain(:body) { @response.body }
end
end
Wf.new
.chain(result: :body) { WfService.new(1) }
.chain {|outflow| puts(outflow.result) }
.on_dam {|error| puts(error) }
Padlocks, Pont des arts - Paris
pdf = ConvertFile.new(file: file, format: 'pdf').call
pictures = ExtractPictures.new(file: pdf).call
segments = ExtractTranslationSegments.new(file: pdf).call
Useful whenever you have small specialized/reusable service objects
begin
useful_data1 = ExceptionService.new(1).call
useful_data2 = ExceptionService.new(404).call
puts(useful_data1, useful_data2)
rescue ExceptionService::RequestError => e
puts(e.message)
end
service1 = BooleanService.new(1)
service1.call
if service1.success?
service2 = BooleanService.new(404)
service2.call
if service2.success?
puts(service1.body, service2.body)
else
puts service2.error
end
else
puts service1.error
end
service1 = EventService.new(1)
service1.on(:success) do |res1|
service2 = EventService.new(404)
service2.on(:success) {|res2| puts(res1, res2) }
service2.on(:error) {|err| puts err }
service2.call
end
service1.on(:error) {|err| puts err }
service1.call
Success({})
.bind {|outflow| MonadicService.new(1).call.bind {|r| outflow.merge({ value1: r })}}
.bind {|outflow| MonadicService.new(404).call.bind {|r| outflow.merge({ value2: r })}}
.bind {|outflow| puts(outflow[:value1], outflow[:value2]); true }
.or {|error| puts(error); false }
Wf.new
.chain(result1: :body) { WfService.new(1) }
.chain(result2: :body) { WfService.new(404) }
.chain {|outflow| puts(outflow.result1, outflow.result2) }
.on_dam {|error| puts(error) }
first step to nicer architecture