2012-09-13 - Maintaining backwards compatibility through adapters
In developing SCIFIO one of our major priorities is backwards compatibility. SCIFIO completely overhauls the Bio-Formats class structure, breaking each reader down to an atomic set of components. While nearly all the Bio-Formats API can be expressed in SCIFIO API, even if there isn't a direct analog method call, there are several new considerations (such as removing state from readers, or adding N-dimensional support) that will change how the software works.
Our first conversion test run for SCIFIO was the
loci.common component. Many converted classes preserved their functionality exactly, but were migrated to a new (ome.scifio) package. This underscores a fundamental compatibility issue: packages are part of your software's contract. The natural solution was to create delegator classes that would maintain the legacy API by passing functional control to modern (SCIFIO) classes.
Unfortunately, there were still some kinks to work out. Interfaces in particular were a problem: if you want your legacy object to be useable in modern method signatures, you might try extending the modern equivalent interface (I did). However, you would be locking yourself into API at that moment. If you ever decide to add functionality in the the modern interface, you immediately break backwards compatibility in the legacy layer because of inheritance. Concrete implementations can mask these changes in low-level API via work-arounds within each method definition, but you still run into typing issues when you have a method that, say, takes the legacy type but then delegates its own functionality to a modern method that wants a modern version of that type (which inheritance would have actually solved for you.. instead it just sits slightly out of reach, tauntingly).
To resolve these issues, we created a LegacyAdapter interface and
Abstract implementation premised on using bi-directional wrappers (i.e. a class that is a Legacy and wraps a Modern, and vice versa) and having each adapter maintain maps of each Legacy or Modern instance to its wrapper.
In this model, there's no inheritance so no worry about accidentally breaking backwards compatibility with future development, and no locking of the API. The wrappers also allow for some elegant logic: whenever you have a Legacy or Modern instance, and need the other, you check if it's already a
Wrapper or not. If so, unwrap it and you get the desired type. If not, wrap it!
Because we wanted to maintain a mapping between instances and wrappers, it made a lot of sense to create a static AdapterTools utility that has one (generic parameterized) method that takes an adapter class and returns the statically maintained instance of that adapter. A static adapter means the maps are statically available, and once an object is wrapped it will never be wrapped again, and you can expect consistent behavior by using either the instance or its wrapper interchangeably anywhere in your software.
The only thing I didn't like about this implementation is that Bio-Formats doesn't use SezPoz yet (it will when SCIFIO fully comes online) so the map has to be populated via hard-coded calls instantiating each known Adapter class. The adapter pattern would be well-suited to an automatic discovery mechanism, so it's high on my wishlist once SCIFIO is ready for the public.
As it stands, there is still one significant limitation to the adapters (that I know of): if you extend a legacy Bio-Formats class (e.g. with a
Kraken extends IRandomAccess class) and then have some method that expects an instance of the extending class (
destroyTheMonster(Kraken kraken)) you will have to create your own
Adapter and then try to get the hard-coded instantiation into the
AdapterTools in the main code base. This could be a significant hurdle, so right now I'd recommend choosing to continue to operate on legacy classes, or convert your code to use the SCIFIO package names. (the latter being preferred, so you can continue to get any improvements from SCIFIO)
In the future, with SezPoz support, you would still have to create an
Adapter, but just by annotating as an
Adapter it would automatically be included in the static
AdapterTools class, and your library would automatically work as a plug-in to Bio-Formats/SCIFIO.