Back to basics - Code architecture
The structure of your code, the choice that could make or break your application before you've written a single character. Also known as code architecture. Here's an attempt at what we need it for, what it actually is and then an example on how you could set it up.
What do we want code architecture for?
What we want, of course, is 'good software'. But that's rather badly defined and not very
Proper definitions like ISO 25010 tell us that there are a lot of properties
The properties are: Functional suitability, Performance efficiency, Compatibility, Usability, Reliability, Security, Maintainability and Portability.
See: https://iso25000.com/en/iso-25000-standards/iso-25010 that your architecture will have to deal with. Properties that you can't all satisfy perfectly. So you'll have to pick and choose which you want to be good, and which you can leave as good enough. For your typical enterprise application (which is what I have in mind for this blog) we want your code to be easy to change, while being fast enough. Note that this might not fit as well for things like a grep alternative or a fast paced game, so you'll have to make different trade-offs there.
What is code architecture?
In order to achieve all this we have only one tool available: walls. Walls and their gates. Also known as 'layers' or 'splitting things up'. It's determining what goes where. Walls allow you to separate bits of code from each other, so that they can't interfere as much anymore with one another.
When classes do need to communicate across a wall, they need to go through its gate, which
is what decides the rules on how to do that. It's these rules that make architectures
unique. For example, if you only allow classes on different sides of a wall to communicate
using events, you get event-driven architecture.
Though practical people often allow read-only requests as well.
Or if you say that a request can either return a value or have a side effect, but not both,
you get CQRS.
In this blog we'll only consider two rules: communication through a gate can only be one directional For example the blue classes in the figure on the right have a reference to the red and yellow classes. But the red and yellow classes do not know of the existence of the blue classes. or there can't be any communication at all. For example the red and yellow classes both do not know of each other's existence. These are very simple rules and for most applications that's all you need. After all, unless you really need complexity, simpler is better.
Placing walls is a good thing, because it ensures that you can change one part of your code without having to worry about the other parts. It also makes your code easier to read, as you don't have to understand everything all at once. There are negative consequences too though. That's because if two bits of code can't interact anymore, then things cannot be optimized between them. So while we want to place walls to reach all those nice properties like maintainability, portability etc., we do want to do that by placing as few walls as possible.
As a rule of thumb: when things are related to one another, they should not have any walls between them. If they aren't, they should have a wall between them.
The approach of Domain-driven design
As an example we'll design a web API Though these principles apply to anything we may want to make. by borrowing ideas from Domain-driven design. We'll first look at the outer layers of the application. These contain all the 'boilerplare logic': Unwrapping our HTTP requests, getting our data (to and) from our database, Yes, the database is an unimportant part. Your domain logic is the important bit. stuff like that.
We'll put all the HTTP stuff behind a wall and call it the controller layer. If we need
things like message queues and events, we'll chuck these in that layer as well. We'll put
all the database stuff behind a wall and shove them in repositories. You could also see repositories as 'aggregate factories' - more on aggregates later.
If necessary we'll have application services to tie them together. In practice this one can often be skipped. A controller hardly does anything anyway, so it can pass things around by itself just fine.
Now we have our request and all necessary data, and we can enter our domain layer. Inside which we'll do all validation, decisions and computations. The result will come back in the application layer again and we'll apply the database changes, queue the events and send out our HTTP response.
Now let's look further inside where we find the two domain layers. This is where all the domain logic happens (and nothing else). So no sneaking in quick queries to fetch some extra data anymore you rogue. Functional programming fans might note that this means the domain layer is pure. As it turns out, what is good object-oriented architecture works for functional applications as well.
The innermost layer is where we'll find the domain model. Most of the logic rests here. It mainly consists of two kinds of objects: objects with an identity (called entities) and objects that are defined by their values (called value objects). Value objects usually immutable and usually belong to an entity. Entities are usually the important objects that do all the logic, while value objects tend to be used to chuck some logic in to not clutter the entity too much. Groups of entities and value objects that work together (called aggregates) are walled off pretty strictly. While all objects inside an aggregate can communicate as they like, they are not allowed to know about objects outside the aggregate, and all communication from the outside has to go through a single entity (called the aggregate root). This aggregate is considered to be a unit, and is responsible to always keep itself consistent and valid.
Sometimes several aggregates need to work together, despite the walls prohibiting them from talking to each other directly. Remember, aggregates don't know of the existence of any objects outside their own wall. That includes each other. This is why we have domain services. Domain services are stateless classes, one layer above the domain model, that are responsible for directing the communication to the aggregates and to keep constraints across multiple aggregates valid.
To wrap up we've seen that code architecture consists of limiting which classes can communicate with one other using what I call walls. Placing walls in strategic places helps your code stay organised.
In my experience setting up an application like this is well worth it. You do not, of course, have to place your walls the same way I do here in every application. In fact, sometimes you really shouldn't. But if you want to create an application that's capable of growth while still staying flexible, you've got to think about which bits go where and place your walls accordingly.
- Domain-driven design - A draughts example app; illustrate the theory from this blogpost by creating a draughts webapp.