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
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