3

HttpClientWeb サーバーとの間で情報を送受信するために使用するクライアント ライブラリを作成しようとしています。このライブラリの中核となるのは、次のWebClientクラスです。

public class WebClient {
    public String send(String apiUrl) {
        // Use HttpClient to send a message to 'apiUrl', such as:
        // http://example.com?a=1&response=xml
        //
        // Wait for a response, and extract the HTTP response's body out
        // as raw text string. Return the body as a string.
    }
}

繰り返しますが、これは私が書いているライブラリmylib.jarです ( )。さまざまなサービスで構成されるライブラリ。各サービスには、API 開発者がサーバーへのデータの読み取り/書き込みに使用できる 1 つ以上のメソッドがあります。したがって、サービスは次のようになります。

WidgetService
    getAllWidgets
    getMostExpensiveWidget
    getLastWidgetUpdated
    etc...
FizzService
    getFizzById
    getFizziestFizz
    etc...
BuzzService
    etc...
...

各サービス メソッドは、1 つ以上の Java プリミティブまたは 1 つ以上のモデル インスタンス (エンティティ、値オブジェクトなど) を取ります。すべてのサービス メソッドは、そのようなモデル オブジェクトを 1 つ返します。たとえば、次のようになります。

public class FizzService {
    public Fizz getFizzById(Long fizzId) {
        // ...
    }

    public Fizz getFizzByBuzz(Buzz buzz) {
        // ...
    }
}

これは、 が受信した HTTP 応答本文をWebClient#send()最終的に Java オブジェクトにマッピングし直す必要があることを意味するため、注意が必要です。

API 開発者の観点から言えば、開発者が各サービス インスタンスをインスタンス化し、それをWebClient内部で使用するために渡し、引数と API URL の間、および HTTP 応答本文とモデル/エンティティの間のすべてのマッピングをサービスに行わせるようにするだけです。

たとえば、次のようになります。

public class FizzService {
    private WebClient webClient;
    private FizzApiBuilder fizzApiBuilder;

    // Getters & setters for all properties...

    public Fizz getFizzByBuzz(Buzz buzz) {
        // apiCall == "http://example.com/fizz-service/getFizzByBuzz?buzz_id=93ud94i49&response=json"
        String apiCall = fizzApiBuilder.build(buzz).toString();

        // We asked API to send back a Fizz as JSON, so responseBody is a JSON String representing
        // the correct Fizz.
        String responseBody = webClient.send(apiCall);

        if(apiCall.contains("json"))
            return JsonFizzMapper.toFizz(reponseBody);
        else
            return XmlFizzMapper.toFizz(responseBody);
    }
}

// What the API developer writes:
WebClient webClient = WebClientFactory.newWebClient();
FizzService fizzSvc = new FizzService(webClient);

Buzz b = getBuzz();

Fizz f = fizzSvc.getFizzByBuzz(b);

これまでのところ、私はこの設定が好きです。ただし、すべてのサービスにわたって、すべてのサービスメソッドに対して同じ「ボイラープレート」コードが必要になります。

String apiCall = someBuilder.build(...)
String responseBody = webClient.send(apiCall)
if(apiCall.contains("json"))
    return JsonMapper.toSomething(responseBody)
else
    return XmlMapper.toSomething(responseBody)

これは、抽象化の主要なユースケースのような匂いがし始めています。理想的には、このボイラープレート コードをすべて に入れ、AbstractService各サービスにその抽象クラスを拡張させたいと考えています。1つだけ問題があります:

public abstract class AbstractService {
    private WebClient webClient;
    private ServiceApiBuilder apiBuilder;

    // Getters & setters for all properties...

    public Object invoke(Object... params) {
        // apiCall == "http://example.com/fizz-service/getFizzByBuzz?buzz_id=93ud94i49&response=json"
        // The injected apiBuilder knows how to validate params and use them to build the api url.
        String apiCall = apiBuilder.build(params).toString();

        String responseBody = webClient.send(apiCall);

        if(apiCall.contains("json"))
            return jsonMapper.to???(reponseBody);
        else
            return xmlMapper.to???(responseBody);
    }
}

public class FizzService extends AbstractService { ... }

問題は、機能を に抽象化すると、API 呼び出しを作成するのが面倒AbstractServiceになることです(ただし、不可能ではありません)。さらに悪いことに、どのモデル/バックをマップするエンティティ。responseBody

これはすべて悪臭を放ち始めています。私のコードが悪臭を放つとき、私は解決策を探します。解決策が見つからない場合は、ここに来ます。だから私は尋ねます:私は根本的に間違ったアプローチをとっていますか?もしそうなら、私のアプローチはどうあるべきですか?そして、私が近い場合、どうすれば正しいマッパーを挿入し、このコードの匂いを良くすることができますか? API 開発者にモデル/サービスを実際に使用してもらいたい方法を示す私のコード スニペットを覚えておいてください。これがここでの最終的な目標です。前もって感謝します。

4

1 に答える 1

2

コメントで触れたように、作成したいリクエストの種類とコンパイル時のレスポンスのタイプの両方を知っているので、実行時にコードを単純に抽出して基本クラス。これらの決定を行うには、追加のコードを作成する必要があり、微妙なバグの余地が残ります。また、純粋にパラメーターに基づいてどの呼び出しを行うかを API ビルダーに判断させると、窮地に立たされます。たとえばgetMostExpensiveWidgetgetLastWidgetUpdatedどちらもパラメーターのないメソッドのように聞こえますが、API ビルダー クラスはどのようにして何をすべきかを知るのでしょうか?

したがって、API ビルダー クラスに、さまざまな API 呼び出しに対して個別のメソッドを提供するとします。また、マッパー クラスのメソッドは静的ではないと仮定します。次に、getFizzByBuzzメソッドは次のようになります。

public Fizz getFizzByBuzz(Buzz buzz) {
    String apiCall = fizzApiBuilder.buildForBuzz(buzz).toString();
    String responseBody = webClient.send(apiCall);
    return jsonFizzMapper.toFizz(reponseBody);
}

同様に、getFizzByIdメソッドは次のようになります。

public Fizz getFizzById(Long fizzId) {
    String apiCall = fizzApiBuilder.buildForId(fizzId).toString();
    String responseBody = webClient.send(apiCall);
    return xmlFizzMapper.toFizz(reponseBody);
}

これらのメソッドはどちらも同じパターンに従いますが、特定の API ビルダー メソッド ( buildForBuzzvsbuildForIdなど) と特定の応答マッパー ( jsonFizzMappervs xmlFizzMapper) が異なります。これらの決定を実行時に行いたくないためです。したがって、唯一の真の冗長性は、Web クライアントへの呼び出しです。それといくつかのインターフェイスを実装すると仮定するJsonFizzMapperと、次のステップは、クラスで次のプライベート メソッドを作成することです。XmlFizzMapperFizzMapperFizzService

private Fizz sendRequest(String apiCall, FizzMapper responseMapper) {
    String responseBody = webClient.send(apiCall);
    return responseMapper.toFizz(reponseBody);
}

getFizzByBuzz次のようになりgetFizzByIdます。

public Fizz getFizzByBuzz(Buzz buzz) {
    String apiCall = fizzApiBuilder.buildForBuzz(buzz).toString();
    return sendRequest(apiCall, jsonFizzMapper);
}

public Fizz getFizzById(Long fizzId) {
    String apiCall = fizzApiBuilder.buildForId(fizzId).toString();
    return sendRequest(apiCall, xmlFizzMapper);
}

これは、純粋さと実用主義のバランスが取れていると思います。理想的には、sendRequest メソッドをビルダー メソッド (例: buildForBuzz) に渡すだけですが、インターフェイスと匿名クラスに関してジャンプしなければならない面倒なことは、それだけの価値がないように思われます。

さらに先に進みたい場合は、 sendRequest メソッドをジェネリックにしてから抽象基本クラスに入れることができます (または、できれば継承よりも構成を優先し、完全に別のクラスに入れることができます)。次に、マッパー インターフェイスをジェネリックにする必要があります。 . ただし、そのクラスには 2 行のメソッドが含まれているだけなので、おそらく価値がないのではないかと思います。したがって、各サービス クラスに対応するプライベート メソッドを作成することは問題ないと思いますが、より複雑になる可能性が高いと思われる場合は、機能を別の場所に抽出します。

于 2012-12-18T17:22:20.127 に答える