如何优雅的实现一个Client

2021-07-14  本文已影响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设计.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的设计、开发工作已经完成,使用者可以使用下面的两种方式根据Gotify Server的实际情况创建GotifyClientConfig实例。

接下来让我们一起设计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的组织形式可以分为两类:

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

选择使用分类式的方式组织API,那么如何设计这些分类的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过程可以分为四步:

下面以获取运行在本地监听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

上一篇 下一篇

猜你喜欢

热点阅读