clean architecture @ 1º hanami sao paulo meetup
TRANSCRIPT
Why?
• Independent of Frameworks
• Testable
• Independent of UI
• Independent of Database
https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
The Dependency Rule
"...source code dependencies can
only point inwards."
https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
This user knows too much
class User < ActiveRecord::Base # ...
def password_required? # Password is required if it is being set, but not for new records if !persisted? false else !password.nil? || !password_confirmation.nil? end endend
https://github.com/plataformatec/devise/wiki/How-To:-Override-confirmations-so-users-can-pick-their-own-passwords-as-part-of-confirmation-activation
Interactors for the rescue
class EmailSignUp < Interactor def call(request, response) # ... validate data without password # ... application rules through domain objects # ... build response endend
class PasswordSignUp < Interactor def call(request, response) # ... validate data, password included # ... application rules through domain objects # ... build response endend
What is this?def update with_unconfirmed_confirmable do if @confirmable.has_no_password? @confirmable.attempt_set_password(params[:user]) if @confirmable.valid? and @confirmable.password_match? do_confirm else do_show @confirmable.errors.clear #so that we wont render :new end else @confirmable.errors.add(:email, :password_already_set) end end
if [email protected]? self.resource = @confirmable render 'devise/confirmations/new' #Change this if you don't have the views on default path endend
https://github.com/plataformatec/devise/wiki/How-To:-Override-confirmations-so-users-can-pick-their-own-passwords-as-part-of-confirmation-activation
Cleaning it up!class Controller def update @presenter = build_response interactor.call(user_params, @presenter)
if @presenter.valid? redirect_to dashboard_path else render :new end endend
class SetPassword < Interactor def call(request, response) # ... validate data and fetch user user.set_password(request[:password]) # ... save user and other application rules endend
class User def set_password(new_password) raise PasswordAlreadySetError unless encrypted_password.blank?
self.encrypted_password = encrypt(new_password) endend
Business Objectclass User def set_password(new_password) raise PasswordAlreadySetError unless encrypted_password.blank?
self.encrypted_password = encrypt(new_password) endend
Use caseclass SetPassword < Interactor def call(request, response) # ... validate data and fetch user user.set_password(request[:password]) # ... save user and other application rules endend
Delivery mechanismclass Controller def update @presenter = build_response interactor.call(user_params, @presenter)
if @presenter.valid? redirect_to dashboard_path else render :new end endend
Interface adapter
Wrap up• Your application should be framework
agnostic
• Your application should be infrastructure agnostic
• Web is a delivery mechanism (so is a rake task, the console)
• Testing should be fast
And what about Hanami?
• Web delivery: Web::Controllers
• Use cases: Hanami::Interactor
• Infrastructure: Hanami::Repository
• Business objects: Hanami::Entity