The Clean Architecture is a new-ish architecture pattern promoted by Robert C. Martin, better known as Uncle Bob. Uncle Bob’s name is probably familiar to anyone who has done any software development in the last decades, from his books like Clean Code, to the SOLID principles, both used and referred to by developers all over the world.
This article is an in-depth analysis of the post The Clean Architecture, from an object-oriented developer’s view, with the goal of determining how well the proposed architecture components and building blocks conform to object-oriented principles. As the book “Clean Architecture” is not yet available at the time of writing of this article, the information is augmented by Uncle Bob’s presentation available on YouTube, and the Clean Code Case Study project on GitHub.
As Uncle Bob is very well known for his lectures and books on object-orientation, it stands to reason that his upcoming book “Clean Architecture” will be read by many object-oriented developers, beginners and experienced ones alike.
It may be important to point out therefore, that his proposed architecture pattern does not seem to target the object-oriented development paradigm. While it may be true, that it can be implemented with an object-oriented language, using object-like constructs and syntax, the bulk of the proposal seems to be based on the procedural mindset.
This article’s goal however is not to compare paradigms, or to argue which one deserves our respect the most, but to analyze The Clean Architecture for those developers who already decided to follow object-oriented principles.
Setting the Stage
Uncle Bob’s post starts by listing several related architectural patterns from which The Clean Architecture drew inspiration from, including:
- Hexagonal Architecture by Alistair Cockburn
- Onion Architecture by Jeffrey Palermo
- Data, Communication and Interaction by Trygve Reenskaug and James O. Coplien
Though these architectures all vary somewhat in their details, they are very similar.
It is true that there are several concepts in all of the listed styles that appear very similar, and indeed from a mixed-paradigm programmer’s view they might just vary in details, but there are significant differences from an object-oriented point of view.
The DCI Paper for example, although its title suggests an object-oriented approach, clearly isn’t one. The stated goals of those authors is to create a mixed- (or multi-) paradigm approach which very consciously has procedural elements to counteract perceived shortcomings of a purely object-oriented design (see Chapter “Where did we go wrong?” in the Paper). Regardless whether those shortcomings are real or not, the authors decided to deviate from object-orientation at the core of their proposal.
The same can not be said of the other architectural styles. While there might be interpretations of those styles that lead to a contradiction of object-oriented principles, they do not do this by design.
They all have the same objective, which is the separation of concerns.
Separation of Concerns is a very important concept, but it is one that has very different meanings in different contexts. In the context of Clean Architecture it seems to be referring to the complete separation of different aspects of the logic, for example separating the Presentation, Persistence and possibly other aspects out of the objects that are actually containing the data and the context for all those operations.
This is a very common interpretation for layered designs, also often seen in Java Enterprise software. It is in fact part of the best-practices for enterprise designs, and goes hand-in-hand with multi-tier designs.
It is however dangerous territory for an object-oriented developer. In object-orientation the separation of concerns refers to two foundational attributes of Objects, their inner Cohesion and Coupling with other objects. Simply, but perhaps less precisely stated, these mean that objects should contain every business function that are highly related to other functions and data in the object.
Object-orientation does not discriminate among functions based on purpose. The function to present some object on the screen or present an object as XML is every bit as valid as other functions that “only” modify the object’s state. Therefore all requirements, regardless of technicalities influence object design, and both Persistence and Presentation are part of the business interface of an object, if requested by the specification.
This of course does not mean that details of the Presentation and Persistence should be in business objects, but it does mean that at least some abstract notion of these should be found in the object, and there should be nothing outside the object that knows the object-related details of these function (like which fields are persisted or presented), at least not with the same semantics as in the context of the object.
Nothing actually needs inherent separation, unless dictated by cohesion or coupling, or outside architectural constraints like distribution or other boundaries. Therefore this goal of separation of certain aspects out of the object is very dangerous for, and perhaps irreconcilable with object-oriented design.
They all achieve this separation by dividing the software into layers.
Layers are good way to separate objects which naturally reside on different abstraction levels. For example one could easily imagine a Network Stack to be composed of different groups of objects mirroring the structure of the ISO/OSI Layers. There would probably be objects for the Network Layer which offer routing capabilities, and there would probably be objects responsible for the Transport Layer, implementing the TCP and UDP protocols. The Transport Layer objects would have access to the Network Layer objects, but not the other way around, hence they would form a layered structure.
This interpretation of layering would be a very effective way to decompose the problem of implementing the network stack. It is however not what is proposed above. Clean Architecture proposes to separate different cohesive aspects of the same object into multiple different pieces, which does not seem to be compatible with basic principles of Object-Oriented Design.
Goals of the Architecture
Each of these architectures produce systems that are:
1. Independent of Frameworks. The architecture does not depend on the existence of some library of feature laden software. […]
If we consider the context in which this statement is made, it seems to suggest that the “business objects” need to be completely independent of frameworks and external libraries. No mathematical library, no “commons” library, no messaging library, no CDI, no EJB, and certainly no presentation or database libraries.
Arguably all of these decisions should be made in light of functional and non-functional requirements, instead of categorically forbidding all of them without any clear and present benefit.
There is however a good point made, that the architecture itself should not depend on a library (or framework). It might be helpful to slightly differentiate what a library and what framework is.
A library is merely a collection of objects (classes) that have some functionality. We depend on libraries because we want to re-use some functionality, like logging, I/O, network communication, etc. A framework also offers functionality, but at the same time imposes some form of design control. It offers its functionality only if some or all parts of the code that uses it conform to some design restrictions, or is implemented in a given paradigm or pattern.
Having a dependency on some framework at the core of the application usually comes with a lot of drawbacks, and the above quote is right to say, that the “architecture” itself often becomes dependent on the framework. This may have a catastrophic impact on maintainability later in the software’s life, so frameworks should be avoided if at all possible.
The quote however goes further, implying that all dependencies should be avoided, even dependencies to a plain library. We already depend on classes from the JRE, probably Log4j, possibly others. If “architecture” itself does not really depend on these things, merely the code (the implementation) does, then this is not really a concern.
Complete independence from all dependencies, as suggested by the above point, may be neither achievable nor desirable.
2. Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.
Testability is an important and desirable aspect, and it should be possible without additional objects or external systems. To be able to write unit tests for a class is actually a good indicator for a good design.
However, this rule should not only include “business rules”. Unit tests should also cover the UI and Persistence too, in exactly the same way, without external elements. The concept of testability should not be used to suggest that the UI or Persistence are inherently less important, or get in the way of testing other aspects of the object.
3. Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
4. Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
5. Independent of any external agency. In fact your business rules simply don’t know anything at all about the outside world.
It is very much debatable whether switching a web-application to a console-based application, or switching a relational database of an application to a NoSQL one are common use-cases in software development. It does happen of course, but it is debatable whether a significant enough portion of a software developer’s work is dedicated to these tasks. Probably much more work is done fixing bugs and implementing new features which do not require a full technology switch.
This complete independence of UI, Database and indeed the whole outside world would therefore be desirable only if this independence would come at no, or very little cost. This is however not the case, as additional “layers” are introduced, with additional boundaries which introduce much unnecessarily complexity into the application. The Keep It Simple principle should apply, and this decision should not be made just for the potential to be a little more flexible for a mostly theoretical use-case.
Other than the above arguably rare use-case, objects should be well aware of the whole spectrum of business functions other objects can ask them to perform. This range of business functions should include at least some abstractions of persistence and presentation if the system is expected to conform to object-oriented principles.
Externalizing the UI and Persistence functions completely also means, that any changes in the “business objects” or “business rules”, even ones which otherwise would be internal to those objects, would ripple through the whole application, or even multiple applications if we are re-using objects.
According to object-orientation, changes to details, like adding or removing private fields in an object should not cause any external changes. This is a very important property which strongly contributes to the maintainability of code, and is arguably impossible to achieve, if parts of the functionality of the object are external to the object itself.
Entities encapsulate Enterprise wide business rules.
What does “Enterprise wide business rule” really mean? Hardly any rule is “enterprise wide”, in the sense that there is some general rule to everything. To sell an upgrade only with a certain product for example is not an “enterprise wide” rule, it is only applicable directly to the sale of the upgrade (whatever that may be), and it probably gets created by the relevant organizational unit (for example Marketing), not involving “the whole enterprise”.
“Enterprise wide business rules” might mean that the Entities should be a semantically unified view on something. Like a Product or a Customer, that has meaning to almost everybody in the enterprise. The problem with this approach is, that organizational units and their applications might have a completely different or even contradicting meaning of these things. The bigger an Enterprise, the more unlikely that a unified (“enterprise wide”) model of some aspect can be found.
Domain-Driven Design for example argues that as software (the scope of a problem) gets bigger, finding a unified model is just not worth the effort even if it would be possible. It defines the notion of a Bounded Context, where objects have a specific limited meaning in a very specific context, which can differ or even contradict meanings of the same term in a different context. For example a Product according to Marketing might be something which can be further customized with options like color, or other add-ons, while for Accounting the term “Product” would mean something that has an SKU, which contains all options and add-ons already. The same thing might mean different things to different people or organizational units.
Even if a unified model was possible and it would be re-used across application boundaries, it would become a global dependency for all projects “enterprise wide”. That would cause organizational and operational problems, like: what team would be responsible for it, how to avoid this team becoming the bottleneck for change, and how would an enterprise wide coordinated upgrade even look like when it needs to change, etc.
An entity can be an object with methods, or it can be a set of data structures and functions.
Obviously an object-oriented developer would be limited to the first option, as pure data structures and functions are parts of different paradigms.
It doesn’t matter so long as the entities could be used by many different applications in the enterprise.
This seems to reinforce the idea, that “enterprise wide” above meant some unified model for certain things. Even if this would be possible, it’s doubtful that this property is advantageous for organizational, operational and maintainability reasons stated above.
They are the least likely to change when something external changes.
This only considers one direction of change, and does not address the cost of changing the entities themselves. It implies, that these “core” entities would be stable enough that their cost of change does not matter.
Considering that these entities would have to be unified “enterprise wide” views of things, potentially including multiple different semantics from multiple organizational units and projects, it seems doubtful that they would be stable, unless they were very abstract to the point of having no real meaning at all.
[The use cases layer] encapsulates and implements all of the use cases of the system.
One of the key tools developers have to tackle large problems in computer science is called decomposition. It just means, that we humans are too limited to work with big models and complicated algorithms, so we have to split problems up into smaller pieces. This can be done in multiple ways.
The simplest way to split up a problem, is to try to list the steps necessary to complete the task. Let’s take for example the steps to implement a cash transfer from an account:
- Check that the account number exists
- Get the account type
- Determine transfer limit for given account type
- Continue only if given amount is not greater than limit
- Check that the account has enough money
- Subtract the value from the account
- Create the transfer for the amount and send it to clearing
This is very simplified of course, but demonstrates how the larger problem of making the transfer is now split into 7 smaller, easier steps, which can then be further divided as needed until it becomes easy enough to directly code.
This is what is commonly known as procedural design or procedural decomposition. It is procedural, because we are concerned about what steps would need to happen, during which we acquire and modify data (elsewhere) as needed.
The whole thing (“cash transfer”) is called a “Use Case”. Since the “data” part resides in the layer below (“Entities”), the proposed Use Case layer would probably contain the steps necessary to perform it, similar to the above.
Having the individual steps listed sequentially sounds intuitive at first. Indeed, this is exactly what the DCI paper also proposes, and criticizes object-orientation for not doing.
The problem with this approach is, that it does not seem to scale. As long as it contains a limited set of distinct use-cases, the approach works, but as soon as overlapping functions are introduced, it starts to break down.
For example let’s introduce an internal cash transfer Use Case, where the steps are almost the same, but the transfer does not go to clearing, instead it goes directly to the other account internally. The problem now is, that these two share steps. Both have to check whether the money is there, both have to check the limits, etc.
This makes the whole thing error prone an unnecessarily complex. We already solved the checks, but we have to think about them again for this new case. If we forget it, we have a problem, and if not, we increased our cognitive load even if we just have to copy or re-use it. Wouldn’t it be nice, if we could just forget about it once we solved it, and it would get automatically applied when subtracting money from the account?
Well, this is exactly what object-orientation does. Object-orientation proposes a different kind of decomposition, where we don’t really think about the “steps” that need to be taken, but the “things” that need to be involved. In this case “Account”, “Transfer”, “Money”, etc. All these things may have their own rules and behavior, therefore there is no need to think about whether a “step” may mess up an account, or transfer more money than the account has, since the Account object would not let these things happen. In effect we can’t subtract money from the account directly, we have to ask the Account to subtract it for us, thereby also making all necessary checks automatically. This approach however relies on the fact that the “data” is hidden behind operations that already has “parts” of the “Use Case” implemented. Indeed, if all objects are designed correctly, there should actually be nothing left of the Use Case to be implemented separately.
Considering the differences between these two approaches, it seems the proposed Use case Layer might be better suited for a procedural approach, and might be entirely incompatible with an object-oriented one.
These use cases orchestrate the flow of data to and from the entities, and direct those entities to use their enterprise wide business rules to achieve the goals of the use case.
The first half of the sentence seems to reinforce the procedural nature of the design, which would indeed include moving data to and from the entities as discussed above.
The second half implies that the entities may still have some form of proper behavior in the object-orientation sense, not just data, this however can not be seen or confirmed in the case study project or the presentation.
The software in this 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…
In an object-oriented approach it is difficult to imagine how such a conversion can happen outside an object, indeed in a completely different layer. Conversion implies access to data, which implies missing encapsulation, one of the key ingredients of object-orientation.
The models [in this layer] are likely just data structures that are passed from the controllers to the use cases…
Since pure data structures are unwelcome in object-orientation, this layer would have to be redesigned to be suitable for an object-oriented software solution. It is not discussed in the Article whether such an alternative design more in line with object-orientation is possible or not.
Frameworks and Drivers
The Web is a detail. The database is a detail.
While it is true, that details of the Web, like whether to use HTML4 or HTML5, and details of the Database like what kind partitioning or indexing is to be used, should be real details and of no concern to business functions, it is highly debatable whether business objects should have absolutely no knowledge of these things.
Arguably the architecture should not be driven by arbitrary notions of cleanliness, but by real requirements and constraints. So the question is: is it really likely that the application will become a non-web-application? If not, why couldn’t an object know that it will be presented on the Web?
What data crosses the boundaries
Typically the data that crosses the boundaries is simple data structures. You can use basic structs or simple Data Transfer objects if you like.
An object-oriented project would probably have to use an alternative approach here, since these things are incompatible with object-orientation.
Or the data can simply be arguments in function calls. Or you can pack it into a hashmap, or construct it into an object.
In the original description of object-orientation, objects are supposed to exchange messages. Objects ask for help to fulfill their own tasks. They ask other objects to do something in their area of responsibility that contributes to the task at hand. Although it is not forbidden, exchanging a lot of data is frowned upon. It usually indicates a bad relationship between objects, where the responsibilities might not be clear enough.
Instead of exchanging data, objects are supposed to exchange other objects for example. These too will have a defined behavior that can be used to fulfill some specific tasks. Whether these parameter or return objects contain a lot of data, or none is usually of no concern to the receiving party.
Therefore, while the above quote could be technically understood to be compatible with object-orientation, the approach itself suggests otherwise.
This analysis revealed fundamental incompatibilities between the proposed “Clean Architecture” and object-orientation in almost all aspects described in the analyzed Post, including:
- The precursor design ideas, like DCI
- The goals of the architecture
- The Entity layer
- The Use-Case layer
- The Interface Adapters
- The Boundaries between layers
Although there are always shades of gray when designing an application or application landscape and there is always room for compromise, it is difficult to image a set of requirements or constraints that would make an object-oriented design similar to what is proposed by Clean Architecture.
This is probably intentional and not an oversight. As Uncle Bob said in his book “Clean Code”:
Good software developers understand these issues without prejudice and choose the approach that is best for the job at hand.
Quite clearly this architecture pattern uses and targets the procedural paradigm, as evidenced by the separation of data and function in almost all building blocks of the architecture.
Furthermore this analysis uncovered some questionable design decisions, which even in non-object-oriented paradigms would probably lead to unwanted consequences. These include:
- Assuming that changing whole technology stacks (like presentation and persistence) is a significant use-case, worth building the whole application around
- Assuming a unified enterprise-wide model is possible and feasible
- Assuming said unified enterprise-wide model will be stable
- Assuming re-using the same business code in multiple applications is a good thing
These choices influence the whole architecture and it is highly debatable whether these apply to any significant number applications.
The conclusion is, that object-oriented developers should probably avoid the “Clean Architecture” altogether, and even developers of other paradigms should check the fundamental assumptions of the architecture in detail, whether it applies to the application that needs to be build, before applying it in any form.