38

メッセージ ベースの Web サービスの利点に関する記事を読みましたが 、ServiceStack で Restful リソースのバージョン管理に推奨されるスタイル/プラクティスがあるかどうか疑問に思っています。バージョンが異なれば、異なる応答がレンダリングされたり、要求 DTO に異なる入力パラメーターが含まれたりする可能性があります。

私は URL タイプのバージョン管理 (つまり /v1/movies/{Id}) に傾いていますが、HTTP ヘッダーでバージョンを設定する他のプラクティスを見てきました (つまり、Content-Type: application/vnd.company.myapp-v2 )。

メタデータ ページで機能する方法を望んでいますが、ルートをレンダリングするときにフォルダー構造/名前空間を使用するだけで問題なく機能することに気付いたので、要件はそれほど多くありません。

例 (これはメタデータ ページで正しくレンダリングされませんが、直接のルート/URL がわかっている場合は適切に実行されます)

  • /v1/movies/{id}
  • /v1.1/movies/{id}

コード

namespace Samples.Movies.Operations.v1_1
{
    [Route("/v1.1/Movies", "GET")]
    public class Movies
    {
       ...
    } 
}
namespace Samples.Movies.Operations.v1
{
    [Route("/v1/Movies", "GET")]
    public class Movies
    {
       ...
    }   
}

および対応するサービス...

public class MovieService: ServiceBase<Samples.Movies.Operations.v1.Movies>
{
    protected override object Run(Samples.Movies.Operations.v1.Movies request)
    {
    ...
    }
}

public class MovieService: ServiceBase<Samples.Movies.Operations.v1_1.Movies>
    {
        protected override object Run(Samples.Movies.Operations.v1_1.Movies request)
        {
        ...
        }
    }
4

3 に答える 3

64

既存のサービスを進化させる (再実装しない) ようにする

バージョン管理の場合、異なるバージョン エンドポイントに対して異なる静的型を維持しようとすると、大変なことになります。最初はこのルートで開始しましたが、最初のバージョンのサポートを開始するとすぐに、同じサービスの複数のバージョンを維持するための開発作業が爆発的に増加します。さまざまなタイプの手動マッピングを維持する必要があるため、簡単に複数のバージョンを維持する必要が生じます。それぞれが異なるバージョン タイプに結合された並列実装 - DRY の大規模な違反。これは、同じモデルを異なるバージョンで簡単に再利用できる動的言語ではあまり問題になりません。

シリアライザーに組み込まれているバージョン管理を利用する

私の推奨事項は、明示的にバージョンを指定するのではなく、シリアル化形式内のバージョン管理機能を利用することです。

例: 通常、 JSON および JSV シリアライザーのバージョン管理機能ははるかに回復力があるため、JSON クライアントのバージョン管理について心配する必要はありません。

既存のサービスを防御的に強化する

XML と DataContract を使用すると、重大な変更を加えることなく、フィールドを自由に追加および削除できます。応答 DTOに追加IExtensibleDataObjectすると、DTO で定義されていないデータにアクセスする可能性もあります。バージョニングに対する私のアプローチは、破壊的な変更を導入しないように防御的にプログラムすることです。古い DTO を使用した統合テストでこれが当てはまることを確認できます。ここに私が従ういくつかのヒントがあります:

  • 既存のプロパティのタイプを変更しない - 別のタイプにする必要がある場合は、別のプロパティを追加し、古い/既存のプロパティを使用してバージョンを決定します
  • プログラムは、古いクライアントには存在しないプロパティを防御的に認識するため、それらを必須にしないでください。
  • 単一のグローバル名前空間を維持する (XML/SOAP エンドポイントにのみ関連)

これを行うには、各 DTO プロジェクトのAssemblyInfo.csで[assembly] 属性を使用します。

[assembly: ContractNamespace("http://schemas.servicestack.net/types", 
    ClrNamespace = "MyServiceModel.DtoTypes")]

assembly 属性を使用すると、各 DTO で明示的な名前空間を手動で指定する必要がなくなります。つまり、次のようになります。

namespace MyServiceModel.DtoTypes {
    [DataContract(Namespace="http://schemas.servicestack.net/types")]
    public class Foo { .. }
}

上記のデフォルトとは異なる XML 名前空間を使用する場合は、次のように登録する必要があります。

SetConfig(new EndpointHostConfig {
    WsdlServiceNamespace = "http://schemas.my.org/types"
});

DTO にバージョニングを埋め込む

ほとんどの場合、防御的にプログラミングし、サービスを適切に進化させる場合、特定のクライアントが使用しているバージョンを正確に知る必要はありません。入力されたデータから推測できるからです。ただし、まれに、クライアントの特定のバージョンに基づいてサービスの動作を微調整する必要がある場合は、DTO にバージョン情報を埋め込むことができます。

公開する DTO の最初のリリースでは、バージョン管理を考えなくても問題なく作成できます。

class Foo {
  string Name;
}

しかし、何らかの理由でフォーム/UI が変更され、クライアントがあいまいなName変数を使用する必要がなくなり、クライアントが使用していた特定のバージョンを追跡したい場合もあります。

class Foo {
  Foo() {
     Version = 1;
  }
  int Version;
  string Name;
  string DisplayName;
  int Age;
}

後でチーム ミーティングで議論されましたが、DisplayName は十分ではなく、別のフィールドに分割する必要があります。

class Foo {
  Foo() {
     Version = 2;
  }
  int Version;
  string Name;
  string DisplayName;
  string FirstName;
  string LastName;  
  DateTime? DateOfBirth;
}

したがって、現在の状態では、3 つの異なるクライアント バージョンがあり、既存の呼び出しは次のようになります。

v1 リリース:

client.Post(new Foo { Name = "Foo Bar" });

v2 リリース:

client.Post(new Foo { Name="Bar", DisplayName="Foo Bar", Age=18 });

v3 リリース:

client.Post(new Foo { FirstName = "Foo", LastName = "Bar", 
   DateOfBirth = new DateTime(1994, 01, 01) });

これらの異なるバージョンを同じ実装 (最新の v3 バージョンの DTO を使用する) で引き続き処理できます。例:

class FooService : Service {

    public object Post(Foo request) {
        //v1: 
        request.Version == 0 
        request.Name == "Foo"
        request.DisplayName == null
        request.Age = 0
        request.DateOfBirth = null

        //v2:
        request.Version == 2
        request.Name == null
        request.DisplayName == "Foo Bar"
        request.Age = 18
        request.DateOfBirth = null

        //v3:
        request.Version == 3
        request.Name == null
        request.DisplayName == null
        request.FirstName == "Foo"
        request.LastName == "Bar"
        request.Age = 0
        request.DateOfBirth = new DateTime(1994, 01, 01)
    }
}
于 2012-09-13T19:05:30.500 に答える
2

私もこれに対する解決策を見つけようとしており、以下のようなことを考えていました。(多くの Googlling および StackOverflow クエリに基づいているため、これは他の多くの人の肩の上に構築されています。)

最初に、バージョンを URI に含めるべきかリクエスト ヘッダーに含めるべきかについて議論したくありません。どちらのアプローチにも長所と短所があるため、それぞれの要件に最も適したものを使用する必要があると思います。

これは、Java メッセージ オブジェクトとリソース実装クラスを設計/構築する方法についてです。

それでは、始めましょう。

私はこれに2つのステップでアプローチします。マイナーな変更 (例: 1.0 から 1.1) とメジャーな変更 (例: 1.1 から 2.0)

マイナーチェンジへの取り組み

それでは、@mythz で使用されているのと同じサンプル クラスを使用するとします。

最初は

class Foo {   string Name; }

このリソースへのアクセスを /V1.0/fooresource/{id} として提供します

私のユースケースでは、JAX-RS を使用します。

@Path("/{versionid}/fooresource")
public class FooResource {

    @GET
    @Path( "/{id}" )
    public Foo getFoo (@PathParam("versionid") String versionid, (@PathParam("id") String fooId) 
    {
      Foo foo = new Foo();
     //setters, load data from persistence, handle business logic etc                   
     Return foo;
    }
}

ここで、Foo に 2 つのプロパティを追加するとします。

class Foo { 
    string Name;   
    string DisplayName;   
    int Age; 
}

この時点で行うことは、プロパティに @Version アノテーションを付けることです

class Foo { 
    @Version(“V1.0")string Name;   
    @Version(“V1.1")string DisplayName;   
    @Version(“V1.1")int Age; 
}

次に、要求されたバージョンに基づいて、そのバージョンに一致するプロパティのみをユーザーに返す応答フィルターがあります。便宜上、すべてのバージョンに対して返される必要があるプロパティがある場合は、それに注釈を付けないだけで、要求されたバージョンに関係なくフィルターがそれを返すことに注意してください。

これは仲介層のようなものです。私が説明したことは単純化されたバージョンであり、非常に複雑になる可能性がありますが、アイデアを理解していただければ幸いです.

メジャーバージョンへの取り組み

あるバージョンから別のバージョンに多くの変更が行われた場合、これは非常に複雑になる可能性があります。そのときは、2 番目のオプションに移動する必要があります。

オプション 2 は基本的に、コードベースから分岐し、そのコード ベースで変更を行い、異なるコンテキストで両方のバージョンをホストすることです。この時点で、アプローチ 1 で導入されたバージョン メディエーションの複雑さを取り除くために、コード ベースを少しリファクタリングする必要があるかもしれません (つまり、コードをよりクリーンにします)。これは主にフィルターにある可能性があります。

これは私が考えていることであり、まだ実装していないことに注意してください。これが良いアイデアかどうか疑問に思います。

また、フィルターを使用せずにこの種の変換を行うことができる優れたメディエーション エンジン/ESB があるかどうかも疑問に思っていましたが、フィルターを使用するほど単純なものは見たことがありません。たぶん私は十分に検索していません。

他の人の考えを知ることに興味があり、この解決策が元の質問に対処するかどうか。

于 2012-10-17T01:45:11.670 に答える
2

問題の枠組み

API は、その式を公開するシステムの一部です。ドメイン内での通信の概念とセマンティクスを定義します。問題は、表現できるものや表現方法を変えたいときです。

表現の仕方も表現する内容も違うかもしれません。最初の問題は、トークンの違い (名前ではなく姓と名) である傾向があります。2番目の問題は、さまざまなものを表現することです(自分の名前を変更する能力)。

長期的なバージョン管理ソリューションでは、これらの両方の課題を解決する必要があります。

API の進化

リソース タイプを変更してサービスを進化させることは、暗黙的なバージョニングの一種です。オブジェクトの構造を使用して動作を決定します。表現方法(名前など)にわずかな変更がある場合に最も効果的です。より複雑な表現方法の変更や表現力の変更にはうまくいきません。コードは全体に分散する傾向があります。

特定のバージョニング

変更がより複雑になると、各バージョンのロジックを分離しておくことが重要になります。神話の例でも、彼はバージョンごとにコードを分離しました。ただし、コードは同じメソッド内に混在しています。異なるバージョンのコードが互いに崩壊し始めるのは非常に簡単で、広がる可能性があります。以前のバージョンのサポートを取り除くのは難しい場合があります。

さらに、古いコードを依存関係の変更と同期させる必要があります。データベースが変更された場合、古いモデルをサポートするコードも変更する必要があります。

より良い方法

私が見つけた最善の方法は、式の問題に直接取り組むことです。API の新しいバージョンがリリースされるたびに、新しいレイヤーの上に実装されます。変更が小さいため、これは一般的に簡単です。

1 つ目は、マッピングを処理するすべてのコードが 1 つの場所にあるため、後で簡単に理解したり削除したりできることです。2 つ目は、新しい API が開発されたときにメンテナンスを必要としないことです (ロシア人形モデル)。

問題は、新しい API が古い API よりも表現力が低い場合です。これは、古いバージョンを維持するための解決策が何であれ、解決する必要がある問題です。問題があり、その問題の解決策が何であるかが明らかになります。

このスタイルの神話の例からの例は次のとおりです。

namespace APIv3 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var data = repository.getData()
            request.FirstName == data.firstName
            request.LastName == data.lastName
            request.DateOfBirth = data.dateOfBirth
        }
    }
}
namespace APIv2 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var v3Request = APIv3.FooService.OnPost(request)
            request.DisplayName == v3Request.FirstName + " " + v3Request.LastName
            request.Age = (new DateTime() - v3Request.DateOfBirth).years
        }
    }
}
namespace APIv1 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var v2Request = APIv2.FooService.OnPost(request)
            request.Name == v2Request.DisplayName
        }
    }
}

露出した各オブジェクトはクリアです。両方のスタイルで同じマッピング コードを記述する必要がありますが、分離スタイルでは、タイプに関連するマッピングのみを記述する必要があります。適用されないコードを明示的にマップする必要はありません (これは別の潜在的なエラーの原因です)。将来の API を追加したり、API レイヤーの依存関係を変更したりする場合、以前の API の依存関係は静的です。たとえば、データ ソースが変更された場合、最新の API (バージョン 3) のみをこのスタイルで変更する必要があります。組み合わせたスタイルでは、サポートされている API ごとに変更をコーディングする必要があります。

コメントの 1 つの懸念は、コード ベースへの型の追加でした。これらの型は外部に公開されているため、これは問題ではありません。コード ベースで型を明示的に指定すると、テストでの検出と分離が容易になります。保守性が明確である方がはるかに優れています。もう 1 つの利点は、このメソッドは追加のロジックを生成せず、追加の型を追加するだけであることです。

于 2012-09-14T22:43:57.777 に答える