--- 摄于 2017 年 9 月 藏川线前段
cita-tool 在刚开始的时候,很大程度上借鉴了我另外一个库 InfluxdbClinet-rs 的实现,但在实现过程中,越来越领悟到 Rust trait 的作用,也就越来越偏向于使用 trait、trait object、generic,实现更高级的抽象。
influxdbclient 这个库是我刚接触 Rust 三个月左右开始写的一个库,当时主要是在写 Python 和一点点 C。业余时间接触 Rust 三个月之后,总是要走出第一步,造一个轮子,刚好当时项目上需要使用 influxdb,并且 Rust 这边社区的库作者说他要去写 Go 了,并不会经常维护。于是,参照他的实现和官方的 Python 库,从头开始写起。
0.2 版本里面,这个库的实现里面有定义一些 trait,如下:
pub struct InfluxdbClient {
host: String,
db: String,
username: String,
passwd: String,
read_timeout: Option<u64>,
write_timeout: Option<u64>,
}
pub trait InfluxClient {
/// Query whether the corresponding database exists, return bool
fn ping(&self) -> bool;
/// Query the version of the database and return the version number
fn get_version(&self) -> Option<String>;
/// Write a point to the database
fn write_point(&self, point: Point, precision: Option<Precision>, rp: Option<&str>) -> Result<bool, error::Error>;
...
}
impl InfluxClient for InfluxdbClient {
...
}
在这个库升级到 0.3 版本的时候,我把所有自己定义的 trait 都删掉了,让实现和类型完全绑定在一起。当时的想法其实很简单,trait 的定义在库中完全是多此一举,导致了很多没有必要的引入,整个库非常简单,引入 Client,就能与 influxdb 进行交互,并不需要 trait。
但是,当你真的想要在 Rust 里面做更好的抽象的时候,你会发现,trait 是必须要了解并掌握的一个概念。在 Rust 标准库里面,定义了大量 trait,用来统一抽象某些行为,比如 Read、Write 抽象 IO 的行为。
在 Rust 中,trait 承担了很多责任:
trait 并不是一种类型,但 trait object 是一种类型,它与泛型的区别在于编译器的处理,泛型属于静态分派,trait object 属于动态分派。这两种行为都是在 Rust 里面实现 “多态” 的重要手段。
就如同比较有代表性的 Python 里面实现多态:
from abc import ABCMeta, abstractmethod
class Animal:
__metaclass__ = ABCMeta
def __init__(self):
pass
@abstractmethod
def name(self):
pass
@abstractmethod
def call(self):
pass
class Dog(Animal):
def __init__(self):
self.name = "Dog"
self.call = "汪"
def name(self):
print(self.name)
def call(self):
print(self.call)
class Cat(Animal):
def __init__(self):
self.name = "Cat"
self.call = "喵"
def name(self):
print(self.name)
def call(self):
print(self.call)
而这些在 Rust 里面相对应来实现:
trait Animal {
fn name(&self);
fn call(&self);
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn name(&self) {
println!("Dog");
}
fn call(&self) {
println!("汪");
}
}
impl Animal for Cat {
fn name(&self) {
println!("Cat");
}
fn call(&self) {
println!("喵");
}
}
在 Rust 里面,就连线程安全的约束都是由两个 trait 去约束的,Rust 语言本身并不知晓 “线程” “并发” 是什么,而是抽象出一些高级的 trait,用来描述类型在并发环境下的特性:
pub fn spawn<F, T>(f: F) -> JoinHandle<T> where
F: FnOnce() -> T, F: Send + 'static, T: Send + 'static
{
Builder::new().spawn(f).unwrap()
}
最早,整个项目只有一个 trait,用来定义 jsonrpc 接口,而且所有的类型都写死,扩展的可能性很低:
pub trait ClientExt {
/// net_peerCount: Get network peer count
fn get_net_peer_count(&mut self, url: &str) -> u32;
/// cita_blockNumber: Get current height
fn get_block_number(&mut self, url: &str) -> Option<u64>;
/// cita_sendTransaction: Send a transaction return transaction hash
fn send_transaction(
&mut self,
url: &str,
code: &str,
address: &str,
current_height: u64,
) -> Result<String, String>;
/// eth_getTransactionReceipt: Get transaction receipt
fn get_transaction_receipt(&mut self, url: &str, hash: &str) -> JsonRpcResponse;
...
}
之后为了统一处理整个接口,修改为使用关联类型加泛型的模式:
pub trait ClientExt<T, E>
where
T: serde::Serialize + serde::Deserialize<'static> + ::std::fmt::Display,
E: Fail,
{
/// Rpc response
type RpcResult;
/// net_peerCount: Get network peer count
fn get_net_peer_count(&mut self, url: &str) -> Self::RpcResult;
/// cita_blockNumber: Get current height
fn get_block_number(&mut self, url: &str) -> Self::RpcResult;
/// cita_sendTransaction: Send a transaction return transaction hash
fn send_transaction(
&mut self,
url: &str,
code: &str,
address: &str,
current_height: Option<u64>,
quota: Option<u64>,
value: Option<u64>,
blake2b: bool,
) -> Self::RpcResult;
/// eth_getTransactionReceipt: Get transaction receipt
fn get_transaction_receipt(&mut self, url: &str, hash: &str) -> Self::RpcResult;
...
}
后来发现关联类型实际上并没有达到我想要的泛型扩展,同时因为参数太多,定义了一个类型 TransactionOptions
,最后改成:
pub trait ClientExt<T, E>
where
T: serde::Serialize + serde::Deserialize<'static> + ::std::fmt::Display,
E: Fail,
{
/// peerCount: Get network peer count
fn get_peer_count(&self) -> Result<T, E>;
/// blockNumber: Get current height
fn get_block_number(&self) -> Result<T, E>;
/// sendTransaction: Send a transaction and return transaction hash
fn send_raw_transaction(&mut self, transaction_option: TransactionOptions) -> Result<T, E>;
/// getBlockByHash: Get block by hash
fn get_block_by_hash(&self, hash: &str, transaction_info: bool) -> Result<T, E>;
/// getBlockByNumber: Get block by number
fn get_block_by_number(&self, height: &str, transaction_info: bool) -> Result<T, E>;
/// getTransactionReceipt: Get transaction receipt
fn get_transaction_receipt(&self, hash: &str) -> Result<T, E>;
...
}
这里关联类型因为不支持下面这种写法,被丢弃了:
pub trait ClientExt<T, E>
where
T: serde::Serialize + serde::Deserialize<'static> + ::std::fmt::Display,
E: Fail,
{
/// Rpc response
type RpcResult = Result<T, E>;
/// peerCount: Get network peer count
fn get_peer_count(&self) -> Self::RpcResult;
/// blockNumber: Get current height
fn get_block_number(&self) -> Self::RpcResult;
/// sendTransaction: Send a transaction and return transaction hash
fn send_raw_transaction(&mut self, transaction_option: TransactionOptions) -> Self::RpcResult;
/// getBlockByHash: Get block by hash
fn get_block_by_hash(&self, hash: &str, transaction_info: bool) -> Self::RpcResult;
/// getBlockByNumber: Get block by number
fn get_block_by_number(&self, height: &str, transaction_info: bool) -> Self::RpcResult;
/// getTransactionReceipt: Get transaction receipt
fn get_transaction_receipt(&self, hash: &str) -> Self::RpcResult;
...
}
cli 在实现过程中,增加了对 CITA 系统合约的支持,用来方便用户更加轻松地调用 CITA 的系统合约,最初,所有合约的实现都是一个 trait 定义的:
/// High degree of encapsulation of system contract operation
pub trait ContractExt: ClientExt<JsonRpcResponse, ToolError> {
/// Get node status
fn node_status(&mut self, url: &str, address: &str) -> Self::RpcResult {
let code = format!(
"{function}{complete}{param}",
function = "0x645b8b1b",
complete = "0".repeat(24),
param = remove_0x(address)
);
self.call(
url,
None,
"00000000000000000000000000000000013241a2",
Some(&code),
"latest",
)
}
...
}
因为合约地址散落在每个 trait 方法里面,这样维护起来非常难受,如果有笔误,几乎就是在大海捞针,并且没有很好得区分每个系统合约,杂乱无章。
接下来,我们尝试为每个系统合约定义一个 trait,用来表达该合约的调用行为,以 node manage 合约为例:
/// High degree of encapsulation of system contract operation
pub trait NodeManageExt: ClientExt<JsonRpcResponse, ToolError> {
/// contract address
const address;
/// Get node status
fn node_status(&mut self, url: &str, address: &str) -> Self::RpcResult {
let code = format!(
"{function}{complete}{param}",
function = "0x645b8b1b",
complete = "0".repeat(24),
param = remove_0x(address)
);
self.call(
url,
None,
Self::address,
Some(&code),
"latest",
)
}
...
}
将 address 定义为每个 trait 的关联 const,这样可以把合约地址统一管理在 trait 的关联类型上,但是,这里又会出现一个问题,把多个合约 trait 同时对同一个类型进行实现的时候,会碰到命名空间冲突的问题,因为都是 const address
,于是这个版本直接没在 git 仓库历史里面出现过就被 pass 了。
接着,为了解决命名空间污染的问题,决定把每个合约都做成一个 struct
类型,每个合约自己的类型由自己的 trait 实现,同时统一抽象一个对于系统合约来说底层的抽象行为 trait:
统一抽象 trait:
/// Call/SendTx to a contract method
pub trait ContractCall {
/// Rpc response
type RpcResult;
/// Prepare contract call arguments
fn prepare_call_args(
&self,
name: &str,
values: &[&str],
to_addr: Option<Address>,
) -> Result<(String, String), ToolError>;
/// SendTx a contract method
fn contract_send_tx(
&mut self,
url: &str,
name: &str,
values: &[&str],
to_addr: Option<Address>,
blake2b: bool,
) -> Self::RpcResult;
/// Call a contract method
fn contract_call(
&self,
url: &str,
name: &str,
values: &[&str],
to_addr: Option<Address>,
) -> Self::RpcResult;
}
每个具体合约 trait:
/// Group System Contract
pub trait GroupExt: ContractCall {
/// Create a ContractClient
fn create(client: Option<Client>) -> Self;
/// Call a group query function
fn group_query(
&self,
url: &str,
function_name: &str,
values: &[&str],
address: &str,
) -> Self::RpcResult;
...
}
重复的 impl
行为,就通过实现过程宏 derive 进行进一步压缩,有关过程宏实现可以看这里。
接下来,为了提高扩展性,将有关系统合约的 trait 实现也写成了泛型模式,整个实现就显得更加灵活:
/// Call/SendTx to a contract method
pub trait ContractCall<R, E>
where
R: serde::Serialize + serde::Deserialize<'static> + ::std::fmt::Display,
E: Fail,
{
/// Prepare contract call arguments
fn prepare_call_args(
&self,
name: &str,
values: &[&str],
to_addr: Option<Address>,
) -> Result<(String, String), E>;
/// SendTx a contract method
fn contract_send_tx(
&mut self,
name: &str,
values: &[&str],
quota: Option<u64>,
to_addr: Option<Address>,
) -> Result<R, E>;
/// Call a contract method
fn contract_call(
&self,
name: &str,
values: &[&str],
to_addr: Option<Address>,
height: Option<&str>,
) -> Result<R, E>;
/// Call a contract method with a to_address
fn contract_call_to_address(
&self,
function_name: &str,
values: &[&str],
address: &str,
height: Option<&str>,
) -> Result<R, E> {
let address = Address::from_str(remove_0x(address)).unwrap();
self.contract_call(function_name, values, Some(address), height)
}
}
其实整个库的真正业务相关的逻辑,都在这些 trait 的折腾上面了。如果对我本身 client 的实现不爽,就可以根据这些泛型 trait 实现自己的一套代码,只需要引用这些 trait 就可以了。要说现在对这个库还有不满意的地方,就是 rpctypes
的定义了,因为确实没有对整个 input 和 output 做整理,所以这些 types 都是一些很简单的开放式定义,所有的 field 都是 pub 的,不限制任何使用。
回想整个过程,确实不易,说了这么多 sdk 的实现,下面一篇将讲解 cli 执行程序的实现。
请登录后评论
评论区
加载更多