Clean Code Chapter 8 - Boundaries
Using Third Party Code
-
There is a natural conflict between the provider of an interface and the user of the interface.
- The provider of an interface wants broad applicability so that their interface can be used in multiple environments.
- The user want an interface that is focused on their needs.
-
An example that is touched on is the use of
java.util.Map
, it has many functions.- One of the functions being
clear
which for example we may not want users to be able to use this method.
- One of the functions being
-
The solution to this issue of having too much accessibility to the wide range of functions from the third party
Map Object
was to encapsulate it in a class, essentially hiding it.- Say you want a Map of Sensors, we can define our business logic and rules for our Map of sensors within a class and also hide the interface of the Map boundary.
public class Sensors { private Map sensors = new HashMap(); public Sensor getById(String id) { return (Sensor) sensors.get(id); } }
-
This approach allows us to not have to pass an interface boundary like a
Map
around the system. -
It gives us back a level of control.
Exploring and Learning Boundaries
- When thinking of using a new third party library, rather than learning the library only through their documentation and integrating the library straight away.
- The better approach is to start by writing learning tests to explore our understanding of this third party code.
- In these learning tests we call the third-party API as we expect to use it in our application, think of it as controlled experimentation.
A Built Up Example with log4j
-
Say we want to test how this library works and we want to begin by simply logging "hello":
@Test public void testLogCreate() { Logger logger = Logger.getLogger("MyLogger"); logger.info("hello"); }
-
However this doesn't work, apparently we need an
Appender
, so we go back to the docs and then we createConsoleAppender
:@Test public void testLogAddAdapter() { Logger logger = Logger.getLogger("MyLogger"); ConsoleAppender appender = new ConsoleAppender(); logger.addAppender(appender); logger.info("hello"); }
-
Now we find out that Appender doesn't have an output stream?? You'd think it'd have one and be configured on the creator of the library's end.
-
After some googling they come up with:
@Test public void testLogAddAdapter() { Logger logger = Logger.getLogger("MyLogger"); logger.removeAllAppenders(); logger.addAppender(new ConsoleAppender( new PatternLayout("%p %t %m%n"), ConsoleAppender.SYSTEM_OUT)); logger.info("hello"); }
-
It turns out that the default ConsoleAppender constructor is "unconfigured", which doesn't seem obvious or useful.
-
Eventually we come up with a good understanding of how log4j works and this knowledge is encoded in a set of simple unit tests:
public class LogTest { private Logger logger; @Before public void initialize() { logger = Logger.getLogger("logger"); logger.removeAllAppenders(); Logger.getRootLogger().removeAllAppenders(); } @Test public void basicLogger() { BasicConfigurator.configure(); logger.info("basicLogger"); } @Test public void addAppenderWithStream() { logger.addAppender(new ConsoleAppender( new PatternLayout("%p %t %m%n"), ConsoleAppender.SYSTEM_OUT )); logger.info("addAppenderWithStream"); } @Test public void addAppenderWithoutStream() { logger.addAppender(new ConsoleAppender( new PatternLayout("%p %t %m%n") )); logger.info("addAppenderWithoutStream"); } }
-
Now we know how log4j works and we can encapsulate this knowledge into a class so that the rest of the application is isolated from the log4j boundary interface.
Learning Tests are Better than Free
- Learning Tests don't cost anything, you have to learn the API regardless and the tests capture what you learn in a very useful way.
- Learning Tests have a positive return on investment.
- If the third party package changes we can run our tests to see if there are behavioral changes.
- Whether you need the learning provided by the learning tests or not, a clean boundary should be supported by a set of outbound tests that exercise the interface the same way the production code does.
- This set of outbound tests can be referred to as boundary tests, they can heavily assist in code migration.
Using Code that Does not yet Exist
- When there is an unknown boundary in the codebase it can be useful to create a fake version of what it is that you don't know yet and implement this how you expect the unknown to work.
- This allows you to not be blocked by this unknown boundary.
- Once you know what is on the other side of this boundary (the API has been implemented) you can create some sort of adapter that allows your code to interact with the actual interface rather than your fake one.
- The fake version of the API can be used for testing other components that use the real one and we can write boundary tests for the now known API.
Clean Boundaries
- Change is one of the biggest thing that happens at boundaries and good software designs accommodate change without huge investments or rework.
- Code at the boundaries needs clear separation and tests that define expectations.
- It's better to depend on things that you control.
- We manage third party boundaries by having very few places in the code that refer to them.
- We may wrap them in something like a class like we saw earlier.
- We may use an adapter to convert from our interface to the provided interface.
- Either way our code speaks to us better, promotes internally consistent usage across the boundary, and has fewer maintenance points when the third party code changes.