美文网首页
如何优雅的实现一个Client

如何优雅的实现一个Client

作者: 大哥你先走 | 来源:发表于2021-07-14 22:58 被阅读0次

原文首发于InfoQ:如何优雅的实现一个 Client

创建Client的主要目的是方便与Server进行交互,进而操作Server的数据或资源。Client可以采用不同的协议和Server进行交互,这完全取决于Server支持哪些协议,比如TCP、UDP、HTTP(S)、WebSocket、gRPC等等。使用不同协议的Client实现复杂度和技巧不同,要解决的核心问题也有所不同。本文无法也没有能力针对使用不同协议的Client提供一些有益的指导,为了下文更加清晰的描述,本文限定Client使用HTTP(S)协议与Server交互,Client调用Server开放的标准RESTful API操作Server的资源(创建、删除、更新、查询)。本文所有的样例均使用Java语言编写,不同语言在语法层面有所不同,解决同一问题的思路也不不一样,但是解决问题的思想是一致的,读者可以使用自己擅长的语言实现。

0 原则

设计一个优雅的Client应当站在使用者的角度,帮助使用者解决痛点,把困难留给自己,便利留给使用者,遵循以下原则有助于设计一个优雅的Client:

  • 文档完整原则:正确、清晰、完整的使用文档和接口文档。

  • 灵活配置原则:Client的配置应当以接口的形式开放,以提供使用者控制Client行为的能力。

  • 接口清晰原则:接口定义清晰,接口名能够自解释。

  • 最小开放原则:Client最小化开放的接口,接口的内部实现应当对使用者隐藏。

  • 测试完备原则:完备的单元测试、集成测试。

Client设计.png

1 Server的API定义

为了方便编写样例代码以阐述Client的设计思路,本文以构建Gotify的Client为例进行说明。Gotify是一个开源的用于发送和接受消息的Server,有兴趣的读者可以去官网查阅相关文档并亲自体验。

2 设计Gotify Client

2.1 开放Client配置

按照Client的设计原则,我们要做的第一件事情就是将Client必要的配置以接口的形式开放给Client的使用者,因此我们思考的第一件事情是Client有哪些必要的配置需要开放给使用者,Client需要开放的配置与具体的Server相关,对于Gotify来说,最基本的配置包括:Gotify的监听地址,Gotify的监听端口,Gotify是否开启ssl,如果开启ssl还需要配置相应的证书,总结下来Client只需要开放四个必要配置。为了简单又不失一般性,在文中仅开放三个必要配置:Gotify的监听地址,Gotify的监听端口,访问Gotify使用的协议(HTTP/HTTPS),下面使用Java的Interface定义Client需要开放的配置:

public interface GotifyClientConfig {
    default String getScheme() {
        return "http";
    }

    String getHost();

    int getPort();
}

Client开放的配置已经定义完成,使用者可以根据自己Gotify Server的情况实现该接口,但是要求使用者提供一个完整的实现,这对于使用者来说非常的不方便。我们需要思考解决使用者如何简单方便的创建Client配置 ,下面我们从简单易用性出发,考虑提供几个方便使用者创建配置的工具。

2.1.1 通过工厂方法创建配置

Client一共三个配置参数,其中两个必选参数,一个可选参数,参数的数量不是很多,因此我们可以考虑提供两个工厂方法(一个包含三个参数,一个包含两个参数),参数的位置按照scheme:host:port的位置进行排列(每个细节都以使用者为中心设计),因为这和三个参数在标准Uri中的顺序一致,方便使用者记忆。

方法1:拥有三个参数的工厂方法

public static GotifyClientConfig build(String scheme, String host, int port) {
  return new GotifyClientConfig() {
    @Override
    public String getScheme() {
      return scheme;
    }

    @Override
    public String getHost() {
      return host;
    }

    @Override
    public int getPort() {
      return port;
    }
  };
}

方法2:拥有两个参数的工厂方法

public static GotifyClientConfig build(String host, int port) {
  return build("http", host, port);
}

我们已经定义创建Client配置的工厂方法,下一个需要我们思考的问题是:这些工厂方法应该被组织到哪个类中?在本例子中我们将工厂方法组织到GotifyClientConfig 接口中,以下两点支撑我们做出这个决定:1、目前为止我们开放给使用者的只有GotifyClientConfig 接口,如果将方法组织到新的类中,比如我们创建一个工厂类GotifyClientConfigFactory,那么这会增加使用的负担,每增加一个开放的类,使用者的学习负担都在增加;2、Java 8 之后的版本从语言层面支持我们这么做,interface提供一些工厂方法在很多场景下都很适用。因此我们将工厂方法组织到GotifyClientConfig接口中,修改后的GotifyClientConfig接口如下:

public interface GotifyClientConfig {
    static GotifyClientConfig build(String scheme, String host, int port) {
        return new GotifyClientConfig() {
            @Override
            public String getScheme() {
                return scheme;
            }

            @Override
            public String getHost() {
                return host;
            }

            @Override
            public int getPort() {
                return port;
            }
        };
    }

    static GotifyClientConfig build(String host, int port) {
        return build("http", host, port);
    }

    // 省略其他方法
}

2.2.2 使用Builder模式

工厂方法可以很好的解决在Client中遇到的关于开放配置的问题,但是使用工厂方法有一点限制。工厂方法仅适用于配置参数不多,比如1-4个参数。复杂Client的配置的参数往往非常的多,超过10个参数都是很正常的,在这种多配置参数情况下使用工厂方法就非常的不方便,使用起来甚至比提供一个完整的实现难度还要大,一个拥有超多参数的方法是难以被正确使用的。对于这种多参数场景,设计模式中的Builder模式(建造者模式)是解决这类问题的不二法宝。下面我以常规的写提供一个Builder来简化配置的创建工作。

  • 第一步:定义一个不对使用者开放的GotifyClientConfig 实现

  • 第二步:定义Builder。

  • Builder的定义非常简单,而且都是样板代码,需要我们重点考虑的是:Builder是一个独立的开放类,还是一个开放类的内部类。如果作为内部类,应该作为哪个类的内部类?关于这一点我们建议是根据参数的个数选择,比如像本文中配置的参数很少,那么Build作为GotifyClientConfig 接口的内部类是可以。对于拥有较多参数的配置,建议开放一个独立类,对于独立的类这里又分两种情况,如果接口的实现开放那么Build可以作为接口实现类的内部类,如果接口实现类不开放,那么则需要创建一个独立的类。考虑使用独立类的主要目的是降低阅读GotifyClientConfig接口的难度,减少工具方法的干扰。

GotifyClientConfig的设计、开发工作已经完成,使用者可以使用下面的两种方式根据Gotify Server的实际情况创建GotifyClientConfig实例。

  • 方式一:使用工厂方法

  • 方式二:使用Builder

接下来让我们一起设计Gotify Client。

2.2 设计Gotify Client

上文提到Client的主要功能是与Server进行交互以操作Server的数据/资源,那么设计Client之前,需要掌握Server开放了多少个操作数据的接口,接口如何使用,接口有没有分类等信息。Gotify Server开放的是标准RESTful API,接口的使用非常的方便,可以通过命令行工具、HTTP 客户端等。使用接口不是难点,因此我们要分析的重点是Gotify提供了多少API,API是否有分类,如何在Client中优雅组织实现并开放API。

Gotify一共开放7大类31个API,7大类API分别操作application、message、client、user、health、plugin、version资源。如何组织31个API是需要设计的第一点,按照是否将所有的API通过一个接口开放,API的组织形式可以分为两类:

  • 集中式,将31个API全部通过一个接口开放,使用者创建一个Client/接口就可以使用全部的API。

  • 分类式,按照Server对API的分类,将不同的API组织到不同的Client/接口中,比如将API通过AppClient,MessageClient等多个接口开放。

两种组织API的方式孰优孰劣?其实,不管哪种方案都无法全面优于另一种方案,每个方案都有自己适用的场景,API的数量是选择何种组织方式的主要考虑因素。API的数量比较少选择集中式,这样使用者的学习使用成本都比较低,Client本身的实现复杂度也会降低。如果API的数量较多,而且Server已经按照资源将API分类,使用分类式的方式组织API就更加顺理成章,做出这种选择主要是因为以下两点:

  • 很少有使用者需要使用全部的API。

  • 分类可以降低使用者的学习成本,使用者只要学习自己需要的API。在众多的API中选择使用合适的API本身就是一件比较困难的事情,尤其在API设计不是很合理的情况下。

选择使用分类式的方式组织API,那么如何设计这些分类的Client呢?在设计之前我们需要回答以下问题:

  • Client有何异同?

  • 如何创建Client?

  • Client是否应有状态?

  • Client是否线程安全?

  • Client应该是单例还是多例?

弄清楚上面的问题,Client的设计、实现方案也就确定了。

  • 问题1::Client有何异同?

  • 异:不同Client开放的接口不同,不同的Client操作不同的数据/资源,这个不同点是很自然的。

  • 同:不同的Client都要和Server进行交互,都需要知道Server的信息,也就是说不同的Client都依赖GotifyClientConfig 。不同的Client从逻辑上讲都是Server Client的一部分,因此我们需要定义这些Client的相同部分,弄清楚这一点对于理解下稳定义和实现Client的方式非常的重要。

  • 问题2::如何创建Client?

  • 创建Client的方式多种多样,但是Client的创建方式应当统一,统一就可以降低使用者学习使用不同Client的难度。

  • 问题3: Client是否应该有状态?

  • 这是实现的一种权衡,无状态的Client实现更加容易。有状态Client的实现难度更、出错的概率更高,需要解决的问题也会更多,但是可能提供更好的使用体验,比如缓存数据可以提升API的响应速度。

  • 问题4:Client是否线程安全?

  • 为了降低使用者的使用难度,我们建议尽量将Client设计并实现为线程安全的Client。

  • 问题5: Client应该是单例还是多例?

  • 线程安全的Client实现为单例和多例区别不大,但是为了避免潜在资源的浪费,建议按照单例实现。非线程安全Client建议实现为多例,以降低使用出错的概率。

问题分析清楚以后Client的设计方案也就浮出水面,我们设计的Client将具备这样的特点:无状态且线程安全,使用工厂方法创建单例Client,所有的Client都实现了同一个接口用于表明这些Client归属一类。Client的逻辑示意图如下:

Client之间的关系1.png

2.2.1 定义Client的共同行为

所有Client的共同行因为Client不同而有所不同,在本文中所有的Client的共同行为是都支持关闭。在Java中可以使用接口和抽象类定义允许有多个实现的类型,使用接口是比抽象类更佳的优秀实践。如果只是想定义一个类型,那么标记接口(不包含任何方法的接口)是一种不错的选择。

public interface CloseableClient {
    void close();
}

2.2.2 定义操作不同资源的Client

AppClient:

public interface AppClient extends CloseableClient {
    Iterable<Application> listApplication();

    boolean deleteApplication(String id);
}

MessageClient:

public interface MessageClient extends CloseableClient {
    Iterable<Message> listMessageOfApplication(String appId);

    boolean deleteOneMessage(String id);
}

操作资源的Client已经定义完整,但是先不要着急去实现这些Client,下一步我们要解决的问题是如何方便的创建这些Client。

2.2.3 使用工厂方法创建Client

按照上文对方案的介绍,GotifyClient将负责创建AppClientMessageClient等操作资源的Client,下面是GotifyClient的定义:

public interface GotifyClient {

    AppClient getAppClient();

    MessageClient getMessageClient();
}

所有操作资源的Client,比如AppClient都将由GotifyClient负责创建。对外的接口已经定义清楚,下面来分别实现AppClientMessageClientGotifyClient,注意这些Client的实现都不对使用者开放

2.2.4 AppClient和MessageClient的实现

AppClientMessageClient的实现需要满足我们对Client的设计要求:Client无状态,Client线程安全。

AppClient 的实现

class AppClientImpl implements AppClient {

    private GotifyClientConfig clientConfig;

    public AppClientImpl(GotifyClientConfig clientConfig) {
        this.clientConfig = clientConfig;
    }
    // 省略实现的接口
}

MessageClient 的实现

class MessageClientImpl implements MessageClient {
    private GotifyClientConfig clientConfig;

    public MessageClientImpl(GotifyClientConfig clientConfig) {
        this.clientConfig = clientConfig;
    }
    // 省略实现的接口
}

2.2.5 实现GotifyClient

GotifyClient的实现需要满足我们对Client的设计要求:Client是单例,Client的创建方式是统一的。

class GotifyClientImpl implements GotifyClient {

    private GotifyClientConfig clientConfig;

    private AtomicReference<AppClient> appClientRef = new AtomicReference<>();

    private AtomicReference<MessageClient> messageClientRef = new AtomicReference<>();

    public GotifyClientImpl(GotifyClientConfig clientConfig) {
        this.clientConfig = clientConfig;
    }

    @Override
    public AppClient getAppClient() {
        return newClient(appClientRef, AppClientImpl::new);
    }

    @Override
    public MessageClient getMessageClient() {
        return newClient(messageClientRef, MessageClientImpl::new);
    }

    // 实现Client的单例要求,而且统一了Client的创建方式
    private synchronized <T extends CloseableClient> T newClient(AtomicReference<T> reference,
                                                                             Function<GotifyClientConfig, T> factory) {
        T client = reference.get();

        if (Objects.isNull(client)) {
            client = factory.apply(clientConfig);
            reference.lazySet(client);
        }

        return client;
    }
}

2.2.6 提供工厂方法创建GotifyClient

我们参考GotifyClientConfig的实现方式,在GotifyClient接口中添加一个静态方法,修改后的GotifyClient 接口如下:

public interface GotifyClient {

    static GotifyClient build(GotifyClientConfig config) {
        return new GotifyClientImpl(config);
    }

    AppClient getAppClient();

    MessageClient getMessageClient();
}

3 GotifyClient 的使用样例

根据上面设计,我们使用GotifyClient过程可以分为四步:

  • 根据需要确定需要哪些资源,需要使用哪些Client?

  • 获取Gotify Server的基本信息,创建GotifyCLientConfig

  • 创建GotifyClient

  • 使用GotifyClient创建需要的操作资源的Client

下面以获取运行在本地监听6875端口的Gotify 所有Application为例,详细讲解如何使用我们上面设计的Client。

// 步骤一:创建配置
GotifyClientConfig config = GotifyClientConfig.Builder.builder()
  .scheme("http")
  .host("localhost")
  .port(6875)
  .build();

// 使用GotifyClientConfig创建GotifyClient
GotifyClient gotifyClient = GotifyClient.build(config);

// 使用GotifyClient创建AppClient
AppClient appClient = gotifyClient.getAppClient();
appClient.listApplication().forEach(System.out::println);

4 总结

实现一个Client一定要站在使用者的角度,以使用者为中心,对使用者屏蔽实现的细节和复杂性,将困难留给自己。总结起来实现一个优雅的Client的需要着重提高Client的封装性和易用性。一个优雅的Client不仅使用者使用起来优雅,实现者也要能够优雅的实现,优雅的修改,这就要求我们的Client一定要很好的封装,仅开放必须开放的接口,内部实现尽量不要暴露给使用者。Client的目的就是为了帮助使用者更好的和Server进行交互,因此易用性更加重要,易用性高的Client才能得到使用者的认可,简单易用的Client也可以降低使用出错的概率。

5 附录

1、样例代码仓库:https://github.com/ctlove0523/gotify-java-client

2、Gotify项目地址:https://github.com/gotify

相关文章

  • 如何优雅的实现一个Client

    原文首发于InfoQ:如何优雅的实现一个 Client[https://xie.infoq.cn/article/...

  • 基于Spring Cloud Feign组件快速构造http c

    如何用java写一个简单的http client端?或者如何用java快速高效的写一个http请求?目前我们实现方...

  • 优雅的调用RESTful API

    代码量越少越优雅,实现越简单越优雅,下面介绍如何优雅的实现API调用。 1 单资源访问 下面以华为云IoT平台提供...

  • WKWebView POST请求

    这一文章介绍如何通过类目让WKWebView优雅的实现POST请求,为啥说是优雅: 实现POST请求 实现原理是通...

  • 实现一个 Fake Client 用于单元测试

    Client 接口 涉及的数据结构: Fake Client 实现

  • 一些js小技巧

    1、如何优雅的取随机字符窜 2、如何使用位运算符优雅的取整 3、如何用正则优雅的实现金钱格式化 4、如何最佳让两个...

  • golang fasthttp上传文件client和server

    server端实现: client端实现:

  • 如何优雅

    如何优雅?这是一个很流行,也很值得追问的问题。 如何优雅地读书?如何优雅地喝茶?如何优雅地跑步?如何优雅地追求女神...

  • 简短优雅地利用js实现 sleep 函数

    简短优雅地实现 sleep 函数 很多语言都有 sleep 函数,显然 js 没有,那么如何能简短优雅地实现这个方...

  • js 骚操作

    1、如何优雅的取随机字符串 2、如何优雅的取整 3、优雅的金钱格式化 4、两个属性换值 5、实现深拷贝

网友评论

      本文标题:如何优雅的实现一个Client

      本文链接:https://www.haomeiwen.com/subject/fbgrultx.html