Published April 30, 2022
by Doug Klugh
Promote Modularity
Build flexible, highly adaptive software systems by separating all but those parts of a system that are closely related and require direct interaction. All other parts of the system should be designed and built independently, while having little to no knowledge of other parts of the system or the system as a whole. This can be achieved by encapsulating cohesive information and/or logic into isolated layers, made of reusable and independently deployable components, containing cohesively bound classes which serve single actors, that are composed of small, single-purpose functions.
One common method for achieving separation of concerns is partitioning a solution with architectural layers — also known as an n-tier architecture pattern. Each horizontal layer encapsulates a logical set of responsibilities and provides abstract interfaces for those responsibilities. To simplify the solution and help manage dependencies between components, each layer can only communicate with the layer immediately above or below.
While every software system will be designed with their own architecture, figure 1 (below) shows one way to layer a software architecture to provide separation of concerns.
Components are used to encapsulate collections of classes to facilitate portability and independent deployability. They promote Separation of Concerns by grouping classes together that all have the same Single Responsibility and serve the same actor. Therefore, these classes are closed to all but that one responsibility and to the needs of every other actor. This will help minimize the number of components that need to change when requirements change.
Architectural boundaries are used to enforce independence between components — isolating the frequency, cost, and risk associated with software changes. This will minimize the impact of changes that propagate through the system, insulating one part of the system from changes in another. Business rules should not have to be recompiled, rebuilt, and redeployed every time a change is made to the user interface, data persistence, or REST transactions. These low-level implementations should be Plug-ins to the business rules to facilitate independent deployability.
Components can be designed to encapsulate functionality that is difficult to change, but easy to extend. And for components that have high afferent couplings (a lot of incoming dependencies), that is exactly what you want. Those are the components that you want to change infrequently, since so many other components depend upon them.
Components can also be designed to ensure that any entity using the component depends upon all classes within that component. This will prevent dependent components from having to be needlessly retested and redeployed because a class, that it does not use, was changed within that component. This aligns with the Law of Demeter by promoting interactions only with closely related classes and by limiting knowledge of other parts of the system. This also helps to deodorize the smell of Immobility.
Classes that are constructed as a group of cohesive functions, containing proper information hiding constructs, with a single responsibility, servicing a single actor, will go a long way in promoting flexible, loosely-coupled architectures. This will help to ensure that any given class encapsulates at most a single concern while establishing independence between those concerns.
Functions that are well-factored do only one thing, but do it very well. For this reason, they are quite small; usually less than 10 lines of code and often less than 5 lines. If, While, and Try-Catch blocks should rarely contain more than a single function call. Exception handling is a responsibility unto itself and should be encapsulated within its own function. If good Naming Conventions are followed, having small functions will make it much easier to understand, navigate, and maintain your code.
The principle of Tell, Don't Ask is useful for reducing query functions and encapsulating knowledge, while avoiding Anemic Domain Models. Functions that frequently interact with data or functions in other modules should be refactored to encapsulate their functionality with the data they change most often. Functions that query other components can be improved by applying the Common Closure Principle, while functions that query other objects within the same component can be improved with the Single Responsibility Principle.
One of the main components of a user story is the actor to which the story provides value. This ensures that the user story services exactly one actor — one user role. As stories service different actors in different ways, this helps to separate (decouple) the responsibilities between actors. Different actors will have different reasons for changing or extending the same functionality. Identifying the one actor the story services promotes alignment with the Single Responsibility Principle — which ensures that components, classes, and functions have a single responsibility — a single reason to change.
Separation of Concerns can be promoted at the organizational level by building small, independent teams with individual code repositories and dedicated product backlogs. By aligning with Conway's Law, teams can focus on building services with independent concerns, which evolve separately from one another. And through the application of DevOps practices, we can tear down the silos between development and operations to ensure that all team members participating in delivery are aligned to a common concern.
A concern is a part of a software system that defines its purpose through information, logic, responsibilities, dependencies, and functionality. It is NOT a discipline or capability of a team member required to define, build, and deploy a solution, but rather a capability of the system being delivered.