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
- Suggest: a class that implements both the
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
- Suggest: a class that implements the
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
- Suggest: a class that implements the
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
- Suggest: a class that implements the
use_case = MyBankingApplication::UseCases::RetailCustomerMakesADeposit.new(input_port)