Published June 22, 2020
by Doug Klugh
Make tightly coupled code testable by extracting the logic into a separate, easy-to-test component that is decoupled from its environment. This pattern is often applied at the boundaries of a system where logic is difficult to test due to framework dependencies or asynchronous code. Extracting this logic decouples it from the boundary making those objects that interact with the boundary so humble they only need to be tested as part of the build process.
Once logic is extracted away from the boundary, the humble object that remains will contain very little code. Its only function will be to load the extracted component and delegate to it. Therefore, the only tests needed to validate the humble object are to verify that the load and delegate functions work properly. While these few tests will require on the order of seconds to execute, it is far less than if all the logic remained integrated with the boundary.
Since the humble object contains very little code that will rarely change, these few tests can be safely omitted from those test suites that are bundled with the unit tests and executed many times throughout the day. Instead, these Humble Object tests can be moved to a test suite that is only executed during the build process.
Designing a good object-oriented application architecture requires us to effectively partition the system. At a high-level, the application should be decoupled from the delivery mechanism as well as all data persistence. To achieve this we must isolate the use cases from the user interface. This is where boundaries come in. Boundary Objects reside within the application architecture forming the boundary that isolates the use cases. In some cases the user interface depends upon these objects and in other cases it will implement them. But there should never be a dependency that crosses that boundary pointing towards the delivery mechanism. All dependencies should always point towards the application.
To further partition the system, we need to encapsulate the application-specific behaviors, represented as use cases, within Interactor Objects. The application-agnostic behaviors should be encapsulated within Entity Objects, which are controlled by the Interactors. Then the Boundary Objects provide a communication channel between the use cases (encapsulated within the Interactors) and the user interface.