April 17, 2015 // By Caleb McElrath
I go on kicks. As a sporadically enthusiastic person, I have many kinds of kicks. As a software developer, I have various types of software development kicks. These have included security implementations, parallel programming, and component creation. At the moment, my kick is architecture. In particular, employing the SOLID principles to software design. You may have heard of Liskov, dependency inversion and the other principles that make SOLID a hard act to follow. Most examples of these principles include small snippets of a system and leave it at that. My question is, why stop there? Why should we not employ these principles as appropriate to the entire system as a whole? The possibility to reap the benefits of such principles by employing them on a system from design time is why I say there is no reason not to do so.
How can the SOLID principles be applied to the entire system? It starts from system boundary interfaces and works its way to the details. Before examining system boundary interfaces let’s take a look at a typical layered architecture.
The figure above depicts a simple layered diagram of a system. There's the application, business, data access and storage layers representing the logical separation of a system. This approach is commonly accepted, performs its duty, and helps separate the basic concerns of the system.
How do the SOLID principles fit? The answer is frightening: they don't. As it stands now, the layers are highly coupled by mere necessity, they are not easily replaced with alternative implementations, and there's a high impact to the system if a change is made to a layer. Beyond this, the actual responsibilities of each layer are easily obscured during implementation. This common approach is in conflict with common system design principles -a paradox that can be resolved by properly employing system boundary interfaces.
What is a System Boundary Interface?
System boundary interfaces rely on definitive integration points. This suggests each layer is associated with a point of integration that resides between its self and the layer below it. The problem with typical layered architectures is that this integration point is not clearly defined. This encourages development malpractice and otherwise defiance of SOLID principles. This cannot be avoided because the typical approach inevitably requires direct communication between the layers. System boundary interfaces are distinct integration-point abstractions defining the communication between the layers of a system. The figure below shows an updated layered diagram with system boundary interfaces ( This is the last depiction of the Storage layer due to the typical abstractions found using frameworks such as ADO.Net and Entity Framework). The layers are explicitly separated by the boundary interface of the layer it depends on. This brings the design closer to alignment with the SOLID principles.
With proper use of boundary interfaces, each high-level layer does not depend on concrete implementations of subsequent layers but rather depend only on the abstraction of the communication between the layers. This is almost taken precisely from the mouth of Robert C. Martin when he describes the Dependency Inversion Principle. Employing the dependency inversion principle with boundary interfaces leads directly to high cohesion and low coupling (the ultimate goal of this principle). Using the business layer as an example, the communication with the data access layer is accomplished through the data access boundary interface. The business layer will only know of the communication interface and will not need to worry about how that interface is implemented. This means the data access layer could be accessing SQL Server, Oracle, SQLite or even flat files with no impact to the business layer. Proper use of common IOC containers such as Autofac can allow the use of special adapters for each data access scenario as necessary while maintaining very little impact to the business layer. This is made possible using system boundary interfaces.
The Interface Segregation Principle states that “clients should not be forced to depend upon interfaces that they do not use”. It could be said that each layer in a system is being used which renders this principle irrelevant. While it may be a fact that each layer is used, is it so unforeseeable to have a system that will need only a subset of a layer's functionality? Beyond that, should each individual component within a system really have access to all the other components' functionality? Let's use a resource planning system as an example. This system would include functional components such as human resources, recruiting, marketing, and sales. It would be absurd to have each of these components within the system require the ability to perform the functionality of each other. While they all may be related, their individual functions are very different.
To apply the interface segregation principle to the use of system boundary interfaces, a single boundary interface can consist of many specialized interfaces. Data access services for human resources, recruiting, marketing, and sales would each have their boundary interface defined. Likewise for the business layer as necessary. This makes it possible to implement a specific interface independent of the other interfaces that make up the boundary abstraction. For example, without using the interface segregation principle exposing sales data as a WCF service would mean updating and re-compiling each system that depends on the data access boundary interface -even if they do not necessarily depend on sales data.
Boundary interfaces abstract layer communication and places the responsibility of layer communication to implementations of the boundary interfaces. This means that updates would be isolated to the boundary interface implementation minimizing the total impact of the change. Appropriately applying the interface segregation principle would further minimize the impact by reducing the coverage from the entire boundary abstraction to only the relevant interface that helps make up the abstraction. In our example, this means only the communication with sales data will need to be updated and there will be no impact on systems that do not depend on sales data communication.
Precisely how to segregate the interfaces that make up a communication abstraction is an important consideration. The resource planning example above was separated by its distinct functional components. This is called functional decomposition. The problem with this type of decomposition is that it is essentially a guaranteed axis of change in the system. This will be discussed more in a later section but, what is the alternative? Juval Lowy answers this question in his excellent talk Zen of Architecture where he describes decomposition by volatility. What is most likely to change within the communication abstraction? This is where the separation should occur.
Liskov Substitution Principle
One of the benefits of using system boundary interfaces is that as long as there is an appropriate adapter implementation, any low-level layer implementation can be used. The high-level layer doesn't care what the low-level layer is doing because it is only dependent on the boundary interface. This is an example of supporting the Liskov Substitution Principle. The high-level layer depends on the abstraction of layer communication. The boundary interfaces defines the communication abstraction while the adapter defines the actual implementation using whatever the low-level layer provides. This means as long as the low-level adapters derive from the boundary interfaces, the high-level layer will be able to use them. This capability is usually reserved for internal components of a layer but, by using system boundary interfaces, the system has this capability at the layer level. System boundary interfaces innately allow this sort of flexibility.
The figure above depicts the transition from LINQ-to-SQL to Entity Framework. What this is showing is that there is no direct impact to the business layer due to the boundary interface for data access. The new implementation of the interface will use Entity Framework instead of LINQ-to-SQL. By employing dependency inversion techniques, the old implementation can simply be swapped out in the business layer. Without the use of system boundary interfaces, this change would mean weeks or even months of rewriting the business layer.
The Open-Closed Principle states that software components should be open for extension but closed for modification. This is key in maintaining the capabilities previously mentioned regarding the Liskov substitution principle. If a boundary interface changes, it cannot support re-use and even more so be relied upon to adhere to SOLID principles. Each change would further diminish the system's adherence and provide an unappreciative notion of a rigid system no matter its true existence. To overcome this deviance the system must remain deterministic and extensible.
A system design that is resilient to change and maintains its deterministic nature is a responsible design. So how is such a system designed? It starts by being closed to modifications. System boundary interfaces that satisfy the open-closed principle do not directly change. High-level layers dependent on such boundary interfaces are capable of using any implementation of that interface. They can truly satisfy the Liskov substitution principle. Each boundary interface implementation can be replaced by another while maintaining the communication contract defined by the boundary interface. Changing the interface would mean invalidating the contract previously defined and thus invalidating existing layer communication.
A system design that is extensible helps to maintain the validity of a system throughout its life cycle. Each extension could be defined as a new communication adaptation or an additional interface that helps define the communication abstraction. Changes to the boundary interface would mean additions rather than updates. This is the essence of an Open-Closed design.
While the Open-Closed principle helps define how a boundary interface should change, the Single Responsibility Principle helps define why. A boundary interface should be defined to the point of granularity that restricts its responsibilities to one. Since each responsibility is an axis of change, the more axes there are, the more reasons for change.
Responsibilities become coupled in abstractions that have more than one responsibility. Each alteration to one responsibility can impact or impede the ability to satisfy any other responsibilities. This creates the notion of a delicate, fragile system. Let's take a look at a simple example using the figure below:
Here, each system layer depends on the core boundary interface. This interface defines all of the communication between each layer. This could be seen as a convenience but it actually means any change to the boundary interface impacts every layer. Alternatively, separate boundary interfaces can be defined as depicted in the figure below:
The figure above shows a proper system configured with boundary interfaces. Here it can be seen that each boundary interface has a distinct responsibility. One defines the communication between the Data Access layer and Business Logic, the other between the Business Logic and Application layer. This allows extension and reduction independently from one another -a key result of adequately applying the single responsibility principle.
System boundary interfaces make layer communication a high priority during system design. This can increase complexity and maintenance costs. It doesn't have to be that bad though. Keep your systems DRY. Don't repeat yourself. Not all communication abstractions need to be manually written. Entity Framework for example uses code generation to automatically create your data access layer. Code generics like SharpRepository and CSLA can further alleviate potential maintenance pain points. Using these frameworks can eliminate the repetitive nature of implementing system boundary interfaces while still maintaining the design's benefits.
There are many benefits to using SOLID principles including highly reusable and maintainable components. These principles can be applied further by first defining the integration points between each system layer. Abstracting integration points may be most useful when there is a possibility that a sub-system will be changed or used in another solution. Each system layer can be substituted with an appropriate implementation of the boundary interface. This is the power of implementing layer-level SOLID principles through boundary interfaces.