Living APIs: Part 1 of N
Writing APIs is hard. There's several factors to balance and it's difficult to get it right the first time. Fortunately, often we don't! We're not all writing code for the JDK, where bad APIs can never be removed in order to retain strict compatibility.Most of the APIs I write are used by only a handful of teams (including my own), and I only need to maintain compatibility with the most recent releases.
I'd like to make changes to the API without changing all callers at the same time.
One example situation comes with distributed code ownership. I own some library that some other teams are dependent on. I don't have full access to their code in version control, preventing me from using IDE refactoring tools to make all the changes. When I change the API, I need to support both the old and new APIs simultaneously. This allows my API's users to migrate painlessly.
In this series, I intend to describe some strategies for changing an API without breaking calling code.
Renaming an Interface...
public interface PizzaStore {
Address getAddress();
PizzaOrder createPizzaOrder();
}
Our customer has changed their menu to focus on salads and pastas, making my chosen interface name inappropriate. Some of the new stores don't even serve pizza:public interface PizzaStore {
Address getAddress();
SaladOrder createSaladOrder();
PastaOrder createPastaOrder();
boolean isPizzaAvailable();
PizzaOrder createPizzaOrder();
}
I need to rename the interface. Unfortunately, it's used by dozens of classes, spread across several teams! Shouldn't I just give-up and document that a PizzaStore
didn't necessarily sell pizza? No! The domain model is the core of the project, and it has to be perfect!Fortunately, renaming an interface in parts is mostly mechanical work. First, we create a copy of the
PizzaStore.java
code to Restaurant.java
:public interface Restaurant {
Address getAddress();
SaladOrder createSaladOrder();
PastaOrder createPastaOrder();
boolean isPizzaAvailable();
PizzaOrder createPizzaOrder();
}
Next, I remove all the methods in PizzaStore
and make it extend my new Restaurant
interface:/** @deprecated replace with Restaurant, which is more general */
@Deprecated
public interface PizzaStore extends Restaurant {
// only methods inherited from Restaurant!
}
Analyzing the Change
PizzaStore
will automatically implement Restaurant
, since the interface and all members are inherited.PizzaStore
will still have access to all the Restaurant
methods and fields, since these are also inherited.Extending an interface without adding new methods is essentially like creating an alias for that interface. I can submit these changes to version control immediately, and then change the callers incrementally.
Fixing the callers
Fixing the callers can be done without any urgency. But only after all callers are fixed can the old bad name be removed.
Now I can go through with my IDE and change all the users of
PizzaStore
in bulk. This is where find-and-replace tools, refactoring plugins, and Jackpot are very helpful. I change parameter types and variable declarations from PizzaStore
to Restaurant
. I do not change return values, and add a few casts where necessary:public class DeliveryService {
private PizzaStoreRestaurant defaultLocation;
public DeliveryService(PizzaStoreRestaurant defaultLocation) {
this.defaultLocation = defaultLocation;
}
PizzaStore getClosestStore(Address address) {
...
}
Delivery createDelivery(PizzaStoreRestaurant origin, Address destination) {
...
}
PizzaStore getDefaultLocation() {
return (PizzaStore)defaultLocation;
}
}
Once nobody is expecting a PizzaStore
as the return type I can change those as well and remove the ugly casts:public class DeliveryService {
private Restaurant defaultLocation;
public DeliveryService(Restaurant defaultLocation) {
this.defaultLocation = defaultLocation;
}
Restaurant getClosestStore(Address address) {
return null;
}
Delivery createDelivery(Restaurant origin, Address destination) {
return null;
}
Restaurant getDefaultLocation() {
return defaultLocation;
}
}
Finally, I can change my implementors from having an implements PizzaStore
block to implements Restaurant
, and remove the PizzaStore
interface for good.That's a lot of work!
Naturally, this is much more labour-intensive than ideal. It shows how getting the names right the first time will save time.
But there's very little reason to hang on to a bad name, even if there's a lot of code that already uses it. In an agile-world, the role and responsibility of a class will change over the lifetime of a project, and it's name should keep up.
By following this example and making changes in a backwards-compatible way, you make it easier for users to upgrade to your latest API - the code still compiles and it still works!
In a future post, I'll discuss some strategies for changing other API elements over time. I'll also rant about some Java language features that aren't well suited for such changes.
Update, July 16: added clarification as to why I cannot use IDE refactoring tools