複数のメソッドを持つ既存のコールバックインターフェイスがあるとします。私のポイントを説明するために、いくつかのHTTPクライアント操作を実行するコードに表示されるようなコールバックを使用します。
public interface GetCallback<T> {
public void onSuccess(T data);
public void onAuthFailure();
public void onError(RequestError error);
//Potentially more methods like onPreGet etc...
}
そして、リクエストを行うメソッドは、このコールバックのインスタンスを引数として取ります。
public void issueRequest(String url, GetCallback<T> callback) {
// Implementation ...
}
明らかなように、これはコールサイトでの冗長性に悩まされています。
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*/}
});
}
私のコードには似たようなものがあり、うまく機能しています。 DefaultGetCallback
メソッドとonAuthFailure
メソッドのデフォルトの実装を提供するonError
クラスを使用します。これらの場合に実行するアクションは、どのリソースに関係なくほとんど同じだからです。リクエストしています。
問題は、ラムダ構文を利用するために、これを単一メソッドインターフェイスのセットで構成されるクラスにリファクタリングすることは意味がありますか?
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;
}
}
ここでGetCallback
を構築するためにBuilderパターンを使用するかもしれませんが、それは重要なことではありません。通話サイトは次のようになります。
public void getData(){
issueRequest(
"http://programmers.stackexchange.com",
new GetCallback<String>(s -> doSomething(s))
.withAuthFailure(() -> askForCredentials())
.withError(error -> showErrorMessage(error))
);
}
1つの利点は、サブクラスを作成したり匿名の内部クラスを作成したりするのではなく、呼び出しサイトで個々の「イベント」(成功、エラーなど)の動作を個別にカスタマイズできることです。
しかし、それでは、いくらですか?特に私の現在のデザインがすでに私に役立っているという事実を考えると?
facade を構築する代わりに、このようなことをするのは意味がありませんか?
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.
//}
});
}
それは以前のAPIと互換性があり、内部的には同じオブジェクトが3つのイベントすべてのリスナーであることを気にします。また、デフォルトの実装を追加し、SuccessListener
のみでリクエストを発行できるようにする可能性も広がります。
注:コードを微調整してdefault
を使用してさらに快適にしました。
クライアント(インターフェースのユーザー)がこの別のAPIに移行するのにコストがかかる場合は、移行しないでください。それほど醜いことではありません。それが何か新しいものである場合、または変更のコストが非常に低い(クライアントが少ない)場合は、読みやすさの点ではるかに優れているので、それを選択してください。
もう1つのポイントは、流暢なインターフェイスを作成するのと同様に、ラムダを使用できるようにするためにこれらの関数型インターフェイスを作成することは非常に冗長であるため、これが内部APIである場合は、より単純で単純な実装(ラムダなし)を使用します。一方、これが別のシステムで使用されるライブラリである場合は、ラムダパスを使用します。 :)
実装言語の機能に基づいてインターフェースを設計するのではなく、ビジネスモデルとOOP設計原則に基づいて)を設計する必要があります。
インターフェイスを分割する理由は、 インターフェイス分離の原則 (私はSOLIDから)の違反になります。これは、GetCallback
実装が通常すべてのメソッドを実装する必要がない場合、分割することが理にかなっていることを意味します。