如何优雅的实现一个 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 最小化开放的接口,接口的内部实现应当对使用者隐藏。
测试完备原则:完备的单元测试、集成测试。
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 需要开放的配置:
Client 开放的配置已经定义完成,使用者可以根据自己 Gotify Server 的情况实现该接口,但是要求使用者提供一个完整的实现,这对于使用者来说非常的不方便。我们需要思考解决使用者如何简单方便的创建 Client 配置 ,下面我们从简单易用性出发,考虑提供几个方便使用者创建配置的工具。
2.1.1 通过工厂方法创建配置
Client 一共三个配置参数,其中两个必选参数,一个可选参数,参数的数量不是很多,因此我们可以考虑提供两个工厂方法(一个包含三个参数,一个包含两个参数),参数的位置按照 scheme:host:port 的位置进行排列(每个细节都以使用者为中心设计),因为这和三个参数在标准 Uri 中的顺序一致,方便使用者记忆。
方法 1:拥有三个参数的工厂方法
方法 2:拥有两个参数的工厂方法
我们已经定义创建 Client 配置的工厂方法,下一个需要我们思考的问题是:这些工厂方法应该被组织到哪个类中?在本例子中我们将工厂方法组织到GotifyClientConfig
接口中,以下两点支撑我们做出这个决定:1、目前为止我们开放给使用者的只有GotifyClientConfig
接口,如果将方法组织到新的类中,比如我们创建一个工厂类GotifyClientConfigFactory
,那么这会增加使用的负担,每增加一个开放的类,使用者的学习负担都在增加;2、Java 8 之后的版本从语言层面支持我们这么做,interface 提供一些工厂方法在很多场景下都很适用。因此我们将工厂方法组织到GotifyClientConfig
接口中,修改后的GotifyClientConfig
接口如下:
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 的逻辑示意图如下:
2.2.1 定义 Client 的共同行为
所有 Client 的共同行因为 Client 不同而有所不同,在本文中所有的 Client 的共同行为是都支持关闭。在 Java 中可以使用接口和抽象类定义允许有多个实现的类型,使用接口是比抽象类更佳的优秀实践。如果只是想定义一个类型,那么标记接口(不包含任何方法的接口)是一种不错的选择。
2.2.2 定义操作不同资源的 Client
AppClient:
MessageClient:
操作资源的 Client 已经定义完整,但是先不要着急去实现这些 Client,下一步我们要解决的问题是如何方便的创建这些 Client。
2.2.3 使用工厂方法创建 Client
按照上文对方案的介绍,GotifyClient
将负责创建AppClient
、MessageClient
等操作资源的 Client,下面是 GotifyClient 的定义:
所有操作资源的 Client,比如AppClient
都将由GotifyClient
负责创建。对外的接口已经定义清楚,下面来分别实现AppClient
、MessageClient
和GotifyClien
t,注意这些 Client 的实现都不对使用者开放。
2.2.4 AppClient 和 MessageClient 的实现
AppClient
和MessageClient
的实现需要满足我们对 Client 的设计要求:Client 无状态,Client 线程安全。
AppClient 的实现
MessageClient 的实现
2.2.5 实现 GotifyClient
GotifyClient
的实现需要满足我们对 Client 的设计要求:Client 是单例,Client 的创建方式是统一的。
2.2.6 提供工厂方法创建 GotifyClient
我们参考GotifyClientConfig
的实现方式,在GotifyClient
接口中添加一个静态方法,修改后的GotifyClient
接口如下:
3 GotifyClient 的使用样例
根据上面设计,我们使用GotifyClient
过程可以分为四步:
根据需要确定需要哪些资源,需要使用哪些 Client?
获取 Gotify Server 的基本信息,创建
GotifyCLientConfig
创建
GotifyClient
使用 GotifyClient 创建需要的操作资源的 Client
下面以获取运行在本地监听 6875 端口的 Gotify 所有 Application 为例,详细讲解如何使用我们上面设计的 Client。
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
版权声明: 本文为 InfoQ 作者【Apologize】的原创文章。
原文链接:【http://xie.infoq.cn/article/66ce488ec2722bd3c46b6b0fb】。文章转载请联系作者。
评论