Rust语言之GoF设计模式:抽象工厂模式


抽象工厂解决了在不指定具体类的情况下创建整个产品系列的问题。

抽象工厂的抽象接口:lib.rs

pub trait Button {
    fn press(&self);
}

pub trait Checkbox {
    fn switch(&self);
}

///  抽象工厂是通过泛型实现的,它允许编译器创建一个不需要在运行时进行动态调度的代码。
pub trait GuiFactory {
    type B: Button;
    type C: Checkbox;

    fn create_button(&self) -> Self::B;
    fn create_checkbox(&self) -> Self::C;
}

/// 使用Box指针定义的抽象工厂。
pub trait GuiFactoryDynamic {
    fn create_button(&self) -> Box<dyn Button>;
    fn create_checkbox(&self) -> Box<dyn Checkbox>;
}

有两种界面元素:按钮Button和选择框CheckBox
然后:
Button按钮有两种风格:Windows和MacOs ;
CheckBox有两种风格:Windows和MacOs .

上述思路是从界面元素切入,那么如何创建这2X2的组合产品呢?
这需要重新从新的角度切入,以生产创建功能的思路切入,也就是工厂方法思路切入:
Windows和MacOS是比Button和CheckBox更大级别的分类,在这两个操作系统中,我们可以分别创建Button和CheckBox。
这种创建的产品是两个以上产品,因此属于复杂的抽象工厂,不是简单工厂,简单工厂只创建一个产品。

我们将Windows风格的按钮Button和选择框CheckBox放在一起作为抽象工厂的实现:

windows-gui是抽象工厂一个实现:

use gui::{Button, Checkbox, GuiFactory, GuiFactoryDynamic};

use crate::{button::WindowsButton, checkbox::WindowsCheckbox};

pub struct WindowsFactory;

impl GuiFactory for WindowsFactory {
    type B = WindowsButton;
    type C = WindowsCheckbox;

    fn create_button(&self) -> WindowsButton {
        WindowsButton
    }

    fn create_checkbox(&self) -> WindowsCheckbox {
        WindowsCheckbox
    }
}

impl GuiFactoryDynamic for WindowsFactory {
    fn create_button(&self) -> Box<dyn Button> {
        Box::new(WindowsButton)
    }

    fn create_checkbox(&self) -> Box<dyn Checkbox> {
        Box::new(WindowsCheckbox)
    }
}

泛型按钮B的具体实现button.rs

use gui::Button;

pub struct WindowsButton;

impl Button for WindowsButton {
    fn press(&self) {
        println!("Windows button has pressed");
    }
}

泛型​​​​​按钮C的实现:checkbox.rs

use gui::Checkbox;

pub struct WindowsCheckbox;

impl Checkbox for WindowsCheckbox {
    fn switch(&self) {
        println!("Windows checkbox has switched");
    }
}

另外一个辅助:lib.rs

以上是Windows风格抽象工厂实现,另外一个是苹果风格实现:macos-gui,点击见源码

客户端代码
抽象工厂的结构基本搭建起来,下面看看魔法的核心在客户端调用处:

下面是调用抽象工厂代码:main.rs

mod render;

use render::render;

use macos_gui::factory::MacFactory;
use windows_gui::factory::WindowsFactory;

fn main() {
    let windows = true;

    if windows {
        render(WindowsFactory);
    } else {
        render(MacFactory);
    }
}

这个main.rs对两个工厂进行了选择切换,具体渲染依赖于render.rs

//! 代码表明,它不依赖于具体的
//工厂的实现。

use gui::GuiFactory;

// 渲染GUI。工厂对象必须作为一个参数传递给这种
工厂调用的
//泛型函数,以利用静态调度。
pub fn render(factory: impl GuiFactory) {
    let button1 = factory.create_button();
    let button2 = factory.create_button();
    let checkbox1 = factory.create_checkbox();
    let checkbox2 = factory.create_checkbox();

    use gui::{Button, Checkbox};

    button1.press();
    button2.press();
    checkbox1.switch();
    checkbox2.switch();
}

在这个渲染方法中,实现了最初功能需求:
Button按钮有两种风格:Windows和MacOs ;
CheckBox有两种风格:Windows和MacOs .

但是,这段代码没有耦合依赖于Windows和MacOs两个工厂,而只是依赖抽象工厂接口。
那么抽象工厂与具体两个工厂实现如何装配在一起呢?在main.rs这个客户端调用代码的if else语句

这样做的好处:客户端能根据自已的上下文环境,自由指定不同的工厂实现:
如果当前应用入口是安装在windows上,就指定windows的工厂创建windows风格的两种界面元素;
而如果当前应用入口是安装在MacOs上,就指定MacOS的工厂创建MacOs风格的两种界面元素;

那么能不能在客户端根据自己运行操作系统环境自动选择工厂呢?不用ifelse这样伪代码?
app-dyn:

mod render;

use render::render;

use gui::GuiFactoryDynamic;
use macos_gui::factory::MacFactory;
use windows_gui::factory::WindowsFactory;

fn main() {
    let windows = false;

      // 根据无法预测的输入,在运行时分配一个工厂对象。
    let factory: &dyn GuiFactoryDynamic = if windows {
        &WindowsFactory
    } else {
        &MacFactory
    };

   
// 工厂的调用可以在这里被内联。
    let button = factory.create_button();
    button.press();

    
// 工厂对象可以作为参数传递给一个函数。
    render(factory);
}

总结
这样,通过抽象工厂,根据不同操作系统创建多个界面元素,如果新增新的操作系统,如Linux,我们只要实现相应工厂实现,在其中实现界面元素的创建,而这个新增代码的过程,不涉及对原代码结构的修改,这样如同盖房子,盖好的结构不用修改,假设钢筋柱都浇筑好了,准备用砖头砌墙,发现两个钢筋柱需要挪移,否则无法砌墙,这在建筑上是致命,同理也适合软件工程。