Separation of concerns (SoC)

Separation of concerns: don’t write your program as one solid block, instead, break up the code into chunks that are finalized tiny pieces of the system each able to complete a simple distinct job.

SoC for programming functions

If we take the lowest level (the actual programming code), SoC instructs us to avoid writing long complex functions. When the function starts to bloat up in sizearrow-up-right, this is the red flag that the method is possibly taking care of too many things at once.

In such a case SoC pushes us to refactorarrow-up-right it, turning into a more laconic and descriptive revision. During this process, parts of the original algorithm get exported and encapsulated in separate smaller functions with a private access level. We gain the code clarity, and chunks of the algorithm eventually become reusable by other parts, even if we initially didn’t expect this to happen.

SoC for modules

At a bit higher level, this principle tells us to group the functions under self-contained modules, each responsible for the fulfillment of a single set of tasks that have a clear logical correlation.

The process very much resembles what we had to do for functions: estrange less-closely related functionality and group up the features serving the same distinct purpose.

Cohesion and Coupling

The application of the Separation of Concerns involves two processes: reduction of couplingarrow-up-right and increasing cohesionarrow-up-right.

Cohesion is the measure of similarity by the set of duties, level of details, and locality. For example, functions drawCircle and drawTriangle are cohesive enough to belong to the same module responsible for drawing, and it feels natural to put these two functions close to each other in the code (high similarity ~ high cohesion).

Coupling, on the other hand, is the measure of dependence of the part on the rest of the system (low dependence ~ loose coupling).

The aforementioned drawCircle and drawTriangle can be used by another function drawCybertruck. We can be tempted to put this function in the drawing module as well, but drawCyberthuck may be dependant on the physics engine and the external state. So this will make the whole drawing module much less reusable and closely coupled with a few other components.

You can tell that primitive drawing functions and drawCyberthuck belong to different levels of abstraction and logic complexity, thus they need to reside in different modules.

And if at some point we decide to use the drawing module in another project - there will be no dependency on the physics engine, so we’ll be able to extract it easier.

A quick way to remember which attribute should be increased or decreased:

  • Decoupling is good - so we need to aim for a loose coupling

  • Cohesive code is good - we need to aim for a high cohesion

Benefits of the Loose Coupling and High Cohesion

Adherence to the principle of Separation of Concerns helps to improve numerous characteristics of the codebase:

  1. Better code clarity. It is much easier to understand what is going on in the program when each module has a concise and clear API with a logically scoped set of methods.

  2. Better code reusability (DRY principlearrow-up-right). The main benefit of reusing the code is reduced maintenance costs. Whenever you need to extend the functionality or fix a bug - it’s much less painful to do when you’re certain it appears in one place only.

  3. Better testability. Independent modules with properly scoped functionality and isolation from the rest of the app are a breeze to test. You don’t need to set up the entire environment to see how your module works - it is sufficient to replace neighboring real modules with dummy mocks or fake data sources. This way you can test the module as the black box by verifying just the output, or as the white box by also seeing which methods are being called on the connected modules (BDDarrow-up-right).

  4. Faster project evolution. Whether it’s a new feature or an update of the existing one, isolation of the modules helps with scoping out the areas of the program that may be affected by the change, thus speeding up the development.

  5. It is easier to organize simultaneous development by multiple engineers. They just need to agree on which module they are working on to make sure they don’t interfere with each other. Only the update of a module’s API can be a flag for explicit notifying other developers, while most of the changes can be added without immediate attention from the other contributors. When coupled with good test coverage, the parallel development becomes as efficient as the cumulative productivity of each individual engineer working solely (it is usually slowerarrow-up-right).

SoC for the system’s design

for system design, we can implement Layered Architecture.

That’s how we get to segregating the modules into layersarrow-up-right. This is not a concrete architectural pattern, but rather a high-level specification for that strategy I was talking about.

The modules get grouped in layers, the same way we’d form a module from the set of distinct functions.

The resulting set of modules within one layer has high cohesion based on the similar duties in the system and the same level of abstraction, while communication and environment awareness between the layers is very much restricted to achieve loose coupling.

We’re not only constraining the communication - the layers with higher environment specifics at the bottom (Repositories, such as a database wrapper or a networking service) are forbidden to directly refer to anything defined in the higher layers (business logic or UI).

So if we take just the networking service that talks to the backend, it should know nothing about the rest of the system and only provide the API for sending the requests.

The business logic layer will be aware of and using that Repository, but it should have no idea if any UI is attached to the system.

The UI layer is aware of the business logic modules and uses their APIs to read the up-to-date data and trigger actions, but at the same time, it knows nothing about the Repository, as the business logic hides the factual underlying infrastructure from it.

This way we can guarantee intrinsic testability of the whole system, where each layer either doesn’t even know the other exists or is decoupled to such a high degree that can easily be surrounded by mocks in tests.

Last updated