Chapter 6. Separate Concerns in Modules

In a system that is both complex and tightly coupled, accidents are inevitable.

Charles Perrow’s Normal Accidents theory in one sentence

Guideline:

  • Avoid large modules in order to achieve loose coupling between them.

  • Do this by assigning responsibilities to separate modules and hiding implementation details behind interfaces.

  • This improves maintainability because changes in a loosely coupled codebase are much easier to oversee and execute than changes in a tightly coupled codebase.

The guidelines presented in the previous chapters are all what we call unit guidelines: they address improving maintainability of individual units (methods/constructors) in a system. In this chapter, we move up from the unit level to the module level.

Note

Remember that the concept of a module translates to a class in object-oriented languages such as Java.

This module-level guideline addresses relationships between classes. This guideline is about achieving loose coupling.

We will use a true story to illustrate what tight coupling between classes is and why it leads to maintenance problems. This story is about how a class called UserService in the service layer of a web application started growing while under development and kept on growing until it violated the guideline of this chapter.

In the first development iteration, the UserService class started out as a class with only three methods, the names and responsibilities of which are shown in this code snippet:

public class UserService {
    public User loadUser(String userId) { ... }

    public boolean doesUserExist(String userId) { ... }

    public User changeUserInfo(UserInfo userInfo) { ... }
}

In this case, the backend of the web application provides a REST interface to the frontend code and other systems.

A REST interface is an approach for providing web services in a simplified manner. REST is a common way to expose functionality outside of the system. The class in the REST layer that implements user operations uses the UserService class like this:

import company.system.services.UserService;

// The @Path and @GET attributes are defined by the the Java REST Service API
@Path("/user")
public class UserRestAPI {
    private final UserService userService = new UserService();
    ...
    @GET
    @Path("/{userId}")
    public Response getUser(@PathParam(value = "userId") String userId) {
        User user = userService.loadUser(userId);
        return toJson(user);
    }
}

During the second development iteration, the UserService class is not modified at all. In the third development iteration, new requirements were implemented that allowed a user to register to receive certain notifications. Three new methods were added to the UserService class for this requirement:

public class UserService {
    public User loadUser(String userId) { ... }

    public boolean doesUserExist(String userId) { ... }

    public User changeUserInfo(UserInfo userInfo) { ... }

    public List<NotificationType> getNotificationTypes(User user) { ... }

    public void registerForNotifications(User user, NotificationType type) {
        ...
    }

    public void unregisterForNotifications(User user, NotificationType type) {
        ...
    }
}

These new functionalities were also exposed via a separate REST API class:

import company.system.services.UserService;

@Path("/notification")
public class NotificationRestAPI {
    private final UserService userService = new UserService();
    ...
    @POST
    @Path("/register/{userId}/{type}")
    public Response register(@PathParam(value = "userId") String userId,
            @PathParam(value = "type" String notificationType)) {
        User user = userService.loadUser(userId);
        userService.registerForNotifications(user, notificationType);
        return toJson(HttpStatus.SC_OK);
    }

    @POST
    @Path("/unregister/{userId}/{type}")
    public Response unregister(@PathParam(value = "userId") String
            userId, @PathParam(value = "type" String notificationType)) {
        User user = userService.loadUser(userId);
        userService.unregisterForNotifications(user, notificationType);
        return toJson(HttpStatus.SC_OK);
    }
}

In the fourth development iteration, new requirements for searching users, blocking users, and listing all blocked users were implemented (management requested that last requirement for reporting purposes). All of these requirements caused new methods to be added to the UserService class.

public class UserService {
    public User loadUser(String userId) { ... }

    public boolean doesUserExist(String userId) { ... }

    public User changeUserInfo(UserInfo userInfo) { ... }

    public List<NotificationType> getNotificationTypes(User user) { ... }

    public void registerForNotifications(User user, NotificationType type) {
        ...
    }

    public void unregisterForNotifications(User user, NotificationType type) {
        ...
    }

    public List<User> searchUsers(UserInfo userInfo) { ... }

    public void blockUser(User user) { ... }

    public List<User> getAllBlockedUsers() { ... }
}

At the end of this development iteration, the class had grown to an impressive size. At this point the UserService class had become the most used service in the service layer of the system. Three frontend views (pages for Profile, Notifications, and Search), connected through three REST API services, used the UserService class. The number of incoming calls from other classes (the fan-in) has increased to over 50. The size of class has increased to more than 300 lines of code.

These kind of classes have what is called the large class smell, briefly discussed in Chapter 4. The code contains too much functionality and also knows implementation details about the code that surrounds it. The consequence is that the class is now tightly coupled. It is called from a large number of places in the code, and the class itself knows details on other parts of the codebase. For example, it uses different data layer classes for user profile management, the notification system, and searching/blocking other users.

Note

Coupling means that two parts of a system are somehow connected when changes are needed. That may be direct calls, but classes could also be connected via a configuration file, database structure, or even assumptions they make (in terms of business logic).

The problem with these classes is that they become a maintenance hotspot. All functionalities related (even remotely) to users are likely to end up in the UserService class. This is an example of an improper separation of concerns. Developers will also find the UserService class increasingly more difficult to understand as it becomes large and unmanageable. Less experienced developers on the team will find the class intimidating and will hesitate to make changes to it.

Two principles are necessary to understand the significance of coupling between classes.

  • Coupling is an issue on the class level of source code. Each of the methods in UserService complies with all guidelines presented in the preceding chapters. However, it is the combination of methods in the UserService class that makes UserService tightly coupled with the classes that use it.

  • Tight and loose coupling are a matter of degree. The actual maintenance consequence of tight coupling is determined by the number of calls to that class and the size of that class. Therefore, the more calls to a particular class that is tightly coupled, the smaller its size should be. Consider that even when classes are split up, the number of calls may not necessarily be lower. However, the coupling is then lower, because less code is coupled.

Motivation

The biggest advantage of keeping classes small is that it provides a direct path toward loose coupling between classes. Loose coupling means that your class-level design will be much more flexible to facilitate future changes. By “flexibility” we mean that you can make changes while limiting unexpected effects of those changes. Thus, loose coupling allows developers to work on isolated parts of the codebase without creating change ripples that affect the rest of the codebase. A third advantage, which cannot be underestimated, is that the codebase as a whole will be much more open to less experienced developers.

The following sections discuss the advantages of having small, loosely coupled classes in your system.

Small, Loosely Coupled Modules Allow Developers to Work on Isolated Parts of the Codebase

When a class is tightly coupled with other classes, changes to the implementation of the class tend to create ripple effects through the codebase. For example, changing the interface of a public method leads to code changes everywhere the method is called. Besides the increased development effort, this also increases the risk that class modifications lead to bugs or inconsistencies in remote parts of the codebase.

Small, Loosely Coupled Modules Ease Navigation Through the Codebase

Not only does a good separation of concerns keep the codebase flexible to facilitate future changes, it also improves the analyzability of the codebase since classes encapsulate data and implement logic to perform a single task. Just as it is easier to name methods that only do one thing, classes also become easier to name and understand when they have one responsibility. Making sure classes have only one responsibility is also known as the single responsibility principle.

Small, Loosely Coupled Modules Prevent No-Go Areas for New Developers

Classes that violate the single responsibility principle become tightly coupled and accumulate a lot of code over time. As with the UserService example in the introduction of this chapter, these classes become intimidating to less experienced developers, and even experienced developers are hesitant to make changes to their implementation. A codebase that has a large number of classes that lack a good separation of concerns is very difficult to adapt to new requirements.

How to Apply the Guideline

In general, this guideline prescribes keeping your classes small (by addressing only one concern) and limiting the number of places where a class is called by code outside the class itself. Following are three development best practices that help to prevent tight coupling between classes in a codebase.

Split Classes to Separate Concerns

Designing classes that collectively implement functionality of a software system is the most essential step in modeling and designing object-oriented systems. In typical software projects we see that classes start out as logical entities that implement a single functionality but over time gain more responsibilities. To prevent classes from getting a large class smell, it is crucial that developers take action if a class has more than one responsibility by splitting up the class.

To demonstrate how this works with the UserService class from the introduction, we split the class into three separate classes. Here are the two newly created classes and the modified UserService class:

public class UserNotificationService {
    public List<NotificationType> getNotificationTypes(User user) { ... }

    public void register(User user, NotificationType type) { ...  }

    public void unregister(User user, NotificationType type) { ...  }
}
public class UserBlockService {
    public void blockUser(User user) { ... }

    public List<User> getAllBlockedUsers() { ... }
}
public class UserService {
    public User loadUser(String userId) { ... }

    public boolean doesUserExist(String userId) { ... }

    public User changeUserInfo(UserInfo userInfo) { ... }

    public List<User> searchUsers(UserInfo userInfo) { ... }
}

After we rewired the calls from the REST API classes, the system now has a more loosely coupled implementation. For example, the UserService class has no knowledge about the notification system or the logic for blocking users. Developers are also more likely to put new functionalities in separate classes instead of defaulting to the UserService class.

Hide Specialized Implementations Behind Interfaces

We can also achieve loose coupling by hiding specific and detailed implementations behind a high-level interface. Consider the following class, which implements the functionality of a digital camera that can take snapshots with the flash on or off:

public class DigitalCamera {
  public Image takeSnapshot() { ... }

  public void flashLightOn() { ... }

  public void flaslLightOff() { ... }
}

And suppose this code runs inside an app on a smartphone device, like this:

// File SmartphoneApp.java:
public class SmartphoneApp {
  private DigitalCamera camera = new DigitalCamera();

  public static void main(String[] args) {
    ...
    Image image = camera.takeSnapshot();
    ...
  }
}

A more advanced digital camera becomes available. Apart from taking snapshots, it can also record video, has a timer feature, and can zoom in and out. The DigitalCamera class is extended to support the new features:

public class DigitalCamera {
  public Image takeSnapshot() { ... }

  public void flashLightOn() { ... }

  public void flaslLightOff() { ... }

  public Image takePanoramaSnapshot() { ... }

  public Video record() { ... }

  public void setTimer(int seconds) { ... }

  public void zoomIn() { ... }

  public void zoomOut() { ... }
}

From this example implementation, it is not difficult to imagine that the extended version of the DigitalCamera class will be much larger than the initial version, which has fewer features.

The codebase of the smartphone app still uses only the original three methods. However, because there is still just one DigitalCamera class, the app is forced to use this larger class. This introduces more coupling in the codebase than necessary. If one (or more) of the additional methods of DigitalCamera changes, we have to review the codebase of the smartphone app, only to find that it is not affected. While the smartphone app does not use any of the new methods, they are available to it.

To lower coupling, we use an interface that defines a limited list of camera features implemented by both basic and advanced cameras:

// File SimpleDigitalCamera.java:
public interface SimpleDigitalCamera {
    public Image takeSnapshot();

    public void flashLightOn();

    public void flashLightOff();
}

// File DigitalCamera.java:
public class DigitalCamera implements SimpleDigitalCamera {
      ...
}

// File SmartphoneApp.java:
public class SmartphoneApp {
  private SimpleDigitalCamera camera = SDK.getCamera();

  public static void main(String[] args) {
    ...
    Image image = camera.takeSnapshot();
    ...
  }
}

This change leads to lower coupling by a higher degree of encapsulation. In other words, classes that use only basic digital camera functionalities now do not know about all of the advanced digital camera functionalities. The SmartphoneApp class accesses only the SimpleDigitalCamera interface. This guarantees that Smart​pho⁠neApp does not use any of the methods of the more advanced camera.

Also, this way your system becomes more modular: it is composed such that a change to one class has minimal impact on other classes. This, in turn, increases modifiability: it is easier and less work to modify the system, and there is less risk that modifications introduce defects.

Replace Custom Code with Third-Party Libraries/Frameworks

A third situation that typically leads to tight module coupling are classes that provide generic or utility functionality. Classic examples are classes called StringUtils and FileUtils. Since these classes provide generic functionality, they are obviously called from many places in the codebase. In many cases this is an occurrence of tight coupling that is hard to avoid. A best practice, though, is to keep the class sizes limited and to periodically review (open source) libraries and frameworks to check if they can replace the custom implementation. Apache Commons and Google Guava are widespread libraries with frequently used utility functionality. In some cases, utility code can be replaced with new Java language features or a company-wide shared library.

Common Objections to Separating Concerns

The following are typical objections to the principle explained in this chapter.

Objection: Loose Coupling Conflicts With Reuse

“Tight coupling is a side effect of code reuse, so this guideline conflicts with that best practice.”

Of course, code reuse can increase the number of calls to a method. However, there are two reasons why this should not lead to tight coupling:

  • Reuse does not necessarily lead to methods that are called from as many places as possible. Good software design—for example, using inheritance and hiding implementation behind interfaces—will stimulate code reuse while keeping the implementation loosely coupled, since interfaces hide implementation details.

  • Making your code more generic, to solve more problems with less code, does not mean it should become a tightly coupled codebase. Clearly, utility functionality is expected to be called from more places than specific functionality. Utility functionality should then also embody less source code. In that way, there may be many incoming dependencies, but the dependencies refer to a small amount of code.

Objection: Java Interfaces Are Not Just for Loose Coupling

“It doesn’t make sense to use Java interfaces to prevent tight coupling.”

Indeed, using interfaces is a great way to improve encapsulation by hiding implementations, but it does not make sense to provide an interface for every class. As a rule of thumb, an interface should be implemented by at least two classes in your codebase. Consider splitting your class if the only reason to put an interface in front of your class is to limit the amount of code that other classes see.

Objection: High Fan-in of Utility Classes Is Unavoidable

“Utility code will always be called from many locations in the codebase.”

That is true. In practice, even highly maintainable codebases contain a small amount of code that is so generic that it is used by many places in the codebase (for example, logging functionality or I/O code). Highly generic, reusable code should be small, and some of it may be unavoidable. However, if the functionality is indeed that common, there may be a framework or library available that already implements it and can be used as is.

Objection: Not All Loose Coupling Solutions Increase Maintainability

“Frameworks that implement inversion of control (IoC) achieve loose coupling but make it harder to maintain the codebase.”

Inversion of control is a design principle to achieve loose coupling. There are frameworks available that implement this for you. IoC makes a system more flexible for extension and decreases the amount of knowledge that pieces of code have of each other.

This objection holds when such frameworks add complexity for which the maintaining developers are not experienced enough. Therefore, in cases where this objection is true, it is not IoC that is the problem, but the framework that implements it.

Thus, the design decision to use a framework for implementing IoC should be considered with care. As with all engineering decisions, this is a trade-off that does not pay off in all cases. Using these types of frameworks just to achieve loose coupling is a choice that can almost never be justified.

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

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