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

作者:张汉东 / 编辑:张汉东

编者按:

本文摘录自开源电子书《Real World Rust Design Pattern》,这本书也是我创建的免费开源电子书,目前正在逐步完善中,欢迎贡献。

这本书旨在挖掘和记录 Rust 开源生态中设计模式的真实实践。欢迎参与贡献!


Facade(外观)模式

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

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

一句话介绍

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

解决了什么问题

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

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

如何解决

心智图:

                 +------------------+
+-------+        |                  |         +---------------+
|       |        |                  |         |  additional   |
|client +------> |     facade       +-------> |  facade       |
+-------+        |                  |         |               |
                 |                  |         |               |
                 +--+----+------+--++         +---------------+
                    |    |      |  |
           +--------+    |      |  +--------+
           |          +--+      +-+         |
           |          |           |         |
           v          |           v         v
       +---+---+  +---v--+   +----+--+  +---+----+
       |       |  |      |   |       |  |        |
       | system|  |system|   |system |  | system |
       |       |  |      |   |       |  |        |
       +-------+  +------+   +-------+  +--------+

真实案例

实现方式:

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

模块 Re-Export

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

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


#![allow(unused)]
fn main() {
src/
    - lib.rs
    - module/
        -- mod.rs
        -- submodule/
            --- mod.rs
}

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


#![allow(unused)]
fn main() {
// 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 来支持多种内存分配器。


#![allow(unused)]
fn main() {
#[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 库。


#![allow(unused)]
fn main() {
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。


#![allow(unused)]
fn main() {
// 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 方法,就使用了门面模式。


#![allow(unused)]

fn main() {
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 来抽象出一个 后台门面,只关心核心逻辑:编译给定的函数。


#![allow(unused)]
fn main() {
/// 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:


#![allow(unused)]

fn main() {
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 方法时,就可以按当前平台信息生成相应指令:


#![allow(unused)]
fn main() {
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 应用最广泛的一个设计模式。感谢阅读,如有错漏,欢迎反馈和补充。