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

编者按:

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

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


Facade(外观)模式

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

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

一句话介绍

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

解决了什么问题

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

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

如何解决

心智图:

  1. +------------------+
  2. +-------+ | | +---------------+
  3. | | | | | additional |
  4. |client +------> | facade +-------> | facade |
  5. +-------+ | | | |
  6. | | | |
  7. +--+----+------+--++ +---------------+
  8. | | | |
  9. +--------+ | | +--------+
  10. | +--+ +-+ |
  11. | | | |
  12. v | v v
  13. +---+---+ +---v--+ +----+--+ +---+----+
  14. | | | | | | | |
  15. | system| |system| |system | | system |
  16. | | | | | | | |
  17. +-------+ +------+ +-------+ +--------+

真实案例

实现方式:

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

模块 Re-Export

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

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

  1. src/
  2. - lib.rs
  3. - module/
  4. -- mod.rs
  5. -- submodule/
  6. --- mod.rs

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

  1. // in module/submodule/mod.rs
  2. pub fn goodbye(){}
  3. // in lib.rs
  4. pub use module::submodule::goodbye;

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

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

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

条件编译

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

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

  1. #[cfg(all(unix, not(fuzzing), feature = "jemalloc"))]
  2. #[path = "jemalloc.rs"]
  3. mod imp;
  4. #[cfg(all(unix, not(fuzzing), feature = "tcmalloc"))]
  5. #[path = "tcmalloc.rs"]
  6. mod imp;
  7. #[cfg(all(unix, not(fuzzing), feature = "mimalloc"))]
  8. #[path = "mimalloc.rs"]
  9. mod imp;
  10. #[cfg(not(all(
  11. unix,
  12. not(fuzzing),
  13. any(feature = "jemalloc", feature = "tcmalloc", feature = "mimalloc")
  14. )))]
  15. #[path = "system.rs"]
  16. mod imp;

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

利用 类型 和 Trait

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

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

  1. pub trait Log: Sync + Send {
  2. /// Determines if a log message with the specified metadata would be
  3. /// logged.
  4. ///
  5. /// This is used by the `log_enabled!` macro to allow callers to avoid
  6. /// expensive computation of log message arguments if the message would be
  7. /// discarded anyway.
  8. fn enabled(&self, metadata: &Metadata) -> bool;
  9. /// Logs the `Record`.
  10. ///
  11. /// Note that `enabled` is *not* necessarily called before this method.
  12. /// Implementations of `log` should perform all necessary filtering
  13. /// internally.
  14. fn log(&self, record: &Record);
  15. /// Flushes any buffered records.
  16. fn flush(&self);
  17. }

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

  1. // env_log
  2. impl Log for Logger {
  3. fn enabled(&self, metadata: &Metadata) -> bool {
  4. self.filter.enabled(metadata)
  5. }
  6. fn log(&self, record: &Record) {
  7. if self.matches(record) {
  8. // ignore many codes
  9. }
  10. }
  11. fn flush(&self) {}
  12. }
  13. // syslog
  14. impl Log for BasicLogger {
  15. fn enabled(&self, metadata: &Metadata) -> bool {
  16. true
  17. }
  18. fn log(&self, record: &Record) {
  19. //FIXME: temporary patch to compile
  20. let message = format!("{}", record.args());
  21. let mut logger = self.logger.lock().unwrap();
  22. match record.level() {
  23. Level::Error => logger.err(message),
  24. Level::Warn => logger.warning(message),
  25. Level::Info => logger.info(message),
  26. Level::Debug => logger.debug(message),
  27. Level::Trace => logger.debug(message)
  28. };
  29. }
  30. fn flush(&self) {
  31. let _ = self.logger.lock().unwrap().backend.flush();
  32. }
  33. }

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

第二个例子是 mio 库。

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

  1. pub struct Poll {
  2. registry: Registry,
  3. }
  4. /// Registers I/O resources.
  5. pub struct Registry {
  6. selector: sys::Selector,
  7. }
  8. impl Poll {
  9. /// Create a separate `Registry` which can be used to register
  10. /// `event::Source`s.
  11. pub fn registry(&self) -> &Registry {
  12. &self.registry
  13. }
  14. pub fn poll(&mut self, events: &mut Events, timeout: Option<Duration>) -> io::Result<()> {
  15. self.registry.selector.select(events.sys(), timeout)
  16. }
  17. }

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

第三个案例是 Cranelift

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

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

  1. /// Top-level machine backend trait, which wraps all monomorphized code and
  2. /// allows a virtual call from the machine-independent `Function::compile()`.
  3. pub trait MachBackend {
  4. /// Compile the given function.
  5. fn compile_function(
  6. &self,
  7. func: &Function,
  8. want_disasm: bool,
  9. ) -> CodegenResult<MachCompileResult>;
  10. // ignore others functions
  11. }

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

  1. impl MachBackend for AArch64Backend {
  2. fn compile_function(
  3. //...
  4. ){/* ... */}
  5. }
  6. impl MachBackend for X64Backend {
  7. fn compile_function(
  8. //...
  9. ){/* ... */}
  10. }
  11. impl MachBackend for Arm32Backend {
  12. fn compile_function(
  13. //...
  14. ){/* ... */}
  15. }

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

  1. pub fn compile_and_emit(/*...*/){
  2. // ...
  3. let info = self.compile(isa)?;
  4. //
  5. }
  6. pub fn compile(&mut self, isa: &dyn TargetIsa) -> CodegenResult<CodeInfo> {
  7. // ...
  8. if let Some(backend) = isa.get_mach_backend() {
  9. let result = backend.compile_function(&self.func, self.want_disasm)?; // 调用 compile_function
  10. let info = result.code_info();
  11. self.mach_compile_result = Some(result);
  12. Ok(info)
  13. }
  14. // ...
  15. }
  16. // cranelift/codegen/src/machinst/adapter.rs
  17. // 返回 MachBackend 对象
  18. fn get_mach_backend(&self) -> Option<&dyn MachBackend> {
  19. Some(&*self.backend)
  20. }

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

结语

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