Skip to Content
Software EngineeringBooksClean CodeCh 8. Boundaries

Ch 8. Boundaries

Clean Boundary Practices in Software Development

  • In this chapter, we explore strategies and practices to maintain the integrity of software boundaries, particularly when incorporating code that isn’t under our direct control.
  • This includes third-party libraries, open-source projects, or even components developed by other teams within the same organization.

The Delicate Balance of Using Third-Party Interfaces

The use of third-party code creates a natural tension. Providers of these packages aim to design interfaces with wide applicability, catering to a large user base with varied requirements. Conversely, users desire interfaces tailored to their specific needs. This discrepancy often manifests at the system boundaries and can lead to integration issues.

Consider the java.util.Map interface as an illustration of this concept:

clear() void – Map containsKey(Object key) boolean – Map containsValue(Object value) boolean – Map entrySet() Set – Map equals(Object o) boolean – Map get(Object key) Object – Map hashCode() int – Map isEmpty() boolean – Map keySet() Set – Map put(Object key, Object value) Object – Map putAll(Map t) void – Map remove(Object key) Object – Map size() int – Map toString() String – Map values() Collection – Map
  • The Map interface provides extensive functionality, which is advantageous but also potentially problematic. For instance, when we share a Map within our application, we might not intend for the data to be modified. However, methods like clear() allow any user to potentially remove all mappings. Furthermore, the Map doesn’t enforce type constraints, making it possible to add any object type, which can lead to unchecked errors.

Managing the Type-Safety and Intended Use of Maps

Often in applications, we need to use a Map of a specific type, like Sensor. We may instantiate it simply:

Map sensors = new HashMap();

Subsequently, whenever we need to retrieve a Sensor, we cast the object:

Sensor s = (Sensor)sensors.get(sensorId);

This pattern is not only repetitive but also introduces the risk of runtime errors and makes the code harder to read. Using Java generics improves readability:

Map<Sensor> sensors = new HashMap<Sensor>(); ... Sensor s = sensors.get(sensorId);

Yet, even with generics, a Map<Sensor> still offers more functionality than might be desirable. If the Map interface were to change, it could necessitate widespread updates throughout the application, which was evident when generics were introduced in Java 5.

Encapsulating Map Usage

A more refined approach encapsulates the usage of Map, rendering the implementation details insignificant to the user:

public class Sensors { private Map sensors = new HashMap(); public Sensor getById(String id) { return (Sensor) sensors.get(id); } // Additional methods }
  • In this design, the boundary interface (Map) is concealed within the Sensors class, minimizing its impact on other application parts and making the use of generics a non-issue. This encapsulation not only simplifies understanding but also reduces the potential for misuse. The Sensors class can uphold design and business constraints more effectively.

Key Takeaways for Boundary Interfaces

  • It’s not necessary to wrap every interface like Map in a class, but it’s advisable to refrain from distributing boundary interfaces throughout your system. Instead, confine them within the class or a close-knit group of classes where they are employed.
  • Avoid exposing them in public APIs, either as return types or as parameters, to maintain clean boundaries in your software’s architecture.

Utilizing Third-Party Code: A Guide to Learning and Integration

Leveraging third-party packages accelerates feature delivery but integrating this external code can be complex. To effectively use these packages, we must:

  1. Start with Learning Tests: Instead of diving directly into production code, employ learning tests to understand the third-party API. These tests help confirm our grasp of the API’s functionality by running controlled experiments.

    Example: Learning log4j

    • Write a simple test case to use apache log4j.
    • Encounter issues like needing an Appender and understanding the ConsoleAppender.
    • Write tests to debug unusual behaviors and document the acquired knowledge.
public class LogTest { private Logger logger; @Before public void setup() { logger = Logger.getLogger("MyLogger"); logger.removeAllAppenders(); // Additional setup can be done here } @Test public void testBasicLogging() { // Tests for basic logging functionality } // More tests... }
  1. Incorporate Knowledge into an Isolated Class: After understanding the third-party code through learning tests, encapsulate the knowledge within a custom logger class. This approach isolates the rest of the application from direct interactions with the log4j API, creating a clean boundary.

  2. Benefits of Learning Tests: Learning tests come at no extra cost, as they are part of the necessary process to understand the API. They serve as documentation and a validation tool for checking compatibility with new versions of the third-party package.

  3. Handling Changes in Third-Party Code: Since external code evolves independently, learning tests act as a safety net, quickly alerting us to incompatibilities. Maintaining a clean boundary with well-defined tests prevents our codebase from being overly tied to the third-party implementation.

  4. Dealing with Unknowns: When facing undefined external interfaces, simulate the interface with a hypothetical construct that represents the desired functionality. For example, a placeholder ‘Transmitter’ interface can help decouple your code from an undefined external API until it is finalized.

  5. Design for Change at Boundaries: Expect changes at the interface boundaries with external code. Well-designed software minimizes rework through clear separation and tests. Avoid letting third-party idiosyncrasies permeate your codebase by controlling the points of interaction.

    • Use Wrappers or Adapters to interact with the third-party APIs.
    • Ensure that your code communicates with these interfaces cleanly and consistently.
    • Centralize the third-party interaction to reduce maintenance efforts when external code changes.
  • In summary, careful handling of third-party code with learning tests and clean boundaries ensures robust integration and eases future transitions, ultimately leading to a more maintainable and adaptable codebase.
Last updated on