3

I need to deprecate APIs in a Java SDK to make them more general. But I can't figure out how to do it for the following case:

public class AdoptDog {
    public interface OnDogAdoption {
        public void onDogAdoption(String dogName);
    }
    public void adoptDog(final String dogName, OnDogAdoption callbackObj) {
        // Perform asynchronous tasks...
            // Then call the callback:
            callbackObj.onDogAdoption(dogName);
    }
}

Users of the SDK make calls as follows:

AdoptDog adoptDog = new AdoptDog();
adoptDog.adoptDog("Snowball", new OnDogAdoption {
    @Override
    public void onDogAdoption(String dogName) {
        System.out.println("Welcome " + dogName);
    }
};

I want to generalize from Dog to Pet and deprecate APIs that mention Dog. For backward compatibility, the code snippet above where Snowball gets adopted should not have to change when I deprecate the APIs.

How I tried to deprecate the Dog API:

// Introduce Pet API

public class AdoptPet {
    public interface OnPetAdoption {
        public void onPetAdoption(String petName);
    }
    public void adoptPet(final String petName, OnPetAdoption callbackObj) {
        // Perform asynchronous tasks...
            // Then call the callback:
            if (callbackObj instanceof OnDogAdoption) {
                ((OnDogAdoption) callbackObj).onDogAdoption(petName);
            }
            else {
                callbackObj.onPetAdoption(petName);
            }
    }
}

// Dog API now extends Pet API for backward compatibility

@Deprecated
public class AdoptDog extends AdoptPet {
    @Deprecated
    public interface OnDogAdoption extends AdoptPet.OnPetAdoption {
        @Deprecated
        public void onDogAdoption(String dogName);
    }
    @Deprecated
    public void adoptDog(final String dogName, OnDogAdoption callbackObj) {
        super.adoptPet(dogName, callbackObj);
    }
}

The problem is it's not fully backward compatible. Users of the SDK have to implement AdoptPet.OnPetAdoption.onPetAdoption() or else they get a compiler error:

AdoptDog adoptDog = new AdoptDog();
adoptDog.adoptDog("Snowball", new OnDogAdoption {
    @Override
    public void onDogAdoption(String dogName) {
        System.out.println("Welcome " + dogName);
    }

    // PROBLEM: How avoid customers having to implement this dummy method? 
    @Override
    public void onPetAdoption(String petName) {
        assert("This code should not be reached");
    }
};

Is there some other way to deprecate AdoptDog (specifically OnDogAdoption) and maintain full backward compatibility?

Michael Osofsky
  • 11,429
  • 16
  • 68
  • 113
  • 1
    If you are on java 8 or later you can do it using the default method implementation as suggested by @Stewart, otherwise I don't think there is a way other than a breaking change. – NiVeR May 09 '18 at 21:24

2 Answers2

5

Java 8 allows you to specify default method implementations. You could use that to help you, for example:

@Deprecated
public interface OnDogAdoption extends AdoptPet.OnPetAdoption {
    @Deprecated
    void onDogAdoption(String dogName);

    default void onPetAdoption(String petName) {
        onDogAdoption(petName);
    }
}

By having a default implementation, the client code will not be required to implement it (but may if they wish) and so there should be no compilation error.


Note: All interface methods are public by default - in fact they can only be public - there's no need to specify that.

Stewart
  • 17,616
  • 8
  • 52
  • 80
  • 1
    Regarding your note, sometimes it's helpful to be explicit – pushkin May 09 '18 at 21:09
  • 1
    @pushkin Depends on the conventions you're used to. Here I was aiming to be brief, but wanted to offset someone complaining that I had not correctly duplicated the OP's code. Sigh, I guess you always get a comment one way or another ;) – Stewart May 09 '18 at 21:10
  • @pushkin sometimes it's misleading to be explicit: you give the impression it can be something other than public. – Andy Turner May 09 '18 at 21:11
  • @Stewart I feel you're right. Including the word `public` in interfaces is just needless noise; and it's kind of insulting to your fellow developers - you're saying "this is in case you people don't know that interface methods are always public". – Dawood ibn Kareem May 09 '18 at 21:12
  • 1
    @NiVeR I will add an explicit note for that. But seriously, Java 7 end-of-life was April 2015. – Stewart May 09 '18 at 21:13
  • 1
    @AndyTurner Fixed – Stewart May 09 '18 at 21:22
  • 1
    Great answer, thank you. To get it work on Android, I had to upgrade to Java 8 by following https://developer.android.com/studio/write/java8-support. In case anyone needs to the documentation on the Java 8 language feature used in the solution, "default methods", it's here https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html – Michael Osofsky May 09 '18 at 21:44
  • 2
    @Stewert I know of software which still targets Java 5. Java 7 is still used by some businesses. Just because something isn't currently maintained doesn't mean it's irrelevant. Ever heard of legacy software? He could be trying to fix up old software that's expected to run in a Java 5 environment. – Vince May 09 '18 at 21:51
  • @Stewart thanks for the quick, elegant solution. Default Methods seems like they were designed for this purpose. But @johannes-kuhn makes a good point that "A `OnDogAdoption` is not a `OnPetAdoption`." I would prefer a solution that avoids arcane syntax, in the case of your solution, Default Methods. His solution doesn't require special syntax, although he did say it can be simplified with Method References. Based on that, I prefer his solution, even though yours has more votes right now. Any additional thoughts? Thank you! – Michael Osofsky May 09 '18 at 22:25
  • 1
    Nothing wrong with using `DogAdoptionDispatcher` - although I would have called it an `Adaptor`. Note that Method References are also Java 8. – Stewart May 09 '18 at 22:31
3

You don't extend the OnPetAdoption. A OnDogAdoption is not a OnPetAdoption. A OnPetAdoption could also handle a cat adoption. A OnDogAdoption can not.

I would suggest that you wrap the OnDogAdoption in a OnDogAdoptionDispatcher, and call the new method with that dispatcher, e.g.:

@Deprecated
public class AdoptDog extends AdoptPet {
    @Deprecated
    public interface OnDogAdoption {
        @Deprecated
        public void onDogAdoption(String dogName);
    }

    private static class DogAdoptionDispatcher implements AdoptPet.OnPetAdoption {
            final OnDogAdoption target;
            public DogAdoptionDispatcher(OnDogAdoption target) {
                this.target = target;
            }
            @Override
            public void onPetAdoption(String petName) {
                target.onDogAdoption(petName);
            }
    }

    @Deprecated
    public void adoptDog(final String dogName, OnDogAdoption callbackObj) {
        super.adoptPet(dogName, new DogAdoptionDispatcher(callbackObj));
    }
}

This way, you still have backward compatibility and a clean new interface.

If you are using Java 8 or higher, you don't need a separate class for this, you could just do

public void adoptDog(final String dogName, OnDogAdoption callbackObj) {
    super.adoptPet(dogName, callbackObj::onDogAdoption);
}
Johannes Kuhn
  • 14,778
  • 4
  • 49
  • 73
  • Thank you, an elegant solution. In case anyone needs the documentation about the double colon syntax, it's at https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html. It doesn't seem to have a formal name but some people call it The Double Colon Operator. – Michael Osofsky May 09 '18 at 21:57
  • 1
    The name is method reference. – Johannes Kuhn May 09 '18 at 22:00
  • 1
    Shouldn't `AdoptDog` not extend `AdoptPet` for the same reason you made above, namely that "A `OnDogAdoption` is not a `OnPetAdoption`"? Maybe `AdoptDog` should dispatch to a private instance of `AdoptPet`. – Michael Osofsky May 11 '18 at 17:47