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
Mapinterface provides extensive functionality, which is advantageous but also potentially problematic. For instance, when we share aMapwithin our application, we might not intend for the data to be modified. However, methods likeclear()allow any user to potentially remove all mappings. Furthermore, theMapdoesn’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 theSensorsclass, 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. TheSensorsclass can uphold design and business constraints more effectively.
Key Takeaways for Boundary Interfaces
- It’s not necessary to wrap every interface like
Mapin 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:
-
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
Appenderand understanding theConsoleAppender. - 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...
}-
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.
-
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.
-
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.
-
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.
-
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
WrappersorAdaptersto 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.
- Use
- 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.