Add this line to your application's Gemfile:
gem 'usecasing'
And then execute:
$ bundle
Let's build a Invoice System, right ? So the product owner will create some usecases/stories to YOU.
Imagine this usecase/story:
As a user I want to finalize an Invoice and an email should be delivered to the customer.
Let's build a controller
class InvoicesController < ApplicationController
def finalize
params[:current_user] = current_user
# params = { invoice_id: 123 , current_user: #<User:007> }
context = FinalizeInvoiceUseCase.perform(params)
if context.success?
redirect_to invoices_path(context.invoice)
else
@errors = context.errors
redirect_to invoices_path
end
end
end
Ok, What is FinalizeInvoiceUseCase ?
FinalizeInvoiceUseCase will be responsible for perform the Use Case/Story. Each usecase should satisfy the Single Responsibility Principle and to achieve this principle, one usecase depends of others usecases building a Chain of Resposibility.
class FinalizeInvoiceUseCase < UseCase::Base
depends FindInvoice, ValidateToFinalize, FinalizeInvoice, SendEmail
end
IMHO, when I read this Chain I really know what this class will do. astute readers will ask: How FindInvoice pass values to ValidateToFinalize ?
When we call in the Controller FinalizeInvoiceUseCase.perform we pass a parameter (Hash) to the usecase.
This is what we call context, the usecase context will be shared between all chain.
class FindInvoice < UseCase::Base
def before
@user = context.current_user
end
def perform
# we could do that in one before_filter
invoice = @user.invoices.find(context.invoice_id)
# assign to the context make available to all chain
context.invoice = invoice
end
end
Is the invoice valid to be finalized ?
class ValidateToFinalize < UseCase::Base
def perform
#failure will stop the chain flow and mark the context as error.
failure(:validate, "#{context.invoice.id} not ready to be finalized") unless valid?
end
private
def valid?
#contextual validation to finalize an invoice
end
end
So, after validate, we already know that the invoice exists and it is ready to be finalized.
class FinalizeInvoice < UseCase::Base
def before
@invoice = context.invoice
end
def perform
@invoice.finalize! #update database with finalize state
context.customer = invoice.customer
end
end
Oww, yeah, let's notify the customer
class SendEmail < UseCase::Base
def perform
to = context.customer.email
# Call whatever service
EmailService.send('customer_invoice_template', to, locals: { invoice: context.invoice } )
end
end
There are 2 ways to stop the dependency flow.
- stop! ( stop the flow without marking the usecase with error )
- failure ( stop the flow but mark the usecase with errors )
Imagine a Read Through Cache Strategy. How can we stop the usecase flow without marking as failure ?
class ReadThrough < UseCase::Base
depends MemCacheReader, DataBaseReader, MemCacheWriter
end
class MemCacheReader < UseCase::Base
def perform
context.data = CacheAdapter.read('key')
stop! if context.data
end
end
class DataBaseReader < UseCase::Base
def perform
context.data = DataBase.find('key')
end
end
class MemCacheWriter < UseCase::Base
def perform
CacheAdapter.write('key', context.data);
end
end
Given the following UseCase called SampleCase, which has a dependency on UseCase AnotherSampleCase:
class SampleCase < UseCase::Base
depends AnotherSampleCase
def before
end
def perform
end
end
By default the execution order will be:
- first execute dependent UseCases (in this case AnotherSampleCase)
- next execute logic in method before if any
- execute logic in method perform if any
It's possible to change this execution order so that it executes in the following order:
- first execute logic in method before if any
- next execute dependent UseCases (in this case AnotherSampleCase)
- lastly execute logic in method perform if any
To do so just configure your UseCase before using it, as follows:
UseCase.configure do |config|
config.before_depends = true
end
As mentioned previously values passed to UseCases are made available by means of an object called 'context', as in the following example UseCase:
class BuildCalendar < UseCase::Base
def perform
context.calendar =
context.current_user.calendar.build(calendar_attributes)
unless context.calendar.valid?
failure(:failure, 'invalid calendar attributes')
else
context.success_message = 'calendar is valid'
end
end
end
In order to help access these values it's possible to indicate, during UseCase definition, setters and getters for attributes of context, similar to ruby's attr_* notation. So we could rewrite the previous UseCase as follows:
class SampleCase < UseCase::Base
context_reader :current_user, :calendar_attributes
context_writer :success_message
context.accessor :calendar
def perform
# we're invoking a setter method, don't forget to use self
self.calendar = current_user.build(calendar_attributes)
unless calendar.valid?
failure(:failure, 'invalid calendar attributes')
else
self.success_message = 'calendar is valid'
end
end
end
There might be cases were we need more control over the execution chain, as in the execution chain changing depending on the result of each UseCase, imagine the following case:
- check if user credentials are valid
- if valid keep going
- if invalid and if it's the fifth retry we send an email to admin
This requires an if condition before invoking the UseCase to check if email should be sent or not. If it is to send email we could manually handle the UseCase invocation, or you could use method 'invoke!':
class ValidateUserCredentials < UseCase::Base
context_reader :credentials
context_writer :admin_email_body
def perform
unless credentials_valid?
failure(:failure, 'invalid credentials')
if too_many_retries?
self.admin_email_body = "#{credentials.email} tried too many times!"
invoke! SendEmailToAdmin
end
end
end
def credentials_valid?
# check if credentials are valid
end
end
This will invoke UseCase SendEmailToAdmin with the current context object.
We can also control the flow with method 'skip!'. This method is similar to method 'stop!' where it stops the execution chain, but it only stops the execution chain of the current UseCase (the following case is only applicable when UseCase is configured with the execution order before -> depends -> perform):
class CreateEventBase < UseCase::Base
depends CreateEvent, SetAlarm, RaiseSuccess
end
class SetAlarm < UseCase::Base
context_reader :event
def before
skip! unless event.set_alarm?
end
depends SendEmail
def perform
# set alarm
end
end
For this example what happens when during the invocation of CreateEventBase.perform the method 'skip!' in SetAlarm#before is invoked? The UseCase SetAlarm and its dependent UseCases will not be invoked, without interrupting the execution chain, meaning that RaiseSuccess will still be invoked.
Let me know what do you think about it.
# None of those methods are required.
class BusinessRule < UseCase::Base
def before
# executed before perform
end
def perform
# execute the responsibility that you want
end
def rollback
# Will be called only on failure
end
end
Create real case examples (40%)
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request