Layers, ports & adapters - Part 2, Layers
The first key concept of what I think is a very simple, at the very least "clean" architecture, is the concept of a layer. A layer itself is actually nothing, if you think about it. It's simply determined by how it's used. Let's stay a bit philosophical, before we dive into some concrete architectural advice.
Qualities of layers
A layer in software serves the following (more or less abstract) purposes:
- A layer helps protect what's beneath it. A layer acts like some kind of barrier: data passing it will be checked for basic structural issues before it gets passed along to a deeper layer. It will be transformed or translated to data that can be understood and processed by deeper layers. A layer also determines which data and behavior from a deeper layer will be allowed to be used in higher layers.
- A layer comes with rules for which classes belong to it. If (as a team) you agree on which layers your software will have, you will know for every class you're wandering around with, in which layer to put it.
- A system of layers can be used to modify the build order of a project. You could in fact build layer upon layer if you like. You can start at the outside, working inward, or at the inside, working towards the "world outside".
- Being able to change the build order is an important tool for software architects. With layers you can build a big part of the application without deciding on which framework, ORM, database, messaging system, etc. to use.
- Most legacy software consists of code without layers, which can be characterised as spaghetti code: everything can use or call anything else inside such an application. With a system of layers in place, and good rules for what they mean, and which things belong in which layer, you will have true separation of concerns. If you document the rules, and reinstate them in code reviews, I'm sure you will start producing code that is less likely to end up being considered "legacy code".
- In order to do so, you need to write tests of course. Having a good system of layers in place will certainly make that easier. A different type of test will be suitable for each type of layer. The purpose of each test suddenly becomes more clear. The test suite as a whole will become much more stable, and it will run faster too.
A warning from Twitter:
"The object-oriented version of spaghetti code is, of course, 'lasagna code'. Too many layers." - Roberto Waltman.— Programming Wisdom (@CodeWisdom) March 7, 2016
I've never seen lasagna code to be honest, I did see a lot of spaghetti code. And I've written code that I thought was properly layered, but in hindsight, the layers were not well chosen. In this article I describe a better set of layers, for the biggest part based on how Vaughn Vernon describes them in his book "Implementing Domain-Driven Design" (see the reference below). Please note that layers are not specific to DDD though, although they do make way for a clean domain model, and at least, a proper amount of attention paid to it by the developer.
Directory layout & namespaces
Directly beneath my
src/ directory I have a directory for every Bounded Context that I distinguish in my application. This directory is also the root of the namespace of the classes in each of these contexts.
Inside each Bounded Context directory I add three directories, one for every layer I'd like to distinguish:
I will briefly describe these layers now.
Layer 1 (core): Domain
The domain layer contains classes of any of the familiar DDD types/design patterns:
- Value objects
- Domain events
- Domain services
Domain I create a subdirectory
Model, then within
Model another directory for each of the aggregates that I model. An aggregate directory contains all the things related to that aggregate (value objects, domain events, a repository interface, etc.).
Domain model code is ethereal as I like to call it. It has no touching points with the real world. And if it were not for the tests, no one would call this code yet (it happens in the higher layers). Tests for domain model code can be purely unit tests, as all they do is execute code in memory. There is no need for domain model code to reach out to the world outside (like approaching the file system, making
Truncated by Planet PHP, read more at the original (another 9330 bytes)