【每周一知】Rust中的三种设计模式

我们都知道如何在 Rust 中初始化一个简单的结构体,但面对复杂结构体时,我们应该选择怎样的方式去初始化它呢?在分析 Substrate 代码的过程中,学习到一些复杂结构体初始化的设计模式,本文整理总结如下:

new模式

builder模式

Default模式

new模式

初始化结构体的第一种模式,就是在结构体中使用以下签名声明一个new函数。

pub fn new(param1, param2 : T) -> Self {

Self {

// 初始化

}

}

这种是最常见的方式。它非常简单,对于简单的结构体也很适用。比如在 Substrate 的 primitives/runtime/src/offchain/http.rs 中有如下代码:

pub struct Header {

name: Vec,

value: Vec,

}

impl Header {

/// Creates new header given it's name and value.

pub fn new(name: &str, value: &str) -> Self {

Header {

name: name.as_bytes().to_vec(),

value: value.as_bytes().to_vec(),

}

}

}

但是,随着结构体复杂性的增加,它开始出现问题。比如:

impl Client where

B: backend::Backend,

E: CallExecutor,

Block: BlockT,

{

/// Creates new Substrate Client with given blockchain and code executor.

pub fn new(

backend: Arc,

executor: E,

build_genesis_storage: &dyn BuildStorage,

fork_blocks: ForkBlocks,

bad_blocks: BadBlocks,

execution_extensions: ExecutionExtensions,

_prometheus_registry: Option,

) -> sp_blockchain::Result {

}

如果要在其它文件或 crate 中构造此结构体 Client,除非记住 new函数的签名或借助IDE的提示,否则可能会不记得参数列表。而且new模式还有个问题,就是不适合用在供外部使用的API实现上,因为如果结构体增加或减少一个字段,所有调用该new函数的地方都要做相应的修改。

builder模式

上面提到的问题,可以使用builder模式来解决。这是初始化结构体的第二种模式,就是为结构体使用以下签名实现一个build函数。

impl Struct {

pub fn build(self) -> Struct {

// 初始化

Struct {

}

}

}

在分析 Substrate 启动流程代码的过程中,有一个很重要的类型ServiceBuilder,通过构建它来启动整个区块链服务所需要的各种组件。代码在client/service/src/builder.rs中:

pub struct ServiceBuilder

TExPool, TRpc, Backend>

{

config: Configuration,

pub (crate) client: Arc,

backend: Arc,

tasks_builder: TaskManagerBuilder,

keystore: Arc>,

fetcher: Option,

select_chain: Option,

pub (crate) import_queue: TImpQu,

finality_proof_request_builder: Option,

finality_proof_provider: Option,

transaction_pool: Arc,

rpc_extensions: TRpc,

remote_backend: Option>>,

marker: PhantomData<(TBl, TRtApi)>,

background_tasks: Vec<(&'static str, BackgroundTask)>,

}

impl ServiceBuilder<(), (), (), (), (), (), (), (), (), (), ()> {

/// Start the service builder with a configuration.

pub fn new_light(

config: Configuration,

) -> Result

...

>, Error> {

...

Ok(ServiceBuilder {

...

})

}

}

impl

ServiceBuilder<

...

> where

...

{

/// Builds the service.

pub fn build(self) -> Result

...

>, Error>

where TExec: CallExecutor,

{

...

Ok(Service {

...

})

}

}

从build函数的签名可以看出builder模式不需要指定所有内容来构建结构体ServiceBuilder。我们看如何使用它,代码在bin/node/cli/src/service.rs中:

/// Builds a new service for a light client.

pub fn new_light(config: Configuration)

-> Result {

type RpcExtension = jsonrpc_core::IoHandler;

let inherent_data_providers = InherentDataProviders::new();

let service = ServiceBuilder::new_light::(config)?

.with_select_chain(|_config, backend| {

Ok(LongestChain::new(backend.clone()))

})?

...

.build()?;

Ok(service)

}

由于这个结构体相当的复杂,它的构建方法build函数有503行。这说明了builder模式的一个大缺点:非常长。行数是new模式的几倍。

Default模式

这是初始化结构体的第三种模式,就是先为结构体实现Default,实现default函数,然后再为其实现一个类似build的函数。

impl Default for Struct {

fn default() -> Self {

// 初始化部分

}

}

impl Struct {

pub fn build(self) -> Struct {

// 初始化

Struct {

}

}

}

在 Substrate 中有个投票规则的构建器VotingRulesBuilder,它使用一组规则来逐步约束投票。代码在client/finality-grandpa/src/voting_rule.rs中:

pub struct VotingRulesBuilder {

rules: Vec>>,

}

impl Default for VotingRulesBuilder where

Block: BlockT,

B: HeaderBackend,

{

fn default() -> Self {

VotingRulesBuilder::new()

.add(BeforeBestBlockBy(2.into()))

.add(ThreeQuartersOfTheUnfinalizedChain)

}

}

impl VotingRulesBuilder where

Block: BlockT,

B: HeaderBackend,

{

/// Return a new `VotingRule` that applies all of the previously added

/// voting rules in-order.

pub fn build(self) -> impl VotingRule + Clone {

VotingRules {

rules: Arc::new(self.rules),

}

}

}

这段代码看起来非常类似于builder模式,但是与其相比,我们大大降低了build代码的长度。如果需要进行一些默认的操作,则可以在default()函数中进行。关于使用,我们可以在bin/node/cli/src/service.rs中看到如下的代码:

voting_rule: grandpa::VotingRulesBuilder::default().build(),

结语

在开发过程中,我们可以根据实际需要,灵活使用这三种设计模式。

Rust 相关文章

Rust 为什么会有两种字符串类型 String 和&str?

Rust 异步入门

理解智能指针 Box

如何理解 Rust 的默认线程安全?

如何理解 Rust 中的可变与不可变?

Rust 语言精简手册

Rust 学习:如何解读函数签名?