Separation of Concerns
Separation of Concerns is one of the most important concepts that a software architect must internalize; it is also one of the most difficult to describe, because it is an abstraction of an abstraction.
Some Example Separations
Let’s start with a slightly less abstract example. In web programming, there is a design principle that says that your web site implementation should be separated into three domains:
- Style and presentation — the visual appearance of the site.
- Business Logic — the way that the site behaves in response to user actions.
- Content — the actual data being presented, such as blog posts or articles.
Following these rules, one would try to avoid having business logic and presentation mixed together, and the same is true for content. If possible, these elements would be contained in different source or data files; if not, then they would be in distinct and easily identified sections of the same file.
There are a number of good reasons for doing this.
The first is that these elements are often changed independently. For example, you may want to present the same content, with the same business logic, but with a different style. Or you may want to present different content, but with the same style and logic. Keeping these elements separate makes it easy to swap out one without affecting the others, especially if the interactions between them are formal and well-defined — just plug and play!
Another reason is that there are often different team roles associated with these different elements. The person in charge of managing the content might not be the same as the person designing the look and feel of the site. Separating the domains allows individuals to work freely without stepping on each other’s toes; it also means that they are not forced to understand parts of the system that don’t relate to their task. Thus, a person whose job it is to craft the style sheets for the site doesn’t need to learn how to maintain a content database as well; people can specialize.
A third reason is organizational: splitting the implementation into well-defined pieces makes it easier to find things. If you are trying to find out why the site is behaving in a particular way, you can limit your search to the business logic portions, you don’t have to waste time looking at the source files dealing with presentation or content. This conforms to the general principle, “make the code easy to reason about”.
While this particular rule of separation (style, logic, content) is fairly standard throughout the web industry, it is not the only possible separation. In fact, I once worked at a startup that did banking software; in that environment, there were additional roles related to domains such as branding and security, each of which had its own set of specialized engineers. Unfortunately, the technology back then was too primitive to separate out all of these elements, which meant that changing one element required teasing out the particulars from the tangle of code.
Also, the separation into these three domains doesn’t have to be the only separation! For example, if your web site has a login page, a profile page, and an account management page, these should also be separated from each other; and each of those pages will have it’s own presentation, logic and content elements separate from the others.
Now, let’s look at a different example, which is a web server. Typically a web server consists of several layers. Here’s one possible arrangement:
- Input layer —responsible for accepting input HTTP requests, validating them for proper authentication and format, and then dispatching them to the correct logic function.
- Logic layer — contains the algorithms which operate on the data in response to user input.
- Data access layer — responsible for reading and writing in-memory representations of records to and from the database, as well as performing complex queries over the data.
Again, the organization is such that it is possible to know the layer in which a given piece of code is located, without having to search all of the layers. An additional advantage is that these layers can be unit-tested in isolation, allowing for a simpler and easier test regimen.
Making your own rules
Both of these scenarios given above are instances of a more general pattern — separation of concerns. In the web site example, “presentation” is a concern, as is “logic” and “content”.
As you might expect, this pattern applies to all kinds of software, not just web pages and servers.
For the previous examples, I provided you a set of rules for organizing the code, based on common practices within the industry. But what if you don’t have a set of rules given to you? Let’s say you are responsible for designing the architecture of a brand new system or framework. You’ll need to come up with some strategy for dividing the problem into pieces. You don’t want the code to end up as a giant “ball of mud” with everything mixed together.
You may be tempted to chop things up arbitrarily — that is, you start writing code and if a function or class gets too large, split it into pieces. While splitting up large chunks is a good habit to have, if you don’t have a “big picture” plan in mind, then the result is likely to be chaos — no one looking at the code will be able to predict where a given feature or function will reside, they will have to do a brute-force search whenever they want to find something.
You need to divide the code into chunks that are meaningful. But how do you decide what is meaningful?
One strategy is to look for natural divisions in the problem space.
Think for a moment about the geographical outlines of countries on a map. What defines the shape of these borders? In some cases, the border is an arbitrary political boundary — a straight line. However, in most cases the border follows a natural terrain boundary such as a river or mountain range. Because these terrain features are barriers, it means that there is stronger and more frequent commercial and cultural intercourse within the boundaries than across them. Thus, the boundaries denote strongly-connected regions, while separating regions that are more weakly-connected.
In software, there are also natural boundaries, and interactions both within and across those boundaries. In general, code inside a boundary will look and operate similarly to other code within the same region; while the code may interact with code outside the boundary, that code will typically have a behavior and structure — a “flavor” — that is distinctly different.
I often think of these divisions as “cleavage planes”, in the sense that they represent the natural place to cut the problem into smaller sub-problems.
It is also best if these bounded regions correspond to high-level concepts in the design space — what we are calling “concerns” — rather than based on incidental details of technical implementation. Thus, in our previous example, there is a natural boundary between “presentation” code and “logic” code — close examination reveals that the flavor and methodology used within these regions are distinctly different, but they also represent different high-level concepts.
Scaling and Scope
How many concerns should you have? How fine-grained should the divisions be?
In general, finer-grained is better, but ideally you’ll want the concerns to be organized hierarchically, with smaller concerns nested inside of larger ones. Again, the goal is to be able to have an organization such that there’s a well-defined place for everything, and that it is easy to locate the material you need to work with.
There’s also a limit to how small you can go: you shouldn’t split up individual concepts so far that they become incoherent. I sometimes think of larger ideas being composed of “conceptual atoms”, in the sense that these smaller ideas are indivisible — trying to split them up further makes them more difficult to understand, instead of easier.
Unfortunately, not all concerns are easily separable. Two examples of this are logging and error handling.
While both logging and error handling are arguably separate concerns, in actual implementations they tend to be tightly intertwined with other code. Every call to an external service or resource should, conceivably, check for errors; the check has to happen right at the point where the call is made, and (with our current programming techniques) it’s not easy to simply extract all of the error handling code into a universal “error-handling module”. Similarly, logging statements are in many cases highly particular to the operation being performed.
These are known as “cross-cutting” concerns because they cut across the boundaries of the other concerns — that is, they affect every part of the code base regardless of what the concern of that code is. The downside of cross-cutting concerns is that they are a distraction from the main intent of the code — often, reading the code for a function that makes several i/o calls, there is so much error handling code that one might be forgiven for accidentally thinking that it was an error handling function!
There have been attempts to design programming languages that allow cross-cutting concerns to be placed in separate code modules. Probably the most well-known was the “Aspect-Oriented Programming” model developed at Xerox PARC. The technology allowed code from different concerns to be automatically “woven” together by the compiler.
Unfortunately, the result was not entirely successful and has not caught on; because the ‘weaving’ only happened at the level of functions, and not individual statements, using it effectively required splitting up functions in an unnatural way, distorting the design of the code. The tool intended to reduce distractions became a distraction itself.
Debugging and Concerns
Being able to effectively separate concerns requires cultivating the mental skill of dividing a problem into pieces, looking for a line or plane of division between two regions. A similar skill is often needed when debugging a difficult software problem.
Let’s say you have a reproducible bug where the system is not functioning correctly, but you don’t know in which part of the code the bug lies. This calls for scientific debugging, meaning that you’ll need to form a hypothesis and then devise an experiment to test it — in other words, use the scientific method.
In your mind, you’ll need to consider the many different ways that you can mentally divide your system into two pieces, such that if the bug is in the first piece, then you should see a certain behavior, whereas if it is not, then you should see a different behavior. Do this successfully, and you’ll have cut the size of your debugging task in half.
Separating concerns is a mental skill that you must develop within yourself. It requires both creativity and rigorous organization. The only way to learn it is through practice and observing examples from others.
Do thus successfully, and you will be well on the path to becoming a great software architect.