In this particular case, there is a one-to-one relationship between the methods in the old API and the new -- the new variant just uses different spellings for the arguments and introduces some extra seams.
The process has been "test first" (although I would not call the design "test driven"). It begins with a check, using the legacy implementation. This stage of the exercise is to make sure that I understand how the existing behavior works.
We call a factory method in the production code, which acts as a composition root to create an instance of the legacy implementation. We pass a reference to the interface to a check, which exercises the API through a use case, validating various checks along the way.
Having done this, we then introduce a new test; this one calling a factory method that produces an instance of an adapter, that acts as a bridge between legacy clients and the new API.
The signature of the factory method here is a consequence of the pattern that follows, where I work in three distinct cycles
- RED: begin a test calibration, verifying that the test fails
- GREEN: complete the test calibration, verifying that the test passes
- REPLACE: introduce the adapter into the mix, and verify that the test continues to pass.
Then, I created an abstract decorator, implementing the legacy API by simply dispatching each method to another implementation of the same interface.
And then I define my adapter, which extends the wrapper of the legacy API, and also accepts an instance of the new API.
Finally, with all the plumbing in place, I return a new instance of this class from the factory method.
My implementation protocol then looks like this; first, I run the test using the adapter as is. With no overrides in place, each call in the api gets directed to TEST_CALIBRATION_FACADE, which throws an UnsupportedOperationException, and the check fails.
To complete the test calibration, I override the implementation of the method(s) I need locally, directing them to a local instance of the legacy implementation, like so:
The test passes, of course, because we're using the same implementation that we used to set up the use case originally.
In the replace phase, the legacy implementation gets inlined here in the factory method, so that I can see precisely what's going on, and I can start moving the implementation details to the new API.
Once I've reached the point that all of the methods have been implemented, I can ditch this scaffolding, and provide an implementation of the legacy interface that delegates all of the work directly to v2; no abstract wrapper required.
There's an interesting mirroring here; the application to model interface is v1 to v2, then then I have a bunch of coordination in the new idiom, but at the bottom of the pile, the v2 implementation is just plugging back into v1 persistence. You can see an example of that here - Booking looks fairly normal, just an orchestration with the repository element. WrappedCargo looks a little bit odd, perhaps -- it's not an "aggregate root" in the usual sense, it's not a domain entity lifted into a higher role. Instead, it's a plumbing element wrapped around the legacy root object (with some additional plumbing to deal with the translations).
Longer term, I'll create a mapping from the legacy storage schema to an entity that understands the V2 API, and eventually swap out the O/RM altogether by migrating the state from the RDBMS to a document store.
Another lovely post.
ReplyDeleteThis comment has been removed by the author.
ReplyDelete