8 Boundaries

We seldom control all the software in our systems. Somehow we must cleanly integrate foreign code (thrid-party, open source, from other teams in our own company) with our own.

Using Third-Party Code

Providers of third-party packages and frameworks strive for broad applicability so they can work in many environments and appeal to a wide audience. Users, on the other hand, want an interface that is focused on their particular needs. This tension can cause problems at the boundaries of our systems. Let’s look at java.util.Map as an example. Our application might build up a Map and pass it around. Our intention might be that none of the recipients of our Map delete anything in the map, but the clear() method is still accessible. Or maybe our design convention is that only particular types of objects can be stored in the Map, but Maps do not reliably constrain the types of objects placed within them.
A cleaner way to use Map might be to encapsulate in a a new class. No user would care one bit if generics were used or not. That choice has become (and always should be) an implementation detail.
We are not suggesting that every use of Map be encapsulated in this form. Rather, we are advising you not to pass Maps (or any other interface at a boundary) around your system.

Exploring and Learning Boundaries

It’s not our job to test the third-party code, but it may be in our best interest to write tests for the third-party code we use. In learning tests we call the third-party API, as we expect to use it in our application. We’re essentially doing controlled experiments that check our understanding of that API.

Learning log4j

With a bit googling, reading, and testing, we’ve discovered a great deal about the way that log4j works, and we’ve encoded that knowledge into a set of simple unit tests.

Learning Tests Are Better Than Free

The learning tests end up costing nothing. We had to learn the API anyway, and writing those tests was an easy and isolated way to get that knowledge. Not only are learning tests free, they have a positive return on investment. When there are new releases of the third-party package, we run the learning tests to see whether there are behavioral differences.
A clean boundary should be supported by a set of outbound tests that exercise the interface the same way the production code does. Without these boundary tests to ease the migration, we might be tempted to stay with the old version longer than we should.

Using Code That Does Not Yet Exist

There is another kind of boundary, one that separates the known from the unknown. To keep from being blocked, we defined our own interface. One good thing about writing the interface we wish we had is that it’s under our control. This helps keep client code more readable and focused on what it is trying to accomplish. Once the API was defined, we wrote an Adapter to bridge the gap. The Adapter encapsulated the interaction with the API and provides a single place to change when the API evolves.
This design also gives us a very convenient seam in the code for testing. Using a suitable Fake, we can test the using classes. We can also create boundary tests.

Clean Boundaries

Interesting things happen at boundaries. Change is one of those things. Good software designs accommodate change without huge investments and rework. Code at the boundaries needs clear separation and tests that define expectations. We manage third-party boundaries by having very few places in the code that refer to them.

Previous: 7 Error handlingUp: ContentsNext: 9 Unit Tests