5

Say I have an existing callback interface that has multiple methods. To illustrate my point I use a callback the likes that you would see in code that performs some HTTP client operations:

public interface GetCallback<T> {
    public void onSuccess(T data);
    public void onAuthFailure();
    public void onError(RequestError error);

    //Potentially more methods like onPreGet etc...
}

And a method that makes a request would take an instance of this callback as an argument:

public void issueRequest(String url, GetCallback<T> callback) {
    // Implementation ...
}

As is obvious, this suffers with verbosity at the call site:

public void getData(){
    issueRequest("http://programmers.stackexchange.com", new GetCallback<String>(){
        public void onSuccess(String data) {/*Do Something*/}
        public void onAuthFailure(){/*Ask for credentials*/}
        public void onError(RequestError error){/*Show error message*/}

    });
}

I have something similar in my code and it has been working well. I use a DefaultGetCallback class that provides default implementation of the onAuthFailure and onError methods since the action I want to take in those cases is pretty much the same regardless of what resource I'm requesting.

The question is, does it make sense to refactor this into a class composed of a set of single-method interfaces in order to take advantage of the lambda syntax?

public class GetCallback<T>{
    public interface OnSuccessListener<T> {
        public void onSuccess(T data);
    }

    public interface OnAuthFailureListener {
        public void onAuthFailure();
    }

    public interface OnErrorListener {
        public void onError(RequestError error);
    }

    private OnSuccessListener mSuccess;
    private OnAuthFailureListener mAuthFailure;
    private OnErrorListener mError;

    public GetCallback<T>(OnSuccessListener<T> success) {
        this.mSuccess = success;
    }

    public GetCallback<T> withAuthFailure(OnAuthFailureListener authFailure){
        this.mAuthFailure = authFailure;
        return this;
    }

    public GetCallback<T> withError(OnErrorListener error){
        this.mError = error;
        return this;
    }

}

I might use a Builder pattern to construct the GetCallback here but that's besides the point. The call site now becomes:

public void getData(){
    issueRequest(
        "http://programmers.stackexchange.com", 
        new GetCallback<String>(s -> doSomething(s))
            .withAuthFailure(() -> askForCredentials())
            .withError(error -> showErrorMessage(error))
    );
}

One advantage is I can customize the behavior of individual "events" (success, error etc) in isolation, at the call site, rather than having to sub-class or to create an anonymous inner class.

But then, how much is too much? Especially given the fact that my current design already served me well?

4
  • 1
    one may argue that conceptually, this has been addressed in How would you know if you've written readable and easily maintainable code? If your peers complain about your way of doing things, be it one way or another, you better change to make them feel better
    – gnat
    Sep 16, 2014 at 9:02
  • public void OnErrorListener I assume by this, you mean public interface OnErrorListener?
    – Neil
    Sep 22, 2014 at 14:05
  • The GetCallback is conceptually wrapping three other callbacks. Is there a reason they have to be bundled together? I'd find it much more usable if the issueRequest signature was public void issueRequest<T>(string url, Consumer<T> onSuccess, Runnable onAuthError, Consumer<RequestError> onError) instead of having to create the wrapping type.
    – Jack
    Sep 22, 2014 at 15:48
  • @Neil Thanks for pointing that out. Edited the question. Sep 23, 2014 at 4:34

3 Answers 3

4

Instead of building a facade wouldn't it make sense to do something more like this?

public interface SuccessListener<T> {

    default void onSuccess(T data) {
        // Completely ignore it.
    }

}

public interface AuthFailureListener {

    default void onAuthFailure() {
        // Completely ignore it.
    }

}

public interface ErrorListener {

    default void onError() {
        // Completely ignore it.
    }

}

// Retain the old API
public interface GetCallback<T> extends SuccessListener<T>, AuthFailureListener, ErrorListener {
    // Now empty - we are a conglmerate.
}

public static <T> void issueRequest(String url, SuccessListener<T> successListener, AuthFailureListener failureListener, ErrorListener errorListener) {
    // Implementation ...
}

public static <T> void issueRequest(String url, GetCallback<T> callback) {
    // Ensures backwards compatability.
    issueRequest(url, callback, callback, callback);
}

public void getData() {
    issueRequest("http://programmers.stackexchange.com", new GetCallback<String>() {

        @Override
        public void onSuccess(String data) {
        }

        @Override
        public void onAuthFailure() {
        }

        //@Override
        //public void onError() {
        // Can now leave out the ones you don't want.
        //}
    });
}

It is still compatible with your earlier API and internally who cares that the same object is a listener for all three events. It also opens up the possibilities of adding default implementations and allowing the chance to issue a request with only a SuccessListener.

Note: I tweaked the code to use default for even more niceness.

1

I would say, if there's a cost for your clients (the Users of your interface) to migrate to this another API, don't do it, it's not that ugly at all. If it's something new, or the cost of change is very low (few clients), go for it, as it would be much better for readability.

Another point is: like writing fluent interfaces, it's extremely verbose to write those functional interfaces for being able to use the lambdas, so if this an internal API I would go with the simplier straight forward implementation (without lambdas). In other hand, If this is a library that will be used for another systems, I would go with the lambda path. :)

1

You should not design interfaces based on the features of the implementation language but rather on the business model and on the OOP design principles.

A reason for splitting your interface would be a violation of Interface segregation principle (I from SOLID). This means it makes sense splitting if your GetCallback implementations usually don't need to implement all the methods.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.