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:

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:

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:

action1
  >>=
    (\ x1 -> action2
       >>=
         (\ x2 -> mk_action3 x1 x2 ))

to this:

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.

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.

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:

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.

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

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.

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:

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:

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:

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.

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