# Clean Architecture This gem provides helper interfaces and classes to assist in the construction of application with Clean Architecture, as described in [Robert Martin's seminal book](https://www.amazon.com/gp/product/0134494164). Table of Contents ================= Generated by https://github.com/ekalinin/github-markdown-toc/blob/master/gh-md-toc * [Installation](#installation) * [Philosophy](#philosophy) * [Screaming architecture - use cases as an organisational principle](#screaming-architecture---use-cases-as-an-organisational-principle) * [Design principles](#design-principles) * [SRP - The Single Responsibility principle](#srp---the-single-responsibility-principle) * [OCP - The Open/Closed Principle, LSP - The Liskov Substitution Principle and DIP - The Dependency Inversion Principle](#ocp---the-openclosed-principle-lsp---the-liskov-substitution-principle-and-dip---the-dependency-inversion-principle) * [ISP - The Interface Segregation Principle](#isp---the-interface-segregation-principle) * [Component cohesion](#component-cohesion) * [REP - The Reuse/Release Equivalence Principle, CCP - The Common Closure Principle & CRP - The Common Reuse Principle](#rep---the-reuserelease-equivalence-principle-ccp---the-common-closure-principle--crp---the-common-reuse-principle) * [Component coupling](#component-coupling) * [ADP - The Acyclic Dependencies Principle](#adp---the-acyclic-dependencies-principle) * [SDP - The Stable Dependencies Principle](#sdp---the-stable-dependencies-principle) * [SAP - The Stable Abstractions Principle](#sap---the-stable-abstractions-principle) * [Structure](#structure) * [Practical suggestions for implementation](#practical-suggestions-for-implementation) * [Conventions](#conventions) * [Result objects](#result-objects) * [Idiomatic FP](#idiomatic-fp) * [Multiple bind operations](#multiple-bind-operations) * [Transactions](#transactions) * [Helper classes](#helper-classes) ## Installation Add this line to your application's Gemfile: ```ruby gem 'clean-architecture' ``` And then execute: $ bundle install $ bundle binstubs clean-architecture ## Philosophy The intention of this gem is to help you build applications that are built from the use case down, and decisions about I/O can be deferred until the last possible moment. It relies heavily on the [duckface-interfaces](https://github.com/samuelgiles/duckface) gem to enforce interface implementation. ### Screaming architecture - use cases as an organisational principle Uncle Bob suggests that your source code organisation should allow developers to easily find a listing of all use cases your application provides. Here's an example of how this might look in a Rails application. ``` - lib - my_banking_application - use_cases - retail_customer_opens_bank_account.rb - retail_customer_makes_a_deposit.rb - ... ``` Note that the use case name contains: - the user role - the action - the (sometimes implied) subject ### Design principles #### SRP - The Single Responsibility principle > A function should do one, and only one, thing We satisfy the SRP by following these rules: - An **adapter** is solely responsible for presenting the properties of a business object, or a small number of business objects, in a known interface - A **command** is solely responsible for completing an atomic I/O operation - An **entity** is solely responsible for representing, in memory, a business object whos properties do not come from a single source - An **interface** is a module that represents a contract between two classes - A **serializer** is solely responsible for taking a business object and turning it into a representation made up of purely primitive values - A **strategy** is an algorithm used by commands to compose atomic I/O operations - A **use case** is solely responsible for checking whether an actor has permissions to perform a command, and executing that command if so - A **validator** is solely responsible for validating a business object and returning a validation result #### OCP - The Open/Closed Principle, LSP - The Liskov Substitution Principle and DIP - The Dependency Inversion Principle > A software artefact should be open for extension but closed for modification > A caller should not have to know the type of an object to interact with it > Always depend on or derive from a stable abstraction, rather than a volatile concrete class We satisfy the OCP, LSP & DIP by following these rules: - We create a clean boundary between our business logic, our persistence layer and our application-specific classes using interfaces - We use interfaces wherever possible, allowing concrete implementations of those interfaces to be extended without breaking the contract - We write unit tests against interfaces, never against concrete implementations (unless interfaces don't exist) #### ISP - The Interface Segregation Principle > Where some actors only use a subset of methods available from an interface, the interface should be split into sub-interfaces supporting each type of caller We satisfy the ISP by following these rules: - Each functional area of our code is split into folders (under `lib` in Rails projects) - Each functional area defines its own interfaces - Interfaces are not shared between functional areas ### Component cohesion #### REP - The Reuse/Release Equivalence Principle, CCP - The Common Closure Principle & CRP - The Common Reuse Principle > Classes and modules that are grouped together into a component should be releasable together > Gather into components those changes the change for the same reasons and at the same times. > Classes and modules that tend to be reused together should be placed in the same component We satisfy the REP, CCP and CRP by: - Having team discussions whenever we make decisions about what a new functional area should be called and what it should contain - Ensuring that none of our functional areas make direct reference back to the parent application - Splitting functional areas out into gems when those functional areas change at a different rate than the rest of the codebase - Splitting functional areas out into standalone applications when it makes sense to do so ### Component coupling #### ADP - The Acyclic Dependencies Principle > Don't create circular dependencies I don't think I need to explain this. Just don't do it. I like explicitly including dependencies using `require` because it actually prevents you from doing this. Rails, in so many ways, makes one lazy. #### SDP - The Stable Dependencies Principle > A component always have less things depending on it than it depends on We satisfy the SDP by: - Putting sensible abstractions in place that adhere to the Single Responsibility principle - Not sharing abstractions and entities between multiple functional areas #### SAP - The Stable Abstractions Principle > A component should be as abstract as it is stable We satisfy the SAP by: - Thinking hard about the methods and parameters we specify in our interfaces. Are they solving for a general problem? Are we likely to have to change them when requirements change, and how we can avoid that? ## Structure ### Practical suggestions for implementation * The code that manages your inputs (e.g. a Rails controller) instantiates a persistence layer object * The code that manages your inputs (e.g. a Rails controller) instantiates a use case actor object - Suggest: a class that implements the `UseCaseActor` interface ``` use_case_actor = MyUseCaseActorAdapter.new(devise_current_user) ``` * The code that manages your inputs (e.g. a Rails controller) instantiates a use case input port object - Suggest: a class that implements the `BaseParameters` interface - Suggest: implement the `AuthorizationParameters` interface if you want to make authorization part of your use case logic - Suggest: implement the `TargetedParameters` if your use case operates on a single object - Suggest: use the `TargetedParameters` entity for an out-of-the-box class that gives you all of these ``` input_port = CleanArchitecture::Entities::TargetedParameters.new( use_case_actor, TargetActiveRecordClass.find(params[:id]), strong_params, persistence, other_settings_hash ) ``` * The code that manages your inputs (e.g. a Rails controller) instantiates a use case object - Suggest: a class that implements the `UseCase` interface ``` use_case = MyBankingApplication::UseCases::RetailCustomerMakesADeposit.new(input_port) ``` ## Conventions ### Result objects We make use of the [Dry-Rb](https://dry-rb.org/) collection of Gems to provide better control flow instead of relying on `raise` and `rescue`. Specifically, we use: - https://dry-rb.org/gems/dry-matcher/result-matcher/ - https://dry-rb.org/gems/dry-monads/1.0/result/ ### Idiomatic FP #### Multiple `bind` operations When you want to bind or chain multiple method calls using the previous return value, consider using [Do Notation](https://dry-rb.org/gems/dry-monads/1.0/do-notation/) This is inspired by the Haskell do-notation which lets you go from writing this: ```haskell action1 >>= (\ x1 -> action2 >>= (\ x2 -> mk_action3 x1 x2 )) ``` to this: ```haskell do x1 <- action1 x2 <- action2 mk_action3 x1 x2 ``` #### Transactions If you don't want to manually handle the wiring between multiple Success/Failure objects, you can use the [dry-transaction](https://dry-rb.org/gems/dry-transaction/) gem which abstracts this away so that you just need to define steps, and deal with the input from the output of the previous result. ```ruby require "dry/transaction" class CreateUser include Dry::Transaction step :validate step :create private def validate(input) # returns Success(valid_data) or Failure(validation) end def create(input) # returns Success(user) end end ``` # Helper classes The gem comes with some useful classes that can help you achieve a cleaner architecture with less work. ## Active Record Entity Builder Maintain a separation between your business entities and your database requires the use of gateways that build your entities from records in the database. For Rails applications using ActiveRecord this can involve a bunch of boilerplate code where your simply creating a hash from the attributes of the database record & using those to create a new instance of your struct based entity. The `CleanArchitecture::Builders::AbstractActiveRecordEntityBuilder` can help remove this boilerplate by handling 99% of the mapping for you. ### Usage: Create a builder class and have it inherit from `CleanArchitecture::Builders::AbstractActiveRecordEntityBuilder`, from here you need to point the builder at the entity you wish for it to create instances of with `.acts_as_builder_for_entity`, from there its just a case of instantiating the builder with an instance of your AR model and calling `#build`. Relations are handled easily, just define a builder for said entity and then declare the relation with `has_many :relation_name, use: MyBuilderClass` and `belongs_to :relation_name, use: MyBuilderClass`. If you wish to override the attributes used to construct the entity you can define a `#attributes_for_entity` method with said attributes in a hash, this can be useful for complex relations, files and other attributes that don't map perfectly from the database to your struct based entity. ```ruby class Person < ApplicationRecord has_many :interests, autosave: true, dependent: :destroy belongs_to :father end class Entities::Person < Dry::Struct attribute :forename, Types::Strict::String attribute :surname, Types::Strict::String attribute :father, Types.Instance(Person) attribute :interests, Types.Array(Types.Instance(Interest)) attribute :birth_month, Types::Strict::String end class PersonBuilder < CleanArchitecture::Builders::AbstractActiveRecordEntityBuilder acts_as_builder_for_entity Entities::Person has_many :interests, use: InterestBuilder belongs_to :father, use: PersonBuilder def attributes_for_entity { birth_month: @ar_model_instance.birth_date.month } end end ``` ## Use cases with contracts, errors & form objects Finding a way to map HTTP parameters to parameters within your use case, pass back & display validation errors and coerce types are difficult to replace when moving away from the typical `MyMode.update(params.permit(:some_param))` that a standard Rails app might use. The `CleanArchitecture::UseCases` component contains some useful classes for helping to make replacing these functions a little easier whilst still maintaining good boundaries. The 'contracts' use `dry-validation` and support all options included the sharing of contracts between use cases, more information can be found here: https://dry-rb.org/gems/dry-validation/. Don't be afraid of the seemingly magical `.contract` method that use cases have, all its doing is creating an anonymous `Class` and storing it in a class variable, the methods existence is justified by how it enables form objects & helps to standardise the process a little. `dry-validation` itself is actually built on top of `dry-schema`, as such most of the useful information on predicates can be found here: https://dry-rb.org/gems/dry-schema/basics/built-in-predicates/ ### Usage: Usage is fairly simple, use cases define a contract, parameters handed to a use case are validated, at which point if the parameters aren't valid you'll get an `Errors` object back within a `Failure`, if they are you'll get a success with a `Parameters`. Here is an example use case for a user updating their username that does a pre-flight check to ensure the username is available: ```ruby module MyBusinessDomain module UseCases class UserUpdatesNickname < CleanArchitecture::UseCases::AbstractUseCase contract do option :my_persistence_object params do required(:user_id).filled(:id) required(:nickname).filled(:str) end rule(:nickname).validate(:not_already_taken) register_macro(:not_already_taken) do unless my_persistence_object.username_is_available?(values[key_name]) key.failure('is already taken') end end end extend Forwardable include Dry::Monads::Do.for(:result) def result valid_params = yield result_of_validating_params context(:my_persistence_object).result_of_updating_nickname( valid_params[:id], valid_params[:nickname] ) end end end end ``` You could imagine a page with a simple form asking the user to enter their new username and you may want this form to display that message if the username isn't available. The `Form` class can be used to assist with the mapping of http parameters to the use case parameters. Best of all since the forms aren't tied to the use cases they can live within your web app far away from your business logic. ```ruby module MyWebApp class NicknameUpdateForm < CleanArchitecture::UseCases::Form acts_as_form_for MyBusinessDomain::UseCases::UserUpdatesNickname end end ``` The standard Rails form builder works with instances of `Form`. Putting these both together a controller action would look like the below example. - A new instance of the use case is passed a parameter object built from `params`. - If the use case is successful we'll show a flash message. - If unsuccessful we'll take the returned `Errors` (`Entities::FailureDetails` and plain strings are also handled by `#with_errors`) and add them to the form with `#with_errors` and re-render the `edit` action. ```ruby module MyWebApp class NicknamesController < ApplicationController def update Dry::Matcher::ResultMatcher.call(user_updates_nickname.result) do |matcher| matcher.success do |_| flash[:success] = 'Nickname successfully updated' redirect_to action: :edit end matcher.failure do |errors| @form = nickname_update_form.with_errors(errors) render :edit end end end private def user_updates_nickname MyBusinessDomain::UseCases::UserUpdatesNickname.new(nickname_update_form.to_parameter_object) end def nickname_update_form @nickname_update_form ||= NicknameUpdateForm.new( params: params.permit(:user_id, :nickname), context: { my_persistence_object: MyPersistence.new } ) end end end ``` There won't always be a complex form in front of a use case, sometimes its just one parameter, using the above example example you could easily execute the use case with a manually constructed parameter object if it was say an API only endpoint: ```ruby module MyWebApp class NicknamesController < ApplicationController def update Dry::Matcher::ResultMatcher.call(user_updates_nickname.result) do |matcher| matcher.success do |_| render json: { success: true } end matcher.failure do |errors| render json: { errors: errors.full_messages } end end end private def user_updates_nickname MyBusinessDomain::UseCases::UserUpdatesNickname.new(user_updates_nickname_parameters) end def user_updates_nickname_parameters MyBusinessDomain::UseCases::UserUpdatesNickname.parameters( context: { my_persistence_object: MyPersistence.new }, user_id: params[:user_id], nickname: params[:nickname] ) end end end ``` Elements of contracts can be shared amongst use cases, this can be very helpful for `options` (context) that you know every use case in a domain may require or validation rules that you know will be used in multiple use cases. Shared contracts can help tidy up your specs too by allowing you to test all your validation logic separately to what the use case itself does. ```ruby module MyBusinessDomain module UseCases class SharedContract < CleanArchitecture::UseCases::Contract option :my_persistence_object register_macro(:not_already_taken?) do unless not_already_taken?(values[key_name]) key.failure('is already taken') end end private def not_already_taken?(username) my_persistence_object.username_is_available?(values[key_name]) end end end end ``` Using a shared contract is simple; when you define the contract for a use case just specify the shared contract as an argument to `.contract`: ```ruby module MyBusinessDomain module UseCases class UserUpdatesNickname < CleanArchitecture::UseCases::AbstractUseCase contract(SharedContract) do option :my_persistence_object params do required(:user_id).filled(:id) required(:nickname).filled(:str) end rule(:nickname).validate(:not_already_taken) ``` Use cases themselves are outside of their params just plain old ruby objects. There are only a few methods you'll use composing use cases: ### `#result_of_validating_params` This methods gives you a Result monad with either `Success` containing a hash of the valid params or `Failure` with an `Errors` instance containing the validation errors. The `Do` syntax from `dry-monads` helps to tidy the usage of this method up: ```ruby module MyBusinessDomain module UseCases class UserUpdatesAge < CleanArchitecture::UseCases::AbstractUseCase contract do params do required(:user_id).filled(:int) required(:age).filled(:int) end end include Dry::Monads::Do.for(:result) def result valid_params = yield result_of_validating_params Dry::Monads::Success(valid_params[:age] * 365) end end end end ``` ### `#context` Any context variables defined as `option`'s in your use case contract have to be specified whenever creating an instance of the parameter objects for your use case. In practice this means you can't accidentally forget to pass in say a persistence object / repository / factory / etc. These context variables can be used within the use case using the `context` method: ```ruby module MyBusinessDomain module UseCases class UserUpdatesAge < CleanArchitecture::UseCases::AbstractUseCase contract do option :required_persistence_object params do required(:user_id).filled(:int) required(:age).filled(:int) end end include Dry::Monads::Do.for(:result) def result valid_params = yield result_of_validating_params context(:required_persistence_object).update_user_age_result( valid_params[:user_id], valid_params[:age] ) end end end end ``` You may wish to tidy access to context variables away into private methods to mask the implementation details. ### `#fail_with_error_message` This method can be used for returning a simple message wrapped in an instance of `Errors`. Optionally you can specify the type of error should you wish for your controller to react different for say a record not being found vs an API connection error. ```ruby module MyBusinessDomain module UseCases class UserUpdatesChristmasWishlist < CleanArchitecture::UseCases::AbstractUseCase contract do option :required_persistence_object params do required(:user_id).filled(:int) required(:most_wanted_gift).filled(:str) end end include Dry::Monads::Do.for(:result) CHRISTMAS_DAY = Date.new('2019', '12', '25') def result valid_params = yield result_of_validating_params if Date.today == CHRISTMAS_DAY return fail_with_error_message('Uh oh, Santa has already left the North Pole!') end context(:required_persistence_object).change_most_wanted_gift(user_id, most_wanted_gift) end end end end ```