Rust語言之GoF設計模式:抽象工廠模式

banq 發表於 2022-09-25
設計模式 Go

抽象工廠解決了在不指定具體類的情況下建立整個產品系列的問題。

抽象工廠的抽象介面: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,我們只要實現相應工廠實現,在其中實現介面元素的建立,而這個新增程式碼的過程,不涉及對原始碼結構的修改,這樣如同蓋房子,蓋好的結構不用修改,假設鋼筋柱都澆築好了,準備用磚頭砌牆,發現兩個鋼筋柱需要挪移,否則無法砌牆,這在建築上是致命,同理也適合軟體工程。