抽象工厂解决了在不指定具体类的情况下创建整个产品系列的问题。
抽象工厂的抽象接口: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,我们只要实现相应工厂实现,在其中实现界面元素的创建,而这个新增代码的过程,不涉及对原代码结构的修改,这样如同盖房子,盖好的结构不用修改,假设钢筋柱都浇筑好了,准备用砖头砌墙,发现两个钢筋柱需要挪移,否则无法砌墙,这在建筑上是致命,同理也适合软件工程。