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.

Installation

Add this line to your application's Gemfile:

gem 'clean-architecture'

And then execute:

$ bundle install
$ bundle binstubs clean-architecture

Usage

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.

Clean architecture principles

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?

Practical suggestions for implementation

  • The code that manages your inputs (e.g. a Rails controller) instantiates a persistence layer object
    • Suggest: a class that implements both the Persistence interface and your own persistence interface
  persistence = ActiveRecordPersistence.new
  • 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)