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.

Table of Contents

Generated by https://github.com/ekalinin/github-markdown-toc/blob/master/gh-md-toc

Installation

Add this line to your application’s Gemfile:

“by 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 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 gateway 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 gateway 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, gateway, 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 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

This is inspired by the Haskell do-notation which lets you go from writing this:

“skell action1  »= (\ x1 -> action2  »= (\ x2 -> mk_action3 x1 x2 ))

to this:

“skell 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 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.

“by 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.

“by 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:

“by module MyBusinessDomain module UseCases class UserUpdatesNickname < CleanArchitecture::UseCases::AbstractUseCase contract do option :my_gateway_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_gateway_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_gateway_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.

“by 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.

“by 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_gateway_object: MyGateway.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:

“by 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_gateway_object: MyGateway.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.

“by module MyBusinessDomain module UseCases class SharedContract < CleanArchitecture::UseCases::Contract option :my_gateway_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_gateway_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:

“by module MyBusinessDomain module UseCases class UserUpdatesNickname < CleanArchitecture::UseCases::AbstractUseCase contract(SharedContract) do option :my_gateway_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:

“by 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 gateway object / repository / factory / etc.

These context variables can be used within the use case using the context method:

“by module MyBusinessDomain module UseCases class UserUpdatesAge < CleanArchitecture::UseCases::AbstractUseCase contract do option :required_gateway_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_gateway_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.

“by module MyBusinessDomain module UseCases class UserUpdatesChristmasWishlist < CleanArchitecture::UseCases::AbstractUseCase contract do option :required_gateway_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_gateway_object).change_most_wanted_gift(user_id, most_wanted_gift)
  end
end

end end