web-dev-qa-db-ja.com

単一のエンドポイントを持つWeb APIのアーキテクチャー設計

シナリオ:

複雑なロジックを使用してデータベースからデータを挿入、更新、フェッチする単一のAPIエンドポイントを使用してWebサービスをリファクタリングする必要がある状況があります。クライアントは1つの呼び出しを使用し、送信するデータ転送オブジェクトに応じて、サービスは異なる処理を実行します(APIは私が設計したものではないため、何もできません。既存のクライアントをサポートする必要があります)。

たとえば、クライアントは次のJSONを送信します。

{
    "Id": 1234,
    "FacebookToken": "some facebook access token"
}

次に、サービスはデータベースからフェッチしたデータで応答する必要があります。 JSONリクエストのすべてのフィールドはオプションであり、それらの値の組み合わせの種類と、データベースの現在のコンテンツに応じて、サービスはデータベース内のエンティティを作成、更新、または変更しない必要があります。作成/更新/選択されたエンティティをクライアントに返します。

質問

私の質問は、ロジックをドメインレイヤーのネストされたifステートメントの束として実装することを回避するために使用できるデザインパターンについて考えることができるかどうかです。私はそれをきめの細かい関数に分割することについて話しているのではなく(すでにそれを持っています)、オブジェクトの向きと多態性をデザインに組み込む方法についての詳細は、いくつかのよく知られたデザインパターンを利用することです。

これに対する答えは1つではないことに気づきましたが、誰かが同様の設計上の問題に直面し、いくつかの良いヒントを持っているのではないかと思っています。前もって感謝します。

悪い解決策の例:

これは、私がしないでくださいでサービスを実装する方法を示す簡単なC#の例です。短くしておくのは意図的に単純化していることに注意してください。通常、インターフェースを使用し、依存性注入コンテナなどを使用します。ここでの質問に最も関連する部分は、Service.PostRequest()メソッドです。

public class Request
{
    public string Id { get; set; }
    public string ExternalServiceToken { get; set; }
}

public class Response
{
    public string Id { get; set; }
    public string Name { get; set; }

    public override string ToString()
    {
        return $"{Id}: {Name}";
    }
}

public class UserEntity
{
    public string Id { get; set; }

    public string Name { get; set; }
}

public class Service
{
    private readonly Repository _repository;
    private readonly ExternalService _externalService;

    public Service(Repository repository, ExternalService externalService)
    {
        _repository = repository;
        _externalService = externalService;
    }

    public Response PostRequest(Request request)
    {
        if (request == null) // create new entity
        {
            return ResponseFactoryMethod(_repository.Create(null));
        }

        UserEntity entity;
        if (request.Id != null) // requested specific entity
        {
            entity = _repository.SelectById(request.Id);
            if (request.ExternalServiceToken != null && entity.Name == null) // update entity with Name
            {
                entity.Name = _externalService.GetName(request.ExternalServiceToken);
                _repository.AssignName(entity.Id, entity.Name);
            }
            return ResponseFactoryMethod(entity);
        }

        // entity.Id == null
        if (request.ExternalServiceToken != null)
        {
            string name = _externalService.GetName(request.ExternalServiceToken);
            entity = _repository.SelectByName(name); // try to find entity by Name, otherwise create
            if (entity == null)
            {
                entity = _repository.Create(name);
            }
            return ResponseFactoryMethod(entity);
        }
        return ResponseFactoryMethod(_repository.Create(null));
    }

    private Response ResponseFactoryMethod(UserEntity entity)
    {
        var response = new Response();
        response.Id = entity.Id;
        response.Name = entity.Name;
        return response;
    }
}

public class ExternalService
{
    private Dictionary<string, string> _names = new Dictionary<string, string>
    {
        { "abc", "John" },
        { "def", "Jane" },
        { "ghi", "Bob" }
    };

    public string GetName(string token)
    {
        return _names[token];
    }
}

public class Repository
{
    private readonly List<UserEntity> _entities = new List<UserEntity>();

    public UserEntity Create(string name)
    {
        var newUser = new UserEntity { Id = _entities.Count.ToString(), Name = name };
        _entities.Add(newUser);
        return newUser;
    }

    public UserEntity SelectById(string id)
    {
        return _entities.SingleOrDefault(e => e.Id == id);
    }

    public UserEntity SelectByName(string name)
    {
        return _entities.SingleOrDefault(e => e.Name == name);
    }

    public void AssignName(string id, string name)
    {
        UserEntity user = SelectById(id);
        if (user != null)
        {
            user.Name = name;
        }
    }
}

public class Client
{
    private static Service _service;

    public static void Main(string[] args)
    {
        var repository = new Repository();
        _service = new Service(repository, new ExternalService());

        RequestFromService(null);
        RequestFromService(new Request {Id = "0", ExternalServiceToken = "abc"});

        RequestFromService(new Request());
        RequestFromService(new Request { Id = "1", ExternalServiceToken = "def" });

        RequestFromService(new Request {ExternalServiceToken = "ghi"});
        RequestFromService(new Request {ExternalServiceToken = "ghi" });

        Console.ReadLine();
    }

    static void RequestFromService(Request request)
    {
        Response response = _service.PostRequest(request);

        string responseAsString = response != null ? response.ToString() : "null";

        Console.WriteLine("Response from service: " + responseAsString);
    }
}
4
Piotr

そのようなAPIの犯人であった私は、あなたの痛みを感じています。私は上司に強制されて戻って、既存の実装に影響を与えずにやり直すように強制されてしまいました(うん)、別の答えを考え出しました。

私が行ったリファクタリングの多くは、入力オブジェクトのプロパティを辞書のキーとして使用し、その方法で回避することでした。次に、指定されたこれらの関数に直接アクセスする個々のエンドポイントを追加して、拡張を管理し、間違いを修正しました。たとえば、エンドポイントが/ api/syncの場合、辞書は次のようになります。

<String, Function>
<"Facebook", ProcessFacebookRequest()>
<"PowerSchool", ProcessPowerSchoolRequest()>
//And so on...

次に、特定の「一般」リクエストごとにエンドポイントを追加しました

POST /api/sync/facebook
POST /api/sync/powerschool

これもほんの少しだけ良いです。プロジェクトと同じようにAPIを更新および拡張できる柔軟性を確保するために、バージョン管理スキームを強くお勧めします。したがって、現時点ではエンドポイントはそのままでも問題ありませんが、/v2/sync/facebook/近い将来に標準になり、古い機能を保持し、バグを発生させることなく新しい機能を導入できます。

1
Adam Wells