Atom Feed SITE FEED   ADD TO GOOGLE READER

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
  • Any code that implements PizzaStore will automatically implement Restaurant, since the interface and all members are inherited.
  • Any code that uses 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
  • I agree that API design and good naming is important.

    But you really need to look at a modern IDE like Eclipse or IntelliJ - those tools will do all of this for you automatically.
    I certainly use IntelliJ, and I think it's the best way to refactor a big codebase.

    But what I was trying to explain is that sometimes you cannot commit changes to all dependent projects at the same time.

    Perhaps a reasonable example is if I botched a name in some Glazed Lists API. We've got users at thousands of companies all using various different versions of the library. I still might like to fix the name, even if I cannot break backwards compatibility.

    In general, the types of codebases I'm thinking about are the ones you encounter on big projects, where code is shared by multiple teams.
    Just to be brutally explicit, I'm thinking about situations where I don't have commit privileges for projects that use my API.