Clean Architecture Overview
Luke, 24 Mar 2022
I wanted to give a clear example of what clean architecture really means in practise for a mobile developer in 2022. We're working potentially with SwiftUI, with Combine, async/await, and lots and lots of other technologies. How does it all fit into a clean architecture? Clean architecture has become a go-to in terms of software design philosophy in recent years, for good reason: it has a clear and simple structure that can make software flexible and future proof. Here I just want to go over the core fundamentals of clean architecture, and in another article I'll give an example of how that can work with iOS and SwiftUI.
So heres an overview of the clean architecture philosophy.
- We are splitting the component parts of the application into a hierarchy of layers.
- We use that hierarchy to govern the communication between component parts, to make things testable, reliable and independent.
- This allows the software to be future proof by being sufficiently flexible to future changes.
So first, what are the layers generally?
Entities / Data Layer
So at the core of the hierarchy: the Data layer, containing the Entities. Here you would see a set of Structs or Classes that contain the key elements of the application, for example: Person struct, containing a persons name, age, height, etc. Or a Form struct, containing the contents of a filled in form. Or a CreditCard struct, containing the card number, expiry, owner name, etc. This level does not communicate downwards - ie it doesnt have knowledge of whats below it. Outer layers can have knowledge of inner ones, but not the reverse (according to SOLID principles).
Use Case / Business Rules / Domain Layer
Outside of this are the UseCases or Business Rules, aka Domain layer. These dictate how those previously mentioned data objects are used in combination, to satisy an example use of the application. For example: a user wants to be able to order a credit card. For that to be achieved, some logic must check that the Person is over 18, that they have completed a Form correctly, and that a CreditCard object has been created and has a status of 'ordered'. There will be some logic here, but this standard use case could be repeated across different front end applications, like Web, Mobile App, or even in person in a bank branch. For each scenario, the underlying logic would be the same, which is why its better encapsulated here near the heart of the application, for reuse everywhere. Often the terms Service or Manager are appended to Use Cases.
In terms of communication, its ok for Use Cases to refer to Data layer objects by name, directly. But the reverse is never ok - for Data objects to mention UseCase objects. This is the dependency rule, where upper (or inner) level objects dont trouble themselves with things below their level, so they are safely separate, and changes below their level dont affect them.
At the next level, adapter objects communicate with the above use cases / business rules for consumption by an external agency, such as a mobile or web GUI, or a database. For GUI, this level would contain all elements of a standard application architecture, such as the View and Controllers if using MVC, or the Views and ViewModels in MVVM. A flow of data would be like: a Presenter or Controller from this level would pass a data object to a UseCase object, and an output object on the UseCase could then populate the passed data object with the data that the consumer interface requires, and return it. In reality this exchange would be governed by an interface or interfaces - specifically protocols in Swift. For a database, an adapter here between the database (that actually lives in the infrastructure layer) and the UseCase, would take the data from the database, query a UseCase, get the result, and populate the database with it. The GUI could also query the same database adapter for that result, to display it.
Finally below the adapter layer would lie things like Databases, Web Frameworks and other external items. They are kept to an outward edge so that changes in them dont affect the internal workings of our application. For example a CoreData database might live at this level, but if we decided to swap out this database for another type, it is easier to achieve as it is not mentioned throughout the app, and is safely separated.