从 MVP Winform 客户端使用 WebAPI 的设计模式

Design pattern to consume WebAPI from MVP Winform Client

背景

我正在构建一个两层应用程序:

Winforms 客户端将使用 HttpClient 使用 WebAPI 服务。这两层都大量使用 IoC 和依赖注入设计模式

问题

当 Winforms 应用程序需要来自 WebAPI 服务的数据时,演示者将协调请求。我的问题是,您会直接在演示者内部使用 HttpClient 吗?为了保持演示者可测试,您如何确保不必依赖具体的 HttpClient 调用?我也在考虑以某种方式整合来自这个 question.

的最佳答案

我通过抽象一切来解决这个问题。

在表示层我会有一个服务抽象...

public interface IServiceAgent {
    Task<SomeResultObject> GetSomething(string myParameter);
}

...从网络中提取我想要的内容 API。演示者不需要协调请求。演示者不关心数据来自何处。它只知道它想要某样东西并提出要求 (SoC)。这是服务代理的工作 (SRP)。

服务代理实施可能需要调用不同的数据源。包括网络。因此抽象化 HttpClient 将放松与该实现的耦合。

一个简单的例子,比如...

public interface IHttpClient {
    System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
    //...other members as needed : DeleteAsync, PostAsync, PutAsync...etc
}

一些示例实现可能如下所示...

public class MyPresenter {
    public MyPresenter(IServiceAgent services) {...}
}

public class MyDefaultServiceAgent : IServiceAgent {
    IHttpClient httpClient;

    public MyDefaultServiceAgent (IHttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public async Task<SomeResultObject> GetSomething(string myParameter) {
          var url = "http://localhost/my_web_api_endpoint?q=" + myParameter;
          var result = await httpClient.GetAsync<SomeResultObject>(url);
          return result;
    }
}

public class MyDefaultHttpClient : IHttpClient {
    HttpClient httpClient; //The real thing

    public MyDefaultHttpClient() {
        httpClient = createHttpClient();
    }

    /// <summary>
    ///  Send a GET request to the specified Uri as an asynchronous operation.
    /// </summary>
    /// <typeparam name="T">Response type</typeparam>
    /// <param name="uri">The Uri the request is sent to</param>
    /// <returns></returns>
    public System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class {
        return GetAsync<T>(new Uri(uri));
    }

    /// <summary>
    ///  Send a GET request to the specified Uri as an asynchronous operation.
    /// </summary>
    /// <typeparam name="T">Response type</typeparam>
    /// <param name="uri">The Uri the request is sent to</param>
    /// <returns></returns>
    public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
        var result = default(T);
        //Try to get content as T
        try {
            //send request and get the response
            var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
            //if there is content in response to deserialize
            if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
                //get the content
                string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                //desrialize it
                result = deserializeJsonToObject<T>(responseBodyAsText);
            }
        } catch (Exception ex) {
            Log.Error(ex);
        }
        return result;
    }

    private static T deserializeJsonToObject<T>(string json) {
        var result = JsonSerializer.Deserialize<T>(json);
        return result;
    }
}

通过抽象化这些依赖关系,您可以通过 fake/mocked 服务代理进行单元测试来保持演示者的可测试性。您可以使用 fake/mocked HTTP 客户端测试您的服务代理。如果您需要 change/swap/maintain 您的应用程序组件,它还允许您注入这些接口的任何具体实现。