Developers, as any other human, are limited by the amount of complexity that we are able to understand and handle. For that reason, we group code using mental models like, architectural layers, components and systems. That’s the very known divide and conquer approach.
“The architecture of a system is defined by a set of software components and the boundaries that separate them.” — Robert C Martin
When it comes to separating the code, the objective is always to get code segregated in layers/components maximizing cohesion and minimizing coupling. This brings the benefits of creating reusable, modifiable and understandable systems. That’s what actually makes the software, SOFT, easy to change and maintain. This ability to separate code, by boundaries, hence it’s very important, but yet, I have found it very hard to enforce.
The natural boundary for separating the code is by deploying it separately. In Swift this would imply moving the source code to separate packages in the form of frameworks or static libraries. This however, is a very expensive form of boundary and one that doesn’t scale well. I mean, you can’t go crazy creating packages for each layer of your architecture, then a package for each feature, a package for a set of related features and so on. That boundary doesn’t come free, it comes at the expense of bringing complexity, increasing the launch time of your app etc. So it must be used carefully, balancing the benefits over the trade-offs. This mechanism is not perfect either, when you depend on a framework so you do on its transitive dependencies, so nothing prevents a developer to bypass the framework and use its transitive dependencies directly. At least you use umbrella frameworks which are discouraged and not supported on iOS.
That being said, the following question rises up: Are we condemned to choose between monolithic applications which maximizes develop-ability at the expense of reusability, versus, a package everywhere approach which maximizes separation of concerns and reusability at the expense of complexity and develop-ability?. There must exist a solution in the middle of those 2 opposite forces, Right?. From my experience, I have seen most of the time that solution in the middle is us, the developers, which we all hope share the same set of principles and we should align ourselves to translate those boundaries from theory somehow to the code. That way we are able to defer the modularization until it is justified, making sure that when the right moment comes we will be able to extract those separate modules. Unfortunately, that’s not always the case, good intentions are important but the code usually tells a different story.
“Best design intentions can be destroyed in a flash if you don’t consider the intricacies of implementation strategy” — Simon Brown
We all have seen projects getting out of control, starting small, then growing very fast into a very big monolithic and unmanageable projects, where you can see violations of the architecture all over the place. Or in some other less catastrophic scenario, you need a feature from one app, to be reused in other app. When that happens, the obvious solution is: let’s extract separated and reusable packages!. A frustrating moment comes when managers discover the estimation of such an effort isn’t some few points, but rather some, or many, sprints. But why? it’s always the question. Isn’t it just a matter of moving some files from here to there?. It should be that way, right?. What exactly prevents this from being an easy operation? The answer to those questions is simple: Source Code Dependencies, or lack of well defined source-level boundaries. As many things in life, simple to explain hard to get it done well.
“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. The trick to creating an appropriate boundary crossing is to manage the source code dependencies.” — Clean Architecture Book
So with or without using packages we need source-level boundaries that make possible these separations, and the key for this is enforcing it as much as possible at compile time.
The principle for creating boundaries is simple, see diagram above. It implies the use of dependency injection and program to protocols (or some other partial boundary like a facade). Such that, the objects from one side of the boundary can be initialized with the implementations that live from the other side of the boundary. For communicating the other way around, use dependency inversion, so you don’t introduce a dependency cycle. To summarize:
- Use dependency injection. Because you need to identify the class dependencies in order to manage them. (This also results in a more testable design)
- Program to protocols not to implementations. (or some other partial boundary like a facade)
- Handle the object graph construction so your class dependencies point in the same direction as the dependencies of the components / layers.
The points 1 and 2 are really simple and shouldn’t be a problem. The point 3, for me is the cornerstone for enforcing the boundaries, if you are able to handle gracefully the construction of the object graph, enforcing those boundaries directly during initialization, you should be able to extract a package out of the code without issues, anytime you want.
Usually for managing dependencies, many developers rely on a dependency injection framework to make this easier. However, those frameworks aren’t aware of the boundaries of your architecture at all, they simply resolve the dependency wherever you ask for it. They don’t impose any compile time verification, but the oposite, the dependencies are resolved at runtime. This goes without saying those frameworks in swift aren’t very capable given that the reflexion API on swift is very limited.
What we really need is Dependency Injection with Boundary Crossing Rules enforced at compile time. That way when a new object is constructed, its dependencies must comply with rules of the enclosing boundary.
Let’s rethink the boundary as a polymorphic Interface for a moment. Let’s say the Boundary is a structure enclosing objects inside. Suppose we have 3 boundaries A, B and C (see diagram above). For this example it doesn’t mind if the boundaries are separating components or layers. In order for the ObjectA to be constructed with a dependency to ObjectB, it needs to cross the Boundary A first. If we impose dependencies between boundaries, for instance, let’s say Boundary A depends on Boundary B, then we could allow the ObjectA to be constructed with ObjectB.
On the other hand if Boundary A and Boundary C are not related, i.e Boundary A doesn’t depend on Boundary C, Then ObjectA shouldn’t be allowed to depend on any of the objects inside Boundary C. (red doted line in the diagram). We can control this, if we handle the object graph construction. That way, high level boundary crossing rules would translate directly to dependency rules for classes inside the boundaries.
My point is that in the same way we have the Boundary definition between 2 objects in code, we also need an equivalent definition at higher level, at the layer and component level. So far, based on the software literature, a component-to-component and a layer-to-layer boundary is the collection of the individual object-to-object boundaries between those component and layers. Of course those individual boundaries are not created arbitrarily but following an architectural design between layers and components.
I really think we need a Top Down approach to this problem. We need a definition in code of how a boundary between layer/components looks like, that as a result, drives those object-to-object boundaries to comply the high level design. That definition needs to be flexible, as you would start by defining some common sense boundaries, like layer boundaries or feature boundaries, but as code evolve you would need to rearrange those boundaries.
Failing to concrete those high level boundaries in code, results in the situation described at the beginning, hoping that the architectural style will emerge in code magically. But only takes one of the low level boundaries failing to be respected / defined, and then, there won’t be a high level boundary anymore. Then if you try to deploy that component/layer in a separate package, you won’t be able to do it until you fix every violation. That’s when the architecture and the implementation divorce from each other.
“Architecture is a hypothesis that needs to be proven by implementation and measurement” –Tom Gilb
I think this approach looks promising, at least conceptually. As a mathematician I am influenced that every concept needs a formal definition, in some formal language. As an iOS developer, that language for me is Swift. That’s why I have created a framework that provides mechanisms for modeling those boundaries in code, helping the developer to manage the object graph construction as I explained. In my experience I have seen this enforces those low level boundaries to respect the high level boundaries, and ultimately the architecture. I also refactored an old app, I created some years ago using this framework.
Framework and Sample Code Links:
Boundaries framework — Source-Level Boundaries Modeling Framework for Swift.
Appstore Example App — A clean architecture iOS Example App.