- 3.1 Making Design Decisions
- 3.2 Design Concepts: The Building Blocks for Creating Structures
- 3.3 Design Concepts to Support Performance
- 3.4 Design Concepts to Support Availability
- 3.5 Design Concepts to Support Modifiability
- 3.6 Design Concepts to Support Security
- 3.7 Design Concepts to Support Integrability
- 3.8 Summary
- 3.9 Further Reading
- 3.10 Discussion Questions
3.5 Design Concepts to Support Modifiability
Modifiability is about change. As architects, we need to consider and plan for the cost and risk of making anticipated changes. To be able to plan for modifiability, an architect has to consider three big questions: (1) What can change? (2) What is the likelihood of the change? and (3) When is the change made and who makes it?
Change is so prevalent in the life of software systems that special names have been given to specific flavors of modifiability. Some of the common ones are scalability, variability, portability, and location independence, but there are many others.
3.5.1 Modifiability Tactics
Architects need to worry about modifiability to make the system easy to understand, debug, and extend. Tactics can help address these concerns.
The modifiability tactics categorization is shown in Figure 3.7. There are three major categories of modifiability tactics: Increase Cohesion, Reduce Coupling, and Defer Binding. We will examine these in turn.
FIGURE 3.7 Modifiability tactics categorization
Within the Increase Cohesion category, the tactics are:
Split module. If the module being modified includes responsibilities that are not cohesive, modification costs will likely be high. Refactoring the module to separate the responsibilities should reduce the average cost of future changes.
Redistribute responsibilities. If (related) responsibilities A, A′, and A″ are sprinkled across several distinct modules, they should be grouped together into a single module.
Within the Reduce Coupling category, the tactics are:
Encapsulate. Encapsulation introduces an explicit interface to an element, hiding its implementation details. This ensures that changes to the implementation do not affect the clients, as long as the interface remains stable. All access to the element must then pass through this interface.
Use an intermediary. Intermediaries, such as proxies, bridges, or adapters, are used for breaking dependencies between a set of components Ci or between Ci and the system S.
Abstract common services. When multiple elements provide services that are similar, it may be useful to hide them behind a common abstraction. The resulting encapsulation hides the details of the elements from other components in the system.
Restrict dependencies. This tactic restricts which modules a given module interacts with or depends on.
And within the Defer Binding category, we distinguish the tactics according to how late in the life cycle functionality is bound to the system:
Tactics to bind values at compile or build time: component replacement, compile-time parameterization, aspects.
Tactics to bind values at deployment, startup, or initialization time: configuration-time binding, resource files.
Tactics to bind values at runtime: discovery, interpret parameters, shared repositories, polymorphism.
3.5.2 Modifiability Patterns
In Section 3.2.3, we said that patterns differ from tactics in that tactics typically have just a single goal—the control of a quality attribute response—whereas patterns often attempt to achieve multiple goals and balance multiple competing forces. You might not think of some of the patterns that we present in this section as modifiability patterns at all. For example, we present the client-server pattern in this section. But first we begin with the uber modifiability pattern—layers.
3.5.2.1 Layered Pattern
In complex systems, there is a need to develop and evolve portions independently. For this reason, developers need a clear and well-documented separation of concerns, so that modules may be independently developed and maintained. The software therefore needs to be segmented in such a way that the modules can be evolved separately with little interaction among the parts. To achieve this separation of concerns, we divide the software into units called layers. Each layer is a grouping of modules that offers a cohesive set of services. Layers completely partition the software, and each partition is exposed through a public interface. There is, ideally, a unidirectional allowed-to-use relation among the layers.
But layering has its costs and tradeoffs. The addition of layers adds up-front effort and complexity to a system. Layers also add runtime overhead; there is a performance penalty to every interaction that crosses multiple layers. In Section 5.2.3, we will see how this pattern may be realized in terms of layered APIs.
3.5.2.2 Strategy
The Strategy pattern allows the selection of an algorithm, typically from a family of related algorithms, at runtime. This pattern is a realization of the defer binding tactic. It makes it easy to define a rich set of behaviors or policies and switch between them, typically at runtime, as needed. This pattern eliminates the need to clutter the code with complex switch or if statements to provide for the various options. It also eases the burden for programmers who might later want to evolve the system, adding even more options. The only drawback to this pattern is that it adds some up-front complexity. Thus, it should not be used if only a small and stable number of alternative algorithms or policies need to be implemented.
3.5.2.3 Client-Server Pattern
The Client-Server pattern consists of a server providing services simultaneously to multiple distributed clients. The most common example is a web server providing information to multiple browser clients. You might not think of this pattern as a modifiability pattern at all. But patterns are complex and they can serve multiple purposes. Indeed, the Client-Server pattern offers many benefits:
The connection between a server and its clients is established dynamically.
There is no coupling among the clients.
The number of clients can easily scale and is constrained only by the capacity of the server.
Server functionality can be scaled if needed with no impact on the clients.
Clients and servers can evolve independently as long as their APIs remain stable.
Common services can be shared among multiple clients.
For interactive systems, the interaction with a user is isolated to the client.
Not all of these benefits relate to modifiability, but certainly the aspects of this pattern that limit coupling and defer binding are obvious instantiations of modifiability tactics. A server typically has no prior knowledge of its clients.
The tradeoffs that this pattern introduces primarily relate to performance, availability, and security. When this pattern is implemented, communication occurs over a network. Messages may be delayed by network congestion, leading to degradation (or at least unpredictability) of performance. For clients that communicate with servers over a network shared by other applications, provisions must be made for security. In addition, servers may be single points of failure.