At times it seems that the philosophy of agile software development is at odds with that of framework development. Given the amount of print and web space dedicated to agile methodologies, it’s incredible how little is dedicated to this topic. It’s almost as if this conflict needs to be kept hidden away from view, not to be discussed in public.
At Caplin we are living proof that framework development can be achieved following agile principles. We have been successfully building frameworks and APIs for the past 5 years using various agile methodologies from Scrum, to a mix of Scrum and XP, to Kanban.
This article represents the approaches that we have undertaken to successfully build our frameworks.
The Problem with Refactoring Framework
The specific issue in question is the idea that code should be kept lean, with the bare minimum implemented to achieve the required functionality within an iteration. If further functionality requires this code be be modified, usually within another iteration, then no problem, just refactor it until it does what needs to do. The unit tests that you wrote previously can help give you confidence that the functionality of any unmodified code will still work as expected, whilst new or updated unit tests will cover the additional functionality.
The decision to refactor, even a large refactoring effort, isn’t too hard to make when you are in complete control of the code base, for example when you are producing the whole application. However this luxury isn’t so trivial when you’re developing a framework. By all means you can refactor your code, however if the code you want to change is part of the public API, then you need to consider what impact this might have on the users of your framework.
Techniques for Building Agile Frameworks
Fortunately there are a number of techniques that can be applied to tackle this problem, which are described below. I would also recommend reading the short, but excellent, Chapter “Evolving Frameworks” from Kent Beck’s book Implementation Patterns. It provides valuable insights on the subject, based on Beck’s own experience of making JUnit 4 backwardly compatible, despite some significant changes to the API.
One technique is to simply keep everything backwardly compatible, no matter how difficult it is to do this. All of the classes and methods provided previously must still be present and provide the same functionality, even if the underlying implementation has been modified. New functionality is provided in parallel to this.
Any parts of the framework that you would prefer your customers to stop using, even though it is still fully functional, should be flagged as deprecated, and the documentation should link to the classes or methods that should be used instead. This provides your customers a chance to migrate away from the parts of the API that you are unsatisfied with as it suits them. This seems to be the approach that has been taken with Java where parts of the API have been deprecated for a decade but can still be used.
Once parts of the framework have been deprecated for a while, you also have the option of making an intentional breaking change to remove them. This was the approach taken for Python 3, which is deliberately backwardly incompatible with earlier versions. In this case a automated migration tool was created to convert from Python 2 to Python 3 code to help soften the blow. A similar approach is a good idea to help your customers identify the areas of their code they will need to update once they take the new version of your framework with the breaking change.
A specialisation of keeping everything backwardly compatible is to introduce a versioned API. A versioned API can be used when you have realised that one of the interfaces that your customer’s code implements is missing a particular method. Instead of adding the method to the existing interface, which will cause a compilation error for your customer, you can create a new interface that extends the original one. The new method can be added to the new interface, and your code that calls that method can check whether the object instance implements the old interface, in which case the method is not invoked, or the new interface, where it is. As before, it is worthwhile deprecating the old interface.
An example of a versioned API can be seen with the JMX interface
NotificationEmitter, which extends
NotificationBroadcaster, adding a single method:
removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback).
A problem with this approach is if you need to add a versioned API on top of an existing one. These multiple layers are likely to make the calling code turn pretty nasty, and can bloat out your API. At this stage it is worth considering if it is possible to make a breaking change.
Another problem is what to call the versioned API. The JMX example used different names
NotificationBroadcaster, although without knowing the API I wouldn’t be able to say which one superseded the other. It might also be difficult to come up with a different name that is meaningful. An alternate naming convention is to number to interface, for example
IHTMLDocument2. My main issue with this approach is that it can complicate matters if you ever decide to remove deprecated parts of the API. If
IHTMLDocument was to be removed, it looks odd to have an interface called
IHTMLDocument2 hanging around – surely it should be renamed
IHTMLDocument, however this enters a compatibility minefield.
Most of the time, I consider breaking changes to be a last resort. Breaking changes are always going to be frustrating for your customers, and if you keep forcing them to rewrite their code you’ll soon find that they have ceased to be your customer. However, as I have alluded to earlier, there can be good reasons to make breaking changes.
Maintaining deprecated parts of an API can become an expensive overhead, bloating out what might otherwise be a concise and neat API. Periodically removing these deprecated APIs can be worthwhile, provided that your customers have had adequate time and warning to move onto the new APIs.
Alternatively it might be very difficult to make an API backwardly compatible, or an API may be just too wrong to be kept. In both cases it is vital to explore all the possibilities suggested earlier to see if there is any way to avoid making the breaking change. If it is necessary to make the breaking change, consider how you can lessen the impact for your customers.
Always Consider Your Customer
Regardless of the reasons behind the breaking changes, it is vital that you focus on making your customer’s experience as pain free as possible. After all they want to focus their efforts on adding their business value on top of your framework, not bug fixing issues introduced due to changes to your framework.
- Clearly document which parts of the API are deprecated and provide clear information about what should be used instead. Also give your customers ample warning time about when a deprecated API will be removed.
- Provide a tool to identify code that will be impacted by the breaking change, and provide hints to what changes need to be made to it, or even automatically correct the code. In some cases a good IDE can help identify and correct breaking changes, however you shouldn’t assume that it will without testing it first. If a tool or IDE is infeasible, then a clear set of instructions for what to look for might be adequate.
- Flag which areas of the API are still young and may be subject to change. At Caplin we document newer parts of the API as beta. Early adopters are able to take advantage of the latest features, however they are warned that the API may still evolve. Although this can still be frustrating for our customers this is partially mitigated by the fact that we are listening to their feedback about the beta API and are feeding that back into the next version of the API, making it something even easier to use.
- Try to hold off making breaking changes until a major version release. It is incredibly frustrating if breaking changes are made in minor incremental releases.
- Provide consultancy to your customers to help with the upgrade to the version with the breaking changes.
There is no reason why you can’t build a framework whilst employing an agile methodology. Most of the principles of agile are equally valid for both framework and application developers. The main issue is that your ability to refactor the framework code is restricted when it comes to the public API. As a result, the importance of getting an API “right” the first time may seem vital when developing a framework compared to an application, however failure to create this “right” API is not a disaster. As shown earlier, there are several techniques that can be applied to build up a framework in an iterative and agile way.
At times it is very difficult to maintain backwards compatibility, and sometimes breaking changes are impossible to avoid, but the role of a framework provider should be focused on empowering the users of the framework, its customers, to build their software reliably and efficiently upon it, rather than spending all their time bug fixing issues introduced by new versions of the framework. As a result the processes and practices for agile framework development need to aligned with these goals – however that is a topic for another day.
In two weeks time I will be following this up with a look at one of the processes that we have put into place to help develop our frameworks, “Testing APIs for Backwards Compatibility”.