web-dev-qa-db-ja.com

Webサービスの要求および応答オブジェクトをモデル化する設計パターン

約7 REST=実装するWebサービスがあります。これらのWebサービスには、標準(同一)の応答があるものと、異なる応答があるものがあります。

これらのWebサービスに対する要求は異なりますが、一部の要求と一部の応答には同じ基礎データオブジェクトがあります。

Webサービスごとに個別の要求/応答クラスを構築する必要があるか、標準のサービスを再利用する必要があるかはわかりません。これらのWebサービスの要求オブジェクトと応答オブジェクトをモデル化する設計パターンがあるかどうかを知りたいです。

アカウントとブックは、私のWebサービスが取り組む2つのリソースです。

class Account {
    String username;
    String id;
}


class Book {
    String title;
    String isbn;
}

したがって、私のWebサービスは次のようになります。

MYAPI/CreateAccountandBook
MYAPI/Account/Create
MYAPI/Book/Create
MYAPI/Book/Update/{isbn}
MYAPI/Account/Update/{id}
MYAPI/Account/getInfo/{id} 

等々。

CreateAccountandBookリクエストは、アカウントオブジェクトとペイロードの書籍のリストを受け取ります。また、MYAPI/Account/getInfo/{id}の応答オブジェクトには、アカウントオブジェクトとそのアカウントに関連付けられた書籍のリストがあります。ただし、応答オブジェクトにはstatusCodeDescriptionも含まれます。

次に、これらの要求オブジェクトと応答オブジェクトのクラスを可能な限り最良の方法で作成したいと思います。

始めましょう。

2つの抽象クラスStandardRequestStandardResponseがあります。

すべてのリクエストクラスは、標準リクエストクラスを拡張し、それに応じてカスタマイズします。すべての応答クラスは、標準応答クラスを拡張し、それに応じてカスタマイズします。

ただし、これらの要求と応答は互いに異なる可能性がありますが、同じエンティティオブジェクトを再利用できます。

例えば:

createAccountandBookリクエストオブジェクトは次のようになります。

class CreateAccountAndBookRequest {
   Account account;
   List<Book> books;
}

getInfo Webサービスの応答は次のとおりです。

class GetInfoResponse {
   Account account;
   List<Book> books;
   String statusCode;
   String description;
}

そのため、要求クラスと応答クラスに重複があります。 Webサービスごとに2つの(req/res)クラスを作成できます。しかし、これらのクラスをモデル化するより良い方法があるかどうかを知りたいです。

26
DntFrgtDSemiCln

同様のジレンマがありました。 genericの方向に進み、結果を好みます。それ以来振り返っていない。

GetAccounts AP​​Iメソッドがある場合、署名は次のようになります。

public final Response<Account[]> getAccounts()

当然、リクエストにも同じ原則が適用されます。

public final Response<Account[]> rebalanceAccounts(Request<Account[]>) { ... }

私の考えでは;要求と応答から個々のエンティティを分離すると、よりきれいなドメインとオブジェクトグラフが生成されます。

以下は、そのような一般的な応答オブジェクトがどのように見えるかの例です。私の場合;エラー処理を強化し、ドメインオブジェクトと応答オブジェクト間の結合を低くするために、すべての要求に対して一般的な応答を持つようにサーバーを構築しました。

public class Response<T> {

  private static final String R_MSG_EMPTY = "";
  private static final String R_CODE_OK = "OK";

  private final String responseCode;
  private final Date execDt;
  private final String message;

  private T response;

  /**
   * A Creates a new instance of Response
   *
   * @param code
   * @param message
   * @param execDt
   */
  public Response(final String code, final String message, final Date execDt) {

    this.execDt = execDt == null ? Calendar.getInstance().getTime() : execDt;
    this.message = message == null ? Response.R_MSG_EMPTY : message;
    this.responseCode = code == null ? Response.R_CODE_OK : code;
    this.response = null;
  }

  /**
   * @return the execDt
   */
  public Date getExecDt() {

    return this.execDt;
  }

  /**
   * @return the message
   */
  public String getMessage() {

    return this.message;
  }

  /**
   * @return the response
   */
  public T getResponse() {

    return this.response;
  }

  /**
   * @return the responseCode
   */
  public String getResponseCode() {

    return this.responseCode;
  }

  /**
   * sets the response object
   *
   * @param obj
   * @return
   */
  public Response<T> setResponse(final T obj) {

    this.response = obj;
    return this;
  }
}
11
dasm80x86

質問を含むこれまでのすべての回答に見られる大きな問題は、それらがすべて懸念の分離、情報の隠蔽、カプセル化の原則に違反しているということです。すべての回答で、要求(および応答)クラスはモデルクラスに密結合されています。それはより深刻な問題であり、リクエストとレスポンスの関係よりも重要な問題を提起します...

リクエスト/レスポンスクラスとモデルクラスの関係はどうあるべきですか?

リクエストクラス(CreateBookRequestなど)とモデルクラスBookのデータプロパティはほぼ同じであるため、次のいずれかを実行できます。

A.すべてのデータ/ゲッター/セッターをBookクラスに入れ、CreateBookRequestをクラスから拡張します

B. CreateBookRequestにメンバーとしてBookを含めます(ekostadinov、Juan Henaoによる質問と回答のように、dasm80x86による一般的な使用もこの特殊なケースです)。

C.データ/ゲッター/セッターをBookBaseに配置し、BookとCreateBookRequestの両方をBookBaseから拡張する

D.すべて/一部のデータ/ゲッター/セッターをBookStuffに入れ、BookとCreateBookRequestの両方にBookStuffを含める

E.すべてのデータ/ゲッター/セッターをBookとCreateBookRequestの両方に入れます。 (コピーアンドペーストできます)。

正解はEです。私たちは皆、非常に訓練されており、「再利用」に熱心であるため、これは最も直感的ではありません。

リクエストクラスCreateBookRequest(およびレスポンスクラスCreateBookResponse)とモデルクラスBookは、同じクラス階層(両方がオブジェクトを最上位の親として持つ場合を除く)にあるべきではありません(A、C)。また、CreateBookRequestは、モデルBookまたはBookクラスのメンバーである複合クラス(B、D)を参照/含むべきではありません。

この理由は次のとおりです:

1)モデルオブジェクトまたはリクエストオブジェクトを互いに独立して修正したい。リクエストがmdoelを参照している場合(A-Dなど)、モデルの変更はインターフェイスに反映されるため、APIが破損します。顧客は、要求/応答クラスによって指示されたAPIに従ってクライアントを作成するため、モデルクラスを変更するたびにクライアントを変更することは望みません。リクエスト/レスポンスとモデルを個別に変更したい場合。

2)懸念の分離。リクエストクラスCreateBookRequestには、すべての種類のインターフェイス/プロトコル関連のアノテーションとメンバー(たとえば、JAX-RS実装が実施する方法を知っている検証アノテーション)が含まれる場合があります。これらのインターフェイス関連の注釈は、モデルオブジェクト内にあるべきではありません。 (Aのように)

3)OOパースペクティブから)CreateBookRequestはブック(IS_Aではない)ではなく、ブックを含んでいません。

制御の流れは次のようになります:

1)インターフェース/コントロールレイヤー(Rest-API呼び出しを受信するレイヤー)は、そのレイヤー専用に定義されたリクエスト/レスポンスクラス(CreateBookRequestなど)をパラメーターとして使用する必要があります。コンテナ/インフラストラクチャがREST/HTTP/whateverリクエストからそれらを作成できるようにします。

2)インターフェイス/コントロールレイヤーのメソッドは、何らかの方法でモデルクラスオブジェクトのインスタンスを作成し、リクエストクラスからモデルクラスオブジェクトに値をコピーする必要があります。

3)インターフェイス/コントロールレイヤーのメソッドは、インターフェイスクラス/メソッドパラメータークラスではなく、モデルクラスオブジェクトに渡すBO/Manager/Whatever(モデルレイヤーで...ビジネスロジックを担当)を呼び出す必要があります。オブジェクト(つまり、ルイジメンドーサが答えで示したようにではありません)

4)model/BOメソッドは、何らかのモデルクラスオブジェクトまたは「プリミティブ」を返します。

5)ここで、インターフェイスメソッド(呼び出し元)はインターフェイスクラス応答オブジェクトを作成し、model/BOによって返されたモデルクラスオブジェクトから値をコピーする必要があります。 (答えに示されているように、ルイジメンドーサのように)

6)その後、コンテナ/インフラストラクチャは、応答クラスオブジェクトからJSON/XML/whatever応答を作成します。

質問された今...リクエストとレスポンスクラスの関係はどうあるべきですか?

要求クラスは、要求クラスから拡張する必要があり、応答クラスを拡張したり含むことはできません。逆も同様です。 (質問者からも提案されたように)。通常、非常に基本的なBaseRequestクラスがあり、CreateRequest、UpdateRequestなどのように拡張されます。すべての作成要求に共通のプロパティはCreateRequestにあり、CreateBookRequestなどのより具体的な要求クラスによって拡張されます。
同様に、しかしそれに並行して、Responseクラス階層があります。

また、質問者は、CreateBookRequestとCreateBookResponseの両方に、同じモデルメンバー(たとえば、モデルクラスはありません!)を含めることは問題ないかどうかを尋ねました。

これは、モデルによっても参照されるクラスを要求または応答で参照するほど深刻な問題ではありません。これに関する問題は、APIリクエストを変更してBookStuffInRequestAndResponseで変更する必要がある場合、すぐに応答に影響することです(逆も同様です)。

1)リクエストパラメータを変更したために顧客がクライアントコードを修正する必要がある場合、顧客は変更された応答を修正/修正する可能性があり、2)要求の変更には応答の変更が必要になる可能性が高いため、それほど悪くない方法(たとえば、新しい属性を追加する)が、常にそうであるとは限りません。

23
inor

そのようなデザインパターンがあるかどうかはわかりません。私は次のことを行います:

  • GETリクエストの場合、クエリ文字列またはパスでパラメーターを定義します。望ましい方法はパスです。また、サービスのパラメーターはほとんどありません。各サービスはこれを独自に処理します。ここには再利用性はありません。
  • POSTリクエストの場合、リクエストの本文に含まれるJSON形式のパラメーターを使用します。また、JSONコンテンツをマップするアダプター(使用しているテクノロジーによって異なります)を使用しますパラメーターとして受け取る特定のクラス。
  • 応答には、2つのアプローチがあります。

    • 実際の応答となるカスタムResponseWrapperクラスを作成できます。これには、応答コード、説明、および入力データの処理が成功した場合の応答の実際の内容を格納するvalueというフィールドが含まれます。クラスは次のようになります。

      public class ResponseWrapper {
          private int statusCode;
          private String description;
          private String value;
      }
      

      この場合、 String valueは、具体的な応答をJSON形式で保存します。例えば:

      @Path("/yourapi/book")
      public class BookRestfulService {
      
          @POST("/create")
          @Produces("json")
          public ResponseWrapper createBook(Book book) {
              ResponseWrapper rw = new ResponseWrapper();
              //do the processing...
              BookService bookService = new BookService();
              SomeClassToStoreResult result = bookService.create(book);
              //define the response...
              rw.setStatusCode(...);
              rw.setDescription("...");
              rw.setValue( convertToJson(result) );
          }
      
          static String convertToJson(Object object) {
              //method that probably will use a library like Jackson or Gson
              //to convert the object into a proper JSON strong
          }
      }
      
    • HTTP応答ステータスコード を再利用し、成功した要求には200(または要求タイプに応じて201)を使用し、応答には適切なステータスコードを使用します。応答のステータスコードが200(または201)の場合、適切なオブジェクトをJSON形式で返します。応答のステータスコードが異なる場合は、次のようなJSONオブジェクトを提供します。

      { "error" : "There is no foo in bar." }
      

JSONまたはXMLでRESTfulサービスを使用することにはトレードオフがあります。それは、応答の構造を知らない消費者にとっての複雑さの代償です。 WS- * Webサービスの場合、パフォーマンスの観点からトレードオフが生じます(他のRESTfulアプローチと比較)。

3
Luiggi Mendoza
 public ResponseDto(){
     String username;
     int id;
     ArrayList books <Book> = new ArrayList<Book>();

    // proper getters and setters...
    @Override
    public String toString(){
       //parse this object to return a proper response to the rest service,
       //you can parse using some JSON library like GSON
    }
}
1
Juan Henao

関して

リクエストオブジェクトとレスポンスオブジェクトをモデル化するデザインパターンがある場合

標準的な方法は、 コマンドデザインパターン です。コマンドリクエストをオブジェクトとしてカプセル化できるためです。そして、それにより、さまざまなリクエスト、キューまたはログリクエスト、レスポンスでクライアントをパラメータ化し、取り消し可能な操作などをサポートできます。

サンプル実装として:

  abstract class Request{
    public abstract void Execute();
    public abstract void UnExecute();
  } 

   class AccountAndBookRequest extends Request{
   Account account;
   List<Book> books;
   }
1
ekostadinov