Chapter 7. Couple Architecture Components Loosely

There are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.

C.A.R. Hoare

Guideline:

  • Achieve loose coupling between top-level components.

  • Do this by minimizing the relative amount of code within modules that is exposed to (i.e., can receive calls from) modules in other components.

  • This improves maintainability because independent components ease isolated maintenance.

Having a clear view on software architecture is essential when you are building and maintaining software. A good software architecture gives you insight into what the system does, how the system does it, and how functionality is organized (in component groupings, that is). It shows you the high-level structure, the “skeleton” so to speak, of the system. Having a good architecture makes it easier to find the source code that you are looking for and to understand how (high-level) components interact with other components.

This chapter deals with dependencies on the component level. A component is part of the top-level division of a system. It is defined by a system’s software architecture, so its boundaries should be quite clear from the start of development. As it is touching upon the software architecture domain, it may be outside of your direct control. However, the implementation of software architecture always remains the responsibility of you as a developer.

Components should be loosely coupled; that is, they should be clearly separated by having few entry points for other components and a limited amount of information shared among components. In that case, implementation details of methods are hidden (or encapsulated) which makes the system more modular.

Sounds familiar? Yes, both as a general design principle and on a module level, loose coupling has been discussed in Chapter 6. Component coupling applies the same reasoning but at the higher level of components rather than modules. Module coupling focuses on the exposure of individual modules (classes) to the rest of the codebase. Component coupling focuses specifically on the exposure of modules in one component (group of modules) to the modules in another component.

Note

So a module being called from a module in the same component is considered to be an internal call if we assess at the component level, but when we assess it at the module level, there is module coupling indeed.

In this chapter, we refer to the characteristic of being loosely coupled on a component level as component independence. The opposite of component independence is component dependence. In that case, the inner workings of components are exposed too much to other components that rely on them. That kind of entanglement makes it harder to oversee effects that code changes in one component may have on others, because it does not behave in an isolated manner. This complicates testing, when we must make assumptions or simulations of what happens within another component.

Motivation

System maintenance is easier when changes within a component have effects that are isolated within that component. To clarify the advantages of having loosely coupled components, let us elaborate on the consequences of different types of dependencies with Figure 7-1.

blms 0701
Figure 7-1. Low component dependence (left) and high component dependence (right)

The left side of the figure shows a low level of component dependence. Most calls between modules are internal (within the component). Let us elaborate on internal and noninternal dependencies.

Calls that improve maintainability:

  • Internal calls are healthy. Since the modules calling each other are part of the same component, they should implement closely related functionality. Their inner logic is hidden from the outside.

  • Outgoing calls are also healthy. As they delegate tasks to other components, they create a dependency outward. In general, delegation of distinct concerns to other components is a good thing. Delegation can be done from anywhere within a component and does not need to be restricted to a limited set of modules within the component.

Note

Note that outgoing calls from one component are incoming calls for another component.

Calls that have a negative impact on maintainability:

  • Incoming calls provide functionality for other components by offering an interface. The code volume that is involved in this should be limited. Conversely, the code within a component should be encapsulated as much as possible—that is, it should be shielded against direct invocations from other components. This improves information hiding. Also, modifying code involved in incoming dependencies potentially has a large impact on other components. By having a small percentage of code involved in incoming dependencies, you may dampen the negative ripple effects of modifications to other components.

  • Throughput code is risky and must be avoided. Throughput code both receives incoming calls and delegates to other components. Throughput code accomplishes the opposite of information hiding: it exposes its delegates (implementation) to its clients. It is like asking a question to a help desk that does not formulate its own answer but instead forwards your question to another company. Now you are dependent on two parties for the answer. In the case of code, this indicates that responsibilities are not well divided over components. As it is hard to trace back the path that the request follows, it is also hard to test and modify: tight coupling may cause effects to spill over to other components.

The right side of the figure shows a component with a high level of component dependence. The component has many dependencies with modules outside the component and is thus tightly coupled. It will be hard to make isolated changes, since the effects of changes cannot be easily overseen.

Note

Note that the effects of component independence are enhanced by component balance. Component balance is achieved when the number of components and their relative size are balanced. For elaboration on this topic, see chapter Chapter 8.

To give you an idea of how coupling between components evolves over time, consider how entanglements seem to appear naturally in systems. Entanglements evolve over time because of hasty hacks in code, declining development discipline, or other reasons why the intended architecture cannot be applied consistently. Figure 7-2 illustrates a situation that we encounter often in our practice. A system has a clear architecture with one-way dependencies, but over time they become blurred and entangled. In this case, the entanglement is between layers, but similar situations occur between components.

blms 0702
Figure 7-2. Designed versus implemented architecture

Low Component Dependence Allows for Isolated Maintenance

A low level of dependence means that changes can be made in an isolated manner. This applies when most of a component’s code volume is either internal or outgoing. Isolated maintenance means less work, as coding changes do not have effects outside the functionality that you are modifying.

Important

Note that this reasoning about isolation applies to code on a smaller level. For example, a system consisting of small, simple classes signals a proper separation of concerns, but does not guarantee it. For that, you will need to investigate the actual dependencies (see, for example, Chapter 6).

Low Component Dependence Separates Maintenance Responsibilities

If all components are independent from each other, it is easier to distribute responsibilities for maintenance among separate teams. This follows from the advantage of isolated modification. Isolation is in fact a prerequisite for efficient division of development work among team members or among different teams.

By contrast, if components are tightly intertwined with each other, one cannot isolate and separate maintenance responsibilities among teams, since the effects of modifications will spill over to other teams. Aside from that code being hard to test, the effects of modifications may also be unpredictable. So, dependencies may lead to inconsistencies, more time spent on communication between developers, and time wasted waiting for others to complete their modifications.

Low Component Dependence Eases Testing

Code that has a low dependence on other components (modules with mainly internal and outgoing code) is easier to test. For internal calls, functionality can be traced and tested within the component. For outgoing calls, you do not need to mock or stub functionality that is provided by other components (given that functionality in that other component is finished).

For elaboration on (unit) testing, see also Chapter 10.

How to Apply the Guideline

The goal for this chapter’s guideline is to achieve loose coupling between components. In practice, we find that you can help yourself by adhering to the following principles for implementing interfaces and requests between components.

The following principles help you apply the guideline of this chapter:

  • Limit the size of modules that are the component’s interface.

  • Define component interfaces on a high level of abstraction. This limits the types of requests that cross component borders. That avoids requests that “know too much” about the implementation details.

  • Avoid throughput code, because it has the most serious effect on testing functionality. In other words, avoid interface modules that put through calls to other components. If throughput code exists, analyze the concerned modules in order to solve calls that are put through to other components.

Abstract Factory Design Pattern

Component independence reflects the high-level architecture of a software system. However, this is not a book on software architecture. In this section, we discuss only one design pattern that we frequently see applied in practice to successfully limit the amount of interface code exposed by a component: the Abstract Factory design pattern. A system that is loosely coupled is characterized by relying more on contracts and less on implementation details.

Many more design patterns and software architecture styles can help in keeping your architecture components loosely coupled. An example is using a framework for dependency injection (which allows Inversion of Control). For elaboration on other patterns, we kindly direct you to the many great books on design patterns and software architecture (see, for example, “Related Books”).

The Abstract Factory design pattern hides (or encapsulates) the creation of specific “products” behind a generic “product factory” interface. In this context, products are typically entities for which more than one variant exists. Examples are audio format decoder/encoder algorithms or user interface widgets that have different themes for “look and feel.” In the following example, we use the Abstract Factory design patten to encapsulate the specifics of cloud hosting platforms behind a small factory interface.

Suppose our codebase contains a component, called PlatformServices, that implements the management of services from a cloud hosting platform. Two specific cloud hosting providers are supported by the PlatformServices component: Amazon AWS and Microsoft Azure (more could be added in the future).

To start/stop servers and reserve storage space, we have to implement the following interface for a cloud hosting platform:

public interface CloudServerFactory {
    CloudServer launchComputeServer();

    CloudServer launchDatabaseServer();

    CloudStorage createCloudStorage(long sizeGb);
}

Based on this interface, we create two specific factory classes for AWS and Azure:

public class AWSCloudServerFactory implements CloudServerFactory {
    public CloudServer launchComputeServer() {
        return new AWSComputeServer();
    }

    public CloudServer launchDatabaseServer() {
        return new AWSDatabaseServer();
    }

    public CloudStorage createCloudStorage(long sizeGb) {
        return new AWSCloudStorage(sizeGb);
    }
}
public class AzureCloudServerFactory implements CloudServerFactory {
    public CloudServer launchComputeServer() {
        return new AzureComputeServer();
    }

    public CloudServer launchDatabaseServer() {
        return new AzureDatabaseServer();
    }

    public CloudStorage createCloudStorage(long sizeGb) {
        return new AzureCloudStorage(sizeGb);
    }
}

Note that these factories make calls to specific AWS and Azure implementation classes (which in turn do specific AWS and Azure API calls), but return generic interface types for servers and storage.

Code outside the PlatformServices component can now use the concise interface module CloudServerFactory—for example, like this:

public class ApplicationLauncher {

    public static void main(String[] args) {
        CloudServerFactory factory;
        if (args[1].equals("-azure")) {
            factory = new AzureCloudServerFactory();
        } else {
            factory = new AWSCloudServerFactory();
        }
        CloudServer computeServer = factory.launchComputeServer();
        CloudServer databaseServer = factory.launchDatabaseServer();

The CloudServerFactory interface of the PlatformServices provides a small interface for other components in the codebase. This way, these other components can be loosely coupled to it.

Common Objections to Loose Component Coupling

This section discusses objections regarding component dependence, whether they concern the difficulty of fixing the component dependence itself, or dependency being a requirement within the system.

Objection: Component Dependence Cannot Be Fixed Because the Components Are Entangled

“We cannot get component dependence right because of mutual dependencies between components.”

Entangled components are a problem that you experience most clearly during maintenance. You should start by analyzing the modules in the throughput category, as it has the most serious effect on the ease of testing and on predicting what exactly the functionality does.

When you achieve clearer boundaries for component responsibilities, it improves the analyzability and testability of the modules within. For example, modules with an extraordinary number of incoming calls may signal that they have multiple responsibilities and can be split up. When they are split up, the code becomes easier to analyze and test. For elaboration, please refer to Chapter 6.

Objection: No Time to Fix

“In the maintenance team, we understand the importance of achieving low component dependence, but we are not granted time to fix it.”

We understand how this is an issue. Development deadlines are real, and there may not be time for refactoring, or what a manager may see as “technical aesthetics.” What is important is the trade-off. One should resolve issues that pose a real problem for maintainability. So dependencies should be resolved if the team finds that they inhibit testing, analysis, or stability. You can solidify your case by measuring what percentage of issues arises/maintenance effort is needed in components that are tightly coupled with each other.

For example, throughput code follows complex paths that are hard to test for developers. There may be more elegant solutions that require less time and effort.

Objection: Throughput Is a Requirement

“We have a requirement for a software architecture for a layer that puts through calls.”

It is true that some architectures are designed to include an intermediate layer. Typically, this is a service layer that collects requests from one side (e.g., the user interface) and bundles them for passing on to another layer in the system. The existence of such a layer is not necessarily a problem—given that this layer implements loose coupling. It should have a clear separation of incoming and outgoing requests. So the module that receives requests in this layer:

  • Should not process the request itself.

  • Should not know where and how to process that request (its implementation details).

If both are true, the receiving module in the service layer has an incoming request and an outgoing request, instead of putting requests through to a specific module in the receiving component.

A large-volume service layer containing much logic is a typical code smell. In that case, the layer does not merely abstract and pass on requests, but also transforms them. Hence, for transformation, the layer knows about the implementation details. That means that the layer does not properly encapsulate both request and implementation. If throughput code follows from software architecture requirements, you may raise the issue to the software or enterprise architect.

See Also

A related concept to component independence is that of component balance, discussed in Chapter 8. That chapter deals with achieving an overseeable number of components that are balanced in size.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset