Skip to main content

Attitude is altitude

如何使用 Go 语言写区块链的 SDK (一)

“ 最近在使用 Go 语言写一个区块链的 SDK,从设计到上线总结了其中的一些问题以及思考,如果你也在写 SDK 或者要写 SDK,希望这篇文章可以帮到你。这篇文件不会区分是否是区块链的 SDK,下一篇文章主要写区块链的 SDK 与普通系统 SDK 的一些区别以及相关思考”

当写一个 SDK 时,第一需要考虑的问题应该是这个 SDK 的功能,或者说这个 SDK 是给什么样的用户来用,或者用户使用这个 SDK 能做什么。弄明白了这个问题后才可以开始动手。

一个系统的接口肯定是知道的,但有些情况并不是一个接口就能够完成一个用户的操作或者需求,此时需要系统的多个接口组合调用才可以,同时在调用多个接口时,有很多逻辑是需要在客户端完成的,那么接口调用以及用户的逻辑就需要在 SDK 中完成。所以简单来说,一个 SDK 的基础功能就是系统的接口调用与用户的部分逻辑(当然这只是我抽象出来的,实际情况 SDK 有很多种)。

我个人把 SDK 的出生过程总结如下:

  1. SDK 功能确认;
  2. 系统接口逻辑确认;
  3. SDK 接口设计;
  4. SDK 架构设计;
  5. SDK 使用文档或者示例代码;
  6. 测试;

接下来就分这七步来聊一聊如何开发一个易用、可靠的 SDK。

1. SDK 功能确认

某个系统的 SDK 会有官方的 SDK 也有其他人贡献的 SDK,比如有的企业或者开发者觉得官方提供的 SDK 功能不全,或者使用不方便,满足不了现在的需求.

而且官方很大几率不会再开发一个专门的 SDK 来满足个别的需求,所以开发者有可能根据系统的接口来开发或者在官方 SDK 基础上添加功能。

对于这种现象的出现,我觉得主要原因在于官方的 SDK 在设计之初就是存在问题的,或者说 SDK 的功能确认是错误。

系统可能有几十个接口,但是对于用户有用的或者最常用的可能只有一半或者三分之一,所以 SDK 并不需要把所有的接口都支持,而是要把核心的功能筛选出来,这一点并不与上面提到的功能确认存在问题。

所以该如何确认 SDK 的功能呢?如果系统功能比较少,那就很简单,即使把所有可以暴露的接口都支持也是没问题的,但是系统接口比较多时,就需要做减法。

为什么不是做加法呢?如果做加法你可能就会发现,百分之九十以上的接口都需要支持,这个并不是最好的选择。

从系统的功能或者接口做减法,把不能在 SDK 中调用的,系统后期可能去掉的(向前兼容),以及可能存在问题的或者即使给了用户,用户也不会使用的去掉,然后再把剩下的功能梳理出来,看下能满足用户的哪些需求,最终确定一个功能集,这就是 SDK 的最终功能。

2. 系统接口逻辑确认

为什么我把这部分放在 SDK 功能确认之后呢?首先在写 SDK 时我们要记得一个点,就是系统的接口设计并不是百分百正确或者完美的。

当我们确定了 SDK 功能集时,我们就会知道这些功能是否满足需求,如果不满足需求并且系统的接口同样不能满足,那么就要考虑其他方式了。

如果确定的 SDK 功能集满足需求,我们还要考虑一个点,就是在从功能的角度出发时,系统的接口设计是否真的合理。

在这些问题都确定后,目前对于 SDK 的功能算是真的确定下来了。

3. SDK 接口设计

功能确定并不意味着接口确定了,接口的设计是 SDK 中最为关键的一步。

任何系统不仅仅是为了解决某些问题,同时系统的易用性也决定着其能走多远。其中 SDK 是否好用也起着关键性的作用。

试想当你拿到一个新的 SDK,一个功能需要调用 SDK 的多个接口才能完成,而且其中还有很多逻辑需要自己处理,那么第一反应肯定是:什么垃圾,还不如自己写了。

3.1. 接口尽量简洁

一个接口能完成的事尽量不要用两个接口。说白了就是封装的能力。

SDK 可能支持很多功能,甚至有着三四十个接口。但是一定要保证每个接口有着独立的能力。用户在使用时不至于看到这么多接口时不知道该使用哪个。

但是,有很多接口我们不能够帮用户来做决定,很多情况确实需要用户来组合调用多个 SDK 的接口才能完成一件事情,这个并不冲突。

假设 SDK 现在有 A 接口与 B 接口,每个接口能完成一个功能,同时有一个功能需要先调用 A 接口,然后客户端处理用户逻辑,再调用 B 接口。那么此时我们就可以考虑再增加一个接口 C。这个 C 接口的功能就是先调用 A 接口,然后处理用户逻辑,再调用 B 接口,也就是接口是可以灵活组合的。对于这个功能来说,使用者可以自己选择使用 C 接口还是 A 、B 组合。因为用户逻辑我们是不确定的,但是很多场景下,这部分的用户逻辑是相同的,换句话说 C 接口中实现的用户逻辑能满足大多数场景,不能满足的场景就只能使用 A、B 组合方式。

简单来说就是 SDK 给了用户更多的选择,如果某个接口不能满足某些需求,那么可以调用其他接口组合使用,可以想象成一棵树的形式。

3.2. 接口向后兼容

除此之外,还有一个点需要考虑,那就是系统的升级。我们不能保证每次系统升级后,SDK 都能及时更新。万一出现这个情况,那系统升级还有什么意义呢?

所以在设计 SDK 时,需要考虑是否可以更灵活。

假如系统升级都是向前兼容的,同时很多接口的调用方式是一致的,例如在 grpc 中,某个接口参数中有一个字段是 type,当前系统支持的 type 有 A、B、C 三种,同时 SDK 接口封装时对每个 type 都封装成一个接口,使用 SDK 时可以通过接口名字来区分当前的 type,而不是将 type 当做参数。

此时系统升级支持了 C、D 的 type,但是 SDK 还没有支持。那用户岂不是不能使用这个功能了。

所以在设计 SDK 和需要考虑到这种情况,可以再增加一个接口,或者说一层接口。SDK 最外面有一层接口为 client 层,用户可以直接调用,其中某些参数是写好的。client 层在调用 request 层,request 层也可以暴露给用户。此时就相当于,百分之八十的功能可以直接调用 SDK 的一个接口完成,一些高级功能需要调用多个接口组合使用,对于 C、D 的 type 场景可以理解为高级功能。

3.2. 接口向前兼容

SDK 每次升级最主要的就是向前兼容,除非这是一次重构或者不兼容升级,我们可以在 go.mod 文件中声明为 v2、v3 版本。

go 的 grpc 大家应该都用过,很多 go 语言的 SDK 接口方面设计的都有些类似,比如参数都是:

 func MyFunc(a string, b int, opts ...interface){}

opts 参数作为变长形式可以向前兼容。

所以在设计接口时就需要考虑到:如果功能有增加或者删除或者修改,如何让 SDK 最大程度保持向前和向后兼容。

4. SDK 架构设计

在 3.2 中说过接口可以分层,每一层的接口都有不同的分工,同时可以多层接口组合使用,这个也是架构设计的重要部分。

看过很多 SDK 的大致架构,最终我个人觉得决定 SDK 架构设计的主要有两点,第一是业务形态,第二是 SDK 的功能集。

这里的业务形态可以理解为系统架构是怎样的,比如 fabric,有多种节点角色组成,其交易流程就是至关重要的一部分,那么 SDK 的架构首先就要考虑这种情况。再说以太坊,SDK 非常简单,只要能发送交易、查询信息即可。

再说 SDK 的功能集,确定了功能后,根据功能的多少、实现的复杂度等,再去设计一个合适的架构。

架构设计和设计模式在 SDK 中是比较紧密的。尤其在功能比较少,比较简单的 SDK,可以先选择一个合适的设计模式,这种简单场景更多使用的设计模式有 builder、factory 等,这种不仅实现简单,而且也能很好地维护。

在功能比较复杂的 SDK,首先需要抽象好接口,根据业务、接口划分功能模块以及对应接口,之后在具体的实现过程中再选择设计模式,这是与简单 SDK 的一个区别。

5. SDK 使用文档或者示例代码

这一部分更多的是项目的管理。用户拿到 SDK 时怎么入手是一个关键问题,千万不要因为上手难度或者复杂度将用户拒之门外。

当你拿到一个新的 SDK 或者项目最新做的是什么?我相信大多数都是根据文档先跑一个简单的例子,先跑成功。然后再通过文档或者示例代码来使用更多功能。

所以文档和示例代码非常重要。

文档中包括的内容除了项目的介绍,还需要有一个简单的示例,或者接口文档。在使用 Go 语言开发时,个人建议可以使用 Go doc 来管理接口文档以及示例代码。

文档的第一个目的是能让用户第一次使用时,可以顺利完成第一个简单功能。其中包括 SDK 需要的环境,比如 golang 版本等。

这一部分所说的文档并不只是项目的 README 文档,README 只是一个入口,它更多的作用是用户通过这个文档可以找到对应的文档或者其他需要的东西,包括其他文档、项目、代码等等。

再说一下如何写示例代码。SDK 的功能一定会有低级功能和高级功能的区分,或者常用功能和非常用功能区分。所以很多情况可以在项目中创建一个 example 目录,其中就包括各种示例代码,尤其是高级功能,需要多个接口组合使用,同时在示例代码中写好对应的注释,用户在使用时可以照葫芦画瓢。

6. 测试

测试包括单测和功能测。首先说单测,很多时候在编码是需要考虑单测,甚至为单测可以开一些口子或者埋一些钩子。特别是在有网络请求时。我们可以使用 monkey patch、mock 等方式提高我们的覆盖率,但这不是目的。

我认为单测需要做到这几个点:测试程序某些场景是否 panic,参数检查是否足够,构造的数据是否正确,返回的数据是否正确等。这样每次跑单测时都可以放心之前的逻辑。

还有一点就是不要为了覆盖率写单测,这只会浪费时间。

写好了 SDK 我们如何测试功能是否符合预期呢?比如区块链中的转账功能,这个功能的检查点有转账前两个账户余额,转账后两个账户余额,转账的金额,是否有手续费,交易是否成功,交易中的数据是否和 SDK 中的一致等。所以在测这些功能时我们可以采用单测的形式,在交易前后把相关数据查询,交易后再判断是否符合预期。


写在最后

本文主要是我个人在工作过程中的一些总结,仁者见仁智者见智,如果你有计划写 SDK 或者正在写,希望可以对你有帮助。下一篇文章会聊一下关于区块链系统,应该如何来写 SDK。

comments powered by Disqus