Refactoring Interfaces
In our 2004 OOPSLA paper, Mock Roles not Objects, Steve, Joe, Tim and I described how we used Mock Objects and TDD to guide the design of object-oriented software. Briefly, we described the process as:
- Start by writing a unit test for an object's behaviour
- In the test, mock out interfaces for the services that you find the object requires
- Define expectations on mock objects to specify how the object communicates with those services, and how those services must behave in response
- When you have made the object pass the test, apply the same process to write the objects that provide those services it needs.
Unfortunately we neglected to describe a vital part of the process: refactoring. As the system grows we look out for interfaces that define similar ways in which objects collaborate - common patterns of communication between objects. We then collapse different interfaces that are incompatible but semantically equivalent into the same type.
We took for granted that when programmers refactor a system they apply as much refactoring effort to the interfaces between the objects as they do to the classes of the objects themselves. However, I have found that this is not the case. For example, Martin Fowler's canonical book of Refactoring patterns does not contain any patterns about refactoring interfaces or the communication protocols between objects.
If you follow the interface discovery process without refactoring the interfaces you discover, you end up with a system containing lots of interfaces, many of which represent very similar concepts in incompatible ways. Objects that should be plug-compatible cannot communicate without lots of awkward little adapter classes. As a result, I have found that teams develop a negative reaction to interfaces or even object-oriented design and end up with a design that is difficult to change because its classes are statically coupled.
When you refactor interfaces into a set of common communication patterns, objects in the system become much more "pluggable". You can then change the behaviour of the system by changing the composition of its objects-adding and removing instances, and plugging different combinations together-rather than writing procedural code. The code that composes objects acts a declarative definition of the how entire system will behave. You therefore end up working at a much greater level of abstraction and can focus on what you want the system to do, and not how that is implemented.
Obviously you need to strike a balance. If you end up with interfaces like the following, you've gone too far!
public interface Thing { Object doSomething(Object arg) throws Exception; }
Two refactoring steps I often apply are:
- Collapse Interfaces: if two interface definitions are semantically equivalent but incompatible, replace both interfaces with one interface that represents their common semantics.
- Distinguish Interfaces: define different interfaces so that code that tries to assemble invalid object graphs will not compile.