Skip to content

Instantly share code, notes, and snippets.

@batusa
Last active July 11, 2019 13:21
Show Gist options
  • Save batusa/67eee451bfcbceb5c7a63cd43b988b64 to your computer and use it in GitHub Desktop.
Save batusa/67eee451bfcbceb5c7a63cd43b988b64 to your computer and use it in GitHub Desktop.
Some notes from my reading of Clean Architecture book (from Part V on)

Architecture

The architecture of a software system is the shape given to that system by those who build it. Division into components, arrangement of those components, and the ways in which those components communicate with each other.

The purpose of that shape is to facilitate the development, deployment, operation and maintenance of the software system contained within it.

Minimize lifetime cost of the system and maximize programmer productivity.

Keeping options open

What are the options that we need to leave open? They are the details that don’t matter.

All software systems can be decomposed into two major elements: policy and details. The policy element embodies all the business rules and procedures. The policy is where the true value of the system lives.

The goal of the architect is to create a shape for the system that recognizes policy as the most essential element of the system while making the details irrevelant to that policy.

Decoupling layers

Employ the SRP and the CCP to separate those things that change for different reasons, and to collect those things that change for the same reasons - given the context of the intent of the system.

Decoupling use cases

Use cases are a very natural way to divide the system.

Duplication

There is false or accidental duplication. If two apparently duplicated sections of code evolve along different paths - if they change at different rates, and for different reasons - then they are not true duplicates.

By the same token, when you are separating layers horizontally, you might notice that the data structure of a particular database record is very similar to the data structure of a particular screen view. You may be tempted to simply pass the database record up to the UI, rather than to create a view model that looks the same and copy the elements across. Be careful: This duplication is almost certainly accidental. Creating the separate view model is not a lot of effort, and it will help you keep the layers properly decoupled.

Decoupling mode

A good architecture will allow a system to be born as a monolith, deployed in a single file, then to grow into a set of independently deployable units, and then all the way to independent services and/or micro-services. Later, as things change, it should allow for reversing that progression and sliding all the way back down into a monolith.

Boundaries: drawing lines

Which lines do you draw, and when do you draw them?

You draw (boundary) lines between things that matter and things that don’t. The GUI doesn’t matter to the business rules, so there should be a line between them.

Component BusinessRules:

Class BusinessRules Interface Database

Component Database:

Class DatabaseAccess implements BusinessRules\Database

To draw boundary lines in a software architecture, you first partition the system into components. Some of those components are core business rules; others are plugins that contain necessary functions that are not directly related to the core business. Then you arrange the code in those components such that the arrows between them point in one direction - toward the core business.

Boundary anatomy

Boundary crossing

At runtime, a boundary crossing is nothing more than a function on one side of the boundary calling a function on the other side and passing along some data.

When one source code module changes, other source code modules may have to be changed or recompiled, and then redeployed. Managing and building firewalls against this change is what boundaries are all about.

The Dreaded Monolith

When a high-level client needs to invoke a lower-level service, dynamic polymorphism is used to invert the dependency against the flow of control.

Policy and Level

Policies that change for the same reasons, and at the same times, are at the same level and belong together in the same component. Policies that change for different reasons, or at different times, are at different levels and should be separated into different components.

Level

The farther a policy is from both inputs and outputs of the system, the higher its level. The policies that manage input and output are the lowest-level policies in the system.

Higher-level policies - those that are farthest from the inputs and outputs - tend to change less frequently, and for more important reasons, than lower-level policies. Lower-level policies - those that are closest to the inputs and outputs - tend to change frequently, and with more urgency, but for less important reasons.

Another way to look at this issue is to note that lower-level components should be plugins to the higher-level components.

Business Rules

The business rules should be the most independent and reusable code in the system.

Entities

The Entity object either contains the Critical Business Data or has very easy access to that data. The interface of the Entity consists of the functions that implement the Critical Business Rules that operate on that data.

The Entity is pure business and nothing else.

Use Cases

A use case is a description of the way that an automated system is used. It specifies the input to be provided by the user, the output to be returned to the user, and the processing steps involved in producing that output.

A use case describes application-specific business rules as opposed to the Critical Business Rules within the Entities.

Use cases contain the rules that specify how and when the Critical Business Rules within the Entities are invoked. Use cases control the dance of the Entities.

From the use case, it is impossible to tell whether the application is delivered on the web, or on a thick client, or on a console, or is a pure service.

Entities have no knowledge of the use cases that control them. This is another example of the direction of the dependencies following the Dependency Inversion Principle. High-level concepts, such as Entities, know nothing of lower-level concepts, such as use cases. Instead, the lower-level use cases know about the higher-level Entities.

Request and Response Models

The use case class accepts simple request data structures for its input, and returns simple response data structures as its output. These data structures are not dependent on anything.

Screaming Architecture

So what does the architecture of your application scream? When you look at the top-level directory structure, and the source files in the highest-level package, do they scream “Health Care System”, or “Accounting System”, or “Iventory Management System”? Or do they scream “Rails”, or “Spring/Hibernate”, or “ASP”?

The Theme of an Architecture

Software architecture are structures that support the use cases of the system. Just as the plans for a house or a library scream about the use cases of those buildings, so should the architecture of a software scream about the use cases of the application.

Frameworks are options to be left open.

But what about the Web?

The web is a delivery mechanism - an IO device - and your application architecture should treat it as such. The fact that your application is delivered over the web is a detail and should not dominate your system structure.

Frameworks are Tools, not ways of life

Develop a strategy that prevents the framework from taking over that architecture.

The Clean Architecture

Promotes an architecture that is: independent of frameworks; testable; independent of the UI; independent of the database; independent of any external agency.

Dependency Rule

Source code dependencies must point only inward, toward the higher-level policies.

Entities

Entities encapsulate enterprise-wide Critical Business Rules. An entity can be an object with methods, or it can be a set of data structures and functions.

If you don’t have an enterprise and are writing just a single application, then these entities are the business objects of the application. They encapsulate the most general and high-level rules.

Use Cases

Application-specific business rules. It encapsulates and implements all of the use cases of the system. These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their Critical Business Rules to achieve the goals of the use case.

We also do not expect this layer to be affected by changes to externalities such as the database, the UI, or any of the common frameworks.

Interface Adapters

The software in the interface adapters layer is a set of adapters that convert data from the format most convenient for the use cases and entities, to the format most convenient for some external agency such as the database or the web.

Also in this layer is any other adapter necessary to convert data from some external form, such as an external service, to the internal form used by the use cases and entities.

Only Four Circles?

Frameworks & drivers (web, UI, DB) → Interface Adapters (Controllers, Gateways, Presenters) → Application Business Rules (Use Cases) → Enterprise Business Rules (Entities)

As you move inward, the level of abstract and policy increases. The outermost circle consists of low-level concrete details. As you move inward, the software grows more abstract and encapsulates higher-level policies.

Crossing Boundaries

Suppose the use case needs to call the presenter. This call must not be direct because that would violate the Dependency Rule: No name in an outer circle can be mentioned by an inner circle. So we have the use case call an interface in the inner circle, and have the presenter in the outer circle implement it.

The same technique is used to cross all the boundaries in the architectures. We take advantage of dynamic polymorphism to create source code dependencies that oppose the flow of control so that we can conform to the Dependency Rule.

Which Data Crosses The Boundaries

Typically the data that crosses the boundaries consists of simple data structures. You can use basic structs or simple data transfer objects if you like.

The important thing is that isolated, simple data structures are passed across the boundaries. We don’t want to cheat and pass Entity objects or database rows.

Thus, when we pass data across a boundary, it is always in the form that is most convenient for the inner circle.

Presenters and Humble Objects

At each architectural boundary, we are likely to find the Humble Object pattern lurking somewhere nearby. The communication across that boundary will almost always involve some kind of simple data structure, and the boundary will frequently divide something that is hard to test from something that is easy to test.

The Humble Object Pattern

The idea is very simple: Split the behaviors into two modules or classes. One of those modules is humble; it contains all the hard-to-test behaviors stripped down to their barest essence. The other module contains all the testable behaviors that were stripped out of the humble object.

Presenters and Views

The View is the humble object that is hard to test. The code in this object is kept as simple as possible. It moves data into the GUi but does not process that data.

The Presenter is the testable object. Its job is to accept data from the application and format it for presentation so that the View can simply move it to the screen. For example, if the application wants a date displayed in a field, it will hand the Presenter a Date object. The Presenter will then format that data into an appropriate string and place it in a simple data structure called the View Model, where the View can find it.

Anything and everything that appears on the screen, and that the application has some kind of control over, is represented in the View Model as a string, or a boolean, or an enum. Nothing is left for the View to do other than to load data from the View Model into the screen. Thus the View is humble.

Database Gateways

Between the use case interactors and the database are the database gateways. These gateways are polymorphic interfaces that contain methods for every create, read, update, or delete operation that can be performed by the application on the database.

If the application needs to know the last names of all the users who logged in yesterday, then the UserGateway interface will have a method named getLastNamesOfUsersWhoLoggedInAfter that takes a Date as its argument and returns a list of last names.

We do not allow SQL in the use cases layer; instead, we use gateway interfaces that have appropriate methods. Those gateways are implemented by classes in the database layer. That implementation is the humble object.

The interactors, in contrast, are not humble because they encapsulate application-specific business rules. Although they are not humble, those interactors are testable.

Data Mappers

Objects are not data structures. At least, they are not data structures from their users’ point of view. The users of an object cannot see the data, since it is all private. Those users see only the public methods of that object.

A data structure, in contrast, is a set of public data variables that have no implied behavior.

Case Study: Video Sales

The Product

Our first step in determining the initial architecture of the system is to identify the actors and use cases.

Dependency Management

All dependencies cross the boundary lines in one direction, and they always point toward the components containing the higher-level policy.

Also notice that using relationships (open arrows) point with the flow of control, and that the inheritance relationships (closed arrows) point against the flow of control.

The Missing Chapter

Package By Layer

The first, and perhaps simplest, design approach is the traditional horizontal layered architecture, where we separate our code based on what it does from a technical perspective. This is often called “package by layer”.

The problem, as Martin points out, is that one your software grows in scale and complexity, you will quickly find that having three large buckets of code isn’t sufficient, and you will need to think about modularizing further. As Uncle Bob has already said, a layered architecture doesn’t scream anything about the business domain. Put the code for two layered architectures, from two very different business domains, side by side and they will likely look eerily similar: web, services, and repositories.

Package By Feature

Another option for organizing your code is to adopt a “package by feature” style. This is a vertical slicing, based on related features, domain concepts, or aggregate roots (to use domain-driven design terminology). In the typical implementations that I’ve seen, all of the types are placed into a single Java package, which is named to reflect the concept that is being grouped.

This is very simple refactoring from the “package by layer” style, but the top-level organization of the code now screams something about the business domain. We can now see that this code base has something to do with orders rather than the web, services, and repositories.

Ports and Adapters

The “inside” region contains all of the domain concepts, whereas the “outside” region contains the interactions with the outside world. (e.g., UIs, databases, third-party integrations). The major rule here is that the “outside” depends on the “inside” - never the other way around.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment