前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >真实世界的设计模式 | 外观模式(Facade Pattern)

真实世界的设计模式 | 外观模式(Facade Pattern)

作者头像
张汉东
发布2021-05-11 11:05:02
9620
发布2021-05-11 11:05:02
举报
文章被收录于专栏:Rust 编程Rust 编程

Facade(外观)模式

Rust 中最常用的设计模式是哪个?答案是,外观模式。

为什么这么说?看完本文就明白了。

一句话介绍

Facade,中文术语叫「外观模式」,也叫「门面模式」。在经典设计模式中,归为结构型(Structural)模式分类,因为这种模式用于帮助构建结构。它可以为程序库、框架或其他复杂情况提供一个简单的接口。

解决了什么问题

在软件开发中,有时候要处理很多同类型的业务,但具体处理方式却不同的场景。因此,建立一个「门面」来达到统一管理和分发的目的。

Facade 模式,帮忙建立了统一的接口,使得调用复杂的子系统变得更加简单。因为 Facade 模式只包括应用真正关心的核心功能。

如何解决

心智图:

代码语言:javascript
复制
                 +------------------+
+-------+        |                  |         +---------------+
|       |        |                  |         |  additional   |
|client +------> |     facade       +-------> |  facade       |
+-------+        |                  |         |               |
                 |                  |         |               |
                 +--+----+------+--++         +---------------+
                    |    |      |  |
           +--------+    |      |  +--------+
           |          +--+      +-+         |
           |          |           |         |
           v          |           v         v
       +---+---+  +---v--+   +----+--+  +---+----+
       |       |  |      |   |       |  |        |
       | system|  |system|   |system |  | system |
       |       |  |      |   |       |  |        |
       +-------+  +------+   +-------+  +--------+

真实案例

实现方式:

Rust 中的 门面模式 实现有三类:

  • 模块 re-export:
    • Rust libstd reexport libcore[2]
    • Futures-rs[3]
  • 条件编译:tikv/tikv[4]
  • 利用 「类型」 和 「Trait」:
    • MachBackend[7]
    • LowerBackend[8]
    • log[5]
    • mio[6]
    • cranelift

模块 Re-Export

模块 Re-Export 是重导出功能。

比如,现在有如下模块层级:

代码语言:javascript
复制
src/
    - lib.rs
    - module/
        -- mod.rs
        -- submodule/
            --- mod.rs

Rust 允许你将 潜入到最深处的那个模块 submodule 里定义的函数,使用重导出功能,变成整个库的「门面」接口。

代码语言:javascript
复制
// in module/submodule/mod.rs
pub fn goodbye(){}

// in lib.rs
pub use module::submodule::goodbye;

那么在使用这个库(假设叫 hello)的时候,只需要使用 hello::goodby就可以使用这个函数。

这种方式在 Rust 的世界大量使用。比如 标准库 很多接口是重导出了 核心库 的 API。

在 Furutes-rs 中也有很多重导出。

条件编译

条件编译也是一种 门面模式。

比如在 TiKV 中,使用 条件编译 和 features 来支持多种内存分配器。

代码语言:javascript
复制
#[cfg(all(unix, not(fuzzing), feature = "jemalloc"))]
#[path = "jemalloc.rs"]
mod imp;
#[cfg(all(unix, not(fuzzing), feature = "tcmalloc"))]
#[path = "tcmalloc.rs"]
mod imp;
#[cfg(all(unix, not(fuzzing), feature = "mimalloc"))]
#[path = "mimalloc.rs"]
mod imp;
#[cfg(not(all(
    unix,
    not(fuzzing),
    any(feature = "jemalloc", feature = "tcmalloc", feature = "mimalloc")
)))]
#[path = "system.rs"]
mod imp;

实际上并不存在 imp 模块,通过不同的 cfg 判断,对应不同的 path,从而选择相应的模块:jemalloc.rs/tcmalloc.rs/mimalloc.rs/system.rs。而 imp 模块就是一个「门面」。

利用 类型 和 Trait

第三种方式,就是常规的 利用 类型 和 trait 来实现门面模型。

最典型的就是官方出的 log 库。

代码语言:javascript
复制
pub trait Log: Sync + Send {
    /// Determines if a log message with the specified metadata would be
    /// logged.
    ///
    /// This is used by the `log_enabled!` macro to allow callers to avoid
    /// expensive computation of log message arguments if the message would be
    /// discarded anyway.
    fn enabled(&self, metadata: &Metadata) -> bool;

    /// Logs the `Record`.
    ///
    /// Note that `enabled` is *not* necessarily called before this method.
    /// Implementations of `log` should perform all necessary filtering
    /// internally.
    fn log(&self, record: &Record);

    /// Flushes any buffered records.
    fn flush(&self);
}

官方通过指定这个 trait ,来创建了一个 「门面」。其他 log 库,比如 env_log / sys_log 等其他 log 库,都可以实现 Log trait。

代码语言:javascript
复制
// env_log
impl Log for Logger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        self.filter.enabled(metadata)
    }

    fn log(&self, record: &Record) {
        if self.matches(record) {
            // ignore many codes
        }
    }

    fn flush(&self) {}
}


// syslog

impl Log for BasicLogger {
  fn enabled(&self, metadata: &Metadata) -> bool {
    true
  }

  fn log(&self, record: &Record) {
    //FIXME: temporary patch to compile
    let message = format!("{}", record.args());
    let mut logger = self.logger.lock().unwrap();
    match record.level() {
      Level::Error => logger.err(message),
      Level::Warn  => logger.warning(message),
      Level::Info  => logger.info(message),
      Level::Debug => logger.debug(message),
      Level::Trace => logger.debug(message)
    };
  }

  fn flush(&self) {
      let _ = self.logger.lock().unwrap().backend.flush();
  }
}

这样,不管用户使用哪个 log 库,行为是一样的,达到了一致的用户体验。

第二个例子是 mio 库。

mio 库中的 poll 方法,就使用了门面模式。

代码语言:javascript
复制

pub struct Poll {
    registry: Registry,
}

/// Registers I/O resources.
pub struct Registry {
    selector: sys::Selector,
}

impl Poll {
    /// Create a separate `Registry` which can be used to register
    /// `event::Source`s.
    pub fn registry(&self) -> &Registry {
        &self.registry
    }

    pub fn poll(&mut self, events: &mut Events, timeout: Option<Duration>) -> io::Result<()> {
        self.registry.selector.select(events.sys(), timeout)
    }
}

mio 是实现了跨平台的非阻塞I/O接口的 Rust 抽象,通过实现 Poll 这样一个门面,屏蔽了底层不同平台的 I/O 系统调用细节,比如 epoll/kqueue/IOCP。

第三个案例是 Cranelift

Cranelift 是一个编译器,目前用于 wasmtime 和 rustc debug 模式下。最近 Cranelift 在重构新的 后端,以支持不同的架构平台:Arm/X86等。

在 Cranelift 内部通过一个 MachBackend trait 来抽象出一个 后台门面,只关心核心逻辑:编译给定的函数。

代码语言:javascript
复制
/// Top-level machine backend trait, which wraps all monomorphized code and
/// allows a virtual call from the machine-independent `Function::compile()`.
pub trait MachBackend {
    /// Compile the given function.
    fn compile_function(
        &self,
        func: &Function,
        want_disasm: bool,
    ) -> CodegenResult<MachCompileResult>;

    // ignore others functions
}

然后给不同的平台来实现这个 trait:

代码语言:javascript
复制

impl MachBackend for AArch64Backend {
    fn compile_function(
        //...
    ){/* ... */}
}

impl MachBackend for X64Backend {
    fn compile_function(
        //...
    ){/* ... */}
}

impl MachBackend for Arm32Backend {
    fn compile_function(
        //...
    ){/* ... */}
}

然后在上层代码 Context 接口调用 compile_and_emit[9] 方法时,就可以按当前平台信息生成相应指令:

代码语言:javascript
复制
pub fn compile_and_emit(/*...*/){

    // ...
    let info = self.compile(isa)?;
    //
}

pub fn compile(&mut self, isa: &dyn TargetIsa) -> CodegenResult<CodeInfo> {
    // ...
    if let Some(backend) = isa.get_mach_backend() {
        let result = backend.compile_function(&self.func, self.want_disasm)?; // 调用 compile_function
        let info = result.code_info();
        self.mach_compile_result = Some(result);
        Ok(info)
    } 
    // ...

}

// cranelift/codegen/src/machinst/adapter.rs 
// 返回 MachBackend 对象
fn get_mach_backend(&self) -> Option<&dyn MachBackend> {
    Some(&*self.backend)
}

所以,整个调用流程是:Context -> compile_and_emit -> compile -> get_mach_backend -> compile_function ,然后到各个架构平台。

结语

综上,门面模式是 Rust 应用最广泛的一个设计模式。感谢阅读,如有错漏,欢迎反馈和补充。

参考资料

[1]

开源电子书《Real World Rust Design Pattern》: https://github.com/ZhangHanDong/real-world-rust-design-pattern

[2]

Rust libstd reexport libcore: https://github.com/rust-lang/rust/tree/master/library/std/src/sys

[3]

Futures-rs: https://github.com/rust-lang/futures-rs/blob/master/futures/src/lib.rs

[4]

tikv/tikv: https://github.com/tikv/tikv/tree/master/components/tikv_alloc

[5]

log: https://github.com/rust-lang/log

[6]

mio: https://github.com/tokio-rs/mio

[7]

MachBackend: https://github.com/bytecodealliance/wasmtime/search?q=MachBackend

[8]

LowerBackend: https://github.com/bytecodealliance/wasmtime/search?q=LowerBackend

[9]

compile_and_emit: https://github.com/bytecodealliance/wasmtime/blob/main/cranelift/codegen/src/context.rs#L129

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-04-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 觉学社 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Facade(外观)模式
    • 一句话介绍
      • 解决了什么问题
        • 如何解决
          • 真实案例
            • 模块 Re-Export
            • 条件编译
            • 利用 类型 和 Trait
          • 结语
            • 参考资料
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档