and their different flavors
@apneadiving
Rails & Angular freelancer
They are useless!
According to DHH (Rails creator):
If anything, controller actions are the service layer. Which is the point. Don't put another service layer on top of it.
Talk over?
Community is big, with various point of views.
Services are sometimes recommended:
do you want to have business logic leak in controller actions?
do you really think a fat model always make sense?
then you'd create a class to abstract the logic?
No
No
Yes
you have a service object...
class PlaceOrder
attr_reader :order, :client
def initialize(client, order)
@client, @order = client, order
end
def call
ActiveRecord::Base.transaction do
debit_client
remove_items_from_stock
validate_order
prepare_for_shipping
end
true
rescue ActiveRecord::ActiveRecordError => exception
false
end
private
# def debit_client
# def remove_items_from_stock
# def validate_order
# def prepare_for_shipping
end
Meaningful name
class PlaceOrder
attr_reader :order, :client
def initialize(client, order)
@client, @order = client, order
end
def call
ActiveRecord::Base.transaction do
debit_client
remove_items_from_stock
validate_order
prepare_for_shipping
end
true
rescue ActiveRecord::ActiveRecordError => exception
false
end
private
# def debit_client
# def remove_items_from_stock
# def validate_order
# def prepare_for_shipping
end
Standard way to trigger
class PlaceOrder
attr_reader :order, :client
def initialize(client, order)
@client, @order = client, order
end
def call
ActiveRecord::Base.transaction do
debit_client
remove_items_from_stock
validate_order
prepare_for_shipping
end
true
rescue ActiveRecord::ActiveRecordError => exception
false
end
private
# def debit_client
# def remove_items_from_stock
# def validate_order
# def prepare_for_shipping
end
Do what you need
class PlaceOrder
attr_reader :order, :client
def initialize(client, order)
@client, @order = client, order
end
def call
ActiveRecord::Base.transaction do
debit_client
remove_items_from_stock
validate_order
prepare_for_shipping
end
true
rescue ActiveRecord::ActiveRecordError => exception
false
end
private
# def debit_client
# def remove_items_from_stock
# def validate_order
# def prepare_for_shipping
end
Decide the output
# in controller
def place_order
if PlaceOrder.new(current_user, order).call
flash[:success] = "thank you"
render :index
else
flash[:error] = "sorry mate"
render :cart
end
end
def order
@order ||= Order.find(params[:id])
end
Use in controller
if child_service.success?
# continue with parent service code
# extract relevant data
else
# extract errors
# make parent service fail
end
if service1.success?
if service2.success?
if service3.success?
if service4.success?
else
end
else
end
else
end
else
end
class LooksUpTaxPercentageAction
include LightService::Action
expects :order
promises :tax_percentage
executed do |context|
tax_ranges = TaxRange.for_region(context.order.region)
next context if object_is_nil?(tax_ranges, context, 'tax ranges not found')
context.tax_percentage = tax_ranges.for_total(context.order.total)
next context if object_is_nil?(context.tax_percentage, context, 'tax % not found')
end
def self.object_is_nil?(object, context, message)
if object.nil?
context.fail!(message)
return true
end
false
end
end
I'd say YES
error messages
named `promises`, and properties attached to context
context.tax_percentage =
context.fail!(message)
standard if / else check
No win here
I'd say yes (at the cost of a class)
class CalculatesTax
include LightService::Organizer
def self.for_order(order)
with(order: order).reduce(
LooksUpTaxPercentageAction,
CalculatesOrderTaxAction,
ProvidesFreeShippingAction
)
end
end
def update
@order = Order.find(params[:id])
service_result = CalculatesTax.for_order(@order)
if service_result.failure?
render action: :edit, error: service_result.message
else
redirect_to checkout_shipping_path(@order),
notice: "Tax was calculated successfully"
end
end
rolled_back do |context|
context.user.destroy
end
class AuthenticateUser
include Interactor
def call
if user = User.authenticate(context.email, context.password)
context.user = user
context.token = user.secret_token
else
context.fail!(message: "authenticate_user.failure")
end
end
end
I'd say YES
error messages
properties attached to the context
context.fail!(message: "authenticate_user.failure")
context.token =
standard if / else check
No win here
class PlaceOrder
include Interactor::Organizer
organize CreateOrder, ChargeCard, SendThankYou
end
I'd say yes (at the cost of a class)
def rollback
context.order.destroy
end
def create
result = PlaceOrder.call(order_params: order_params)
if result.success?
redirect_to result.order
else
@order = result.order
render :new
end
end
class TaxCalculator
include WaterFall
include ActiveModel::Validations
validates :tax_ranges, :tax_percentage, presence: true
def initialize(order); @order = order; end
def call
self
.when_falsy { valid? }
.dam { errors }
.chain(:tax_result) { calculate }
end
private
def calculate
(@order.total * (tax_percentage/100)).round(2)
end
def tax_percentage
@tax_percentage ||= tax_ranges.for_total(@order.total)
end
def tax_ranges
@tax_ranges ||= TaxRange.for_region(@order.region)
end
end
I'd say YES
error messages
properties attached to the service
.dam { errors }
.chain(:tax_result) { calculate }
Use whatever container for your errors, could be a mere string
self
.chain do
CloseProject.new(project).call
.chain { NotifyProjectClosed.new(project) }
end
.chain { Log.new(:project_closed, project) }
A second service would be a waterfall
so its just a matter of chaining flows
using chain
errors would propagate & stop next chains
Yes, thats still the point of chain
Wf.new
.chain(order: :order) { CreateOrder.new(order_params, current_user) }
.chain(bill: :bill) {|outflow| ChargeCard.new(outflow.order) }
.chain {|outflow| SendThankYou.new(outflow.order, outflow.bill) }
def rollback
order.destroy
end
(FYI I recommend nested transactions though)
def get_tax
Wf.new
.chain(tax: :tax_result) { TaxCalculator.new(order) }
.chain {|outflow| render json: { value: outflow.tax } }
.on_dam {|errors| render json: { errors: errors.full_messages }, status: 422 }
end
Just like when you chain services
Disclaimer: I wrote the library...
(think of it as sync promises if you know javascript)