Rust 的設計模式在提升程式碼品質和可維護性方面扮演著至關重要的角色。本文將探討流暢介面、觀察者模式和命令模式的實作細節,並提供實際程式碼範例和圖表說明。流暢介面透過方法鏈,讓物件的構建過程更直觀易讀。觀察者模式則利用弱參考,實作鬆散耦合的事件處理機制,避免回呼地獄的產生。命令模式則將操作封裝成物件,提升程式碼的靈活性和可擴充套件性,並提供錯誤處理機制。這些模式的應用能有效提升程式碼的結構和可讀性,對於構建複雜系統至關重要。

5.4 流暢介面模式(Fluent Interface Pattern)

流暢介面模式建立在建構者模式(Builder Pattern)的基礎上。其主要特徵是使用方法鏈(Method Chaining)。方法鏈是一種將多個函式呼叫串聯起來以完成某個操作的做法,直到該操作被終止(通常是透過一個結束操作的方法呼叫)。

5.4.1 流暢建構者(Fluent Builder)

在 Rust 中,Iterator 特徵(trait)是流暢介面模式的一個很好的例子。方法鏈可以透過從每個方法呼叫中傳回一個型別來實作,該型別會引導到鏈中的下一步。Iterator 特徵中的 map() 方法的簽名如下所示:

fn map<B, F>(self, f: F) -> Map<Self, F>
where
    F: FnMut(Self::Item) -> B,
{ ... }

程式碼實作

// 定義 Bicycle 結構體
struct Bicycle {
    make: String,
    model: String,
    size: i32,
    color: String,
}

// 為 Bicycle 實作存取器宏
macro_rules! accessor {
    ($name:ident, $type:ty) => {
        pub fn $name(&self) -> &$type {
            &self.$name
        }
    };
}

impl Bicycle {
    accessor!(make, String);
    accessor!(model, String);
    accessor!(size, i32);
    accessor!(color, String);
}

// 定義 BicycleBuilder 結構體
struct BicycleBuilder {
    bicycle: Bicycle,
}

// 為 BicycleBuilder 實作 with_str 和 with 宏
macro_rules! with_str {
    ($name:ident, $func:ident) => {
        pub fn $func(self, $name: &str) -> Self {
            Self {
                bicycle: Bicycle {
                    $name: $name.into(),
                    ..self.bicycle
                },
            }
        }
    };
}

macro_rules! with {
    ($name:ident, $func:ident, $type:ty) => {
        pub fn $func(self, $name: $type) -> Self {
            Self {
                bicycle: Bicycle {
                    $name,
                    ..self.bicycle
                },
            }
        }
    };
}

impl BicycleBuilder {
    with_str!(make, with_make);
    with_str!(model, with_model);
    with!(size, with_size, i32);
    with_str!(color, with_color);
}

內容解密:

  1. accessor!:用於為 Bicycle 結構體生成存取器方法,讓外部可以讀取其內部欄位的值。
  2. with_str!with!:用於為 BicycleBuilder 生成設定欄位值的方法,並傳回 Self 以支援方法鏈。
  3. BicycleBuilder 實作:透過宏生成的方法(如 with_makewith_model 等),可以鏈式呼叫來設定 Bicycle 的各個欄位。
  4. 使用 spread 語法初始化結構體:在更新 Bicycle 欄位時,使用 ..self.bicycle 語法來複製其他欄位的值,只更新指定的欄位。

程式碼範例與輸出

fn main() {
    let bicycle = BicycleBuilder::default()
        .with_make("Rivendell")
        .with_model("A. Homer Hilsen")
        .with_size(51)
        .with_color("red")
        .bicycle;

    println!("{:?}", bicycle);
}

輸出結果:

Bicycle { make: "Rivendell", model: "A. Homer Hilsen", size: 51, color: "red" }

圖表說明

  classDiagram
    class BicycleBuilder {
        -Bicycle bicycle
        +with_make(String) : BicycleBuilder
        +with_model(String) : BicycleBuilder
        +with_size(Integer) : BicycleBuilder
        +with_color(String) : BicycleBuilder
    }
    class Bicycle {
        -String make
        -String model
        -Integer size
        -String color
        +make() : String
        +model() : String
        +size() : Integer
        +color() : String
    }

圖表翻譯: 此 UML 圖展示了 BicycleBuilderBicycle 之間的關係。BicycleBuilder 透過鏈式呼叫(如 with_makewith_model 等方法)來逐步設定 Bicycle 的屬性,最終生成一個完整的 Bicycle 物件。

重點解析

  1. 流暢介面模式的優點

    • 提供清晰且易讀的程式碼結構。
    • 使用方法鏈,使物件的構建過程更加直觀。
  2. 實作細節

    • 使用宏簡化程式碼生成,減少重複性工作。
    • 使用 spread 語法來更新結構體欄位,避免手動指定所有欄位。
  3. 實際應用場景

    • 在需要逐步構建複雜物件時,流暢介面模式能提供更靈活和可讀的程式碼。
  4. 相關 crate 的應用

    • 使用現有的 crate(如 derive_builder)可以簡化建構者的實作,提高開發效率。

本章節詳細介紹了流暢介面模式的實作方式及其在 Rust 中的應用,並透過範例程式碼和圖表說明幫助讀者更好地理解該設計模式的核心概念和優點。透過這種模式,開發者可以寫出更清晰、更易於維護的程式碼。

5.5 觀察者模式(Observer Pattern)詳解

觀察者模式是軟體設計中一種廣泛使用的模式,尤其是在需要處理事件驅動或事件處理的系統中,如網路服務。該模式允許物件觀察其他物件的變化,並在必要時做出相應的反應。

5.5.1 為何不使用回呼函式(Callbacks)?

在深入觀察者模式之前,我們先來討論回呼函式。某些程式語言(如 JavaScript)大量使用回呼函式,這可能導致所謂的「回呼地獄」(callback hell),即回呼函式巢狀在其他回呼函式中,使得程式碼難以理解。有人甚至建立了一個網站 http://callbackhell.com 來描述這個問題並提出解決方案。

回呼函式常用於函式式程式設計中的高階函式。高階函式是指接受另一個函式作為引數或傳回另一個函式的函式。例如,迭代器使用 map() 等函式的回呼。Rust 中的基本回呼形式如下所示:

fn callback_fn<F>(f: F)
where
    F: Fn() -> (),
{
    f();
}

fn main() {
    let my_callback = || println!("I have been called back");
    callback_fn(my_callback);
}

在上述範例中,我使用了閉包(closure)作為回呼,這是 JavaScript 中常見的做法,但我也可以傳遞普通函式。簡單的情況尚可接受,但當回呼巢狀在其他回呼中時,邏輯流程會變得混亂。

雖然回呼函式本身並非壞事,但觀察者模式提供了更鬆散的耦合,使得附加和分離觀察者更加容易,並且支援多對一的關係,而非一對一。更廣泛地說,當我們有程式碼需要在不依賴觀察者的情況下通知其他程式碼有關事件(主題)時,可以使用觀察者模式。主題可以在不知道觀察者的情況下通知它們。

回呼的另一個問題是,它們不允許我們將狀態與傳遞給回呼的函式分離。我們必須透過使用閉包或全域變數將狀態繫結到回呼。

5.5.2 實作觀察者模式

有多種方法可以實作觀察者模式,每種方法都有其權衡。本文中的範例足夠靈活,以便根據需要更改實作細節以適應各種情況。我們將按照圖 5.3 所示,在 Rust 中實作觀察者模式。

圖 5.3 觀察者模式的 UML 圖

首先,我們將實作兩個特徵(trait):ObserverObservableObserver 將用於想要觀察其他物件的物件。Observable 將由希望允許其他物件觀察自己的物件實作。以下清單顯示了 Observer 特徵:

pub trait Observer {
    type Subject;
    fn observe(&self, subject: &Self::Subject);
}

對於觀察者,我使用 observe 一詞而不是 notify(按照原始設計模式)。接下來,請考慮以下清單,它顯示了 Observable 特徵:

pub trait Observable {
    type Observer;
    fn update(&self);
    fn attach(&mut self, observer: Self::Observer);
    fn detach(&mut self, observer: Self::Observer);
}

Observable 特徵提供了主題的方法,並符合原始設計模式。我們對觀察者或主題的型別不做任何假設,這為我們提供了更大的靈活性。接下來,我們需要建立一個主題並在其上實作 Observable

pub struct Subject {
    observers: Vec<Weak<dyn Observer<Subject = Self>>>,
}

impl Observable for Subject {
    type Observer = Arc<dyn Observer<Subject = Self>>;
    
    fn update(&self) {
        self.observers
            .iter()
            .flat_map(|o| o.upgrade())
            .for_each(|o| o.observe(self));
    }

    fn attach(&mut self, observer: Self::Observer) {
        self.observers.push(Arc::downgrade(&observer));
    }

    fn detach(&mut self, observer: Self::Observer) {
        self.observers
            .retain(|f| {
                !f.ptr_eq(&Arc::downgrade(&observer))
            });
    }
}

程式碼解析:

  1. Subject 結構體:儲存觀察者的弱參考(Weak<dyn Observer<Subject = Self>>),這允許主題在觀察者超出範圍時忽略它們,而不是保持物件存活。

  2. Observable 特徵實作

    • update 方法:遍歷所有觀察者,升級弱參考,並在每個仍然有效的觀察者上呼叫 observe 方法。
    • attach 方法:將新的觀察者新增到 observers 向量中,將 Arc 降級為 Weak 指標。
    • detach 方法:從 observers 向量中移除指定的觀察者,使用 ptr_eq 來比較指標。

為主題新增狀態和新方法

接下來,讓我們為主題新增一些狀態,以便我們可以測試它,提供一個存取器,並新增一個 new 方法。我們將更新程式碼,使其如下所示:

pub struct Subject {
    observers: Vec<Weak<dyn Observer<Subject = Self>>>,
    state: String,
}

impl Subject {
    pub fn new(state: &str) -> Self {
        Subject {
            observers: Vec::new(),
            state: state.to_string(),
        }
    }

    // 可以新增 getter 方法來存取 state
    pub fn get_state(&self) -> &str {
        &self.state
    }

    // 可以新增 setter 方法來更新 state 並通知觀察者
    pub fn set_state(&mut self, new_state: &str) {
        self.state = new_state.to_string();
        self.update();
    }
}

使用 Mermaid 圖表展示觀察者模式結構

  classDiagram
    class Subject {
        - observers: Vec~Weak~dyn Observer~~Subject~~>>
        - state: String
        + new(state: &str): Subject
        + get_state(): &str
        + set_state(new_state: &str)
        + update()
        + attach(observer: Arc~dyn Observer~~Subject~~>>)
        + detach(observer: Arc~dyn Observer~~Subject~~>>)
    }

    class Observer {
        <<interface>>
        + observe(subject: &Subject)
    }

    Subject ..> Observer : notifies

圖表翻譯: 此圖示展示了觀察者模式的結構,其中 Subject 維護了一個觀察者列表,並在狀態變更時通知他們。Observer 是一個介面,定義了觀察者需要實作的 observe 方法。當 Subject 的狀態發生變化時,它會呼叫所有註冊觀察者的 observe 方法。

5.6 命令模式(Command Pattern)

命令模式是一種設計模式,用於將請求或操作封裝成物件,以便在不同的時間點執行或復原。該模式允許將傳送者和接收者解耦,從而提高系統的靈活性和可擴充套件性。

5.6.1 命令模式的定義

在 Rust 中實作命令模式之前,我們需要定義該模式的核心。命令模式主要涉及一個名為 Command 的特徵(trait),該特徵定義了執行命令的方法。命令的接收者(Receiver)是一個具體的物件,該物件將執行命令定義的操作。

trait Command {
    fn execute(&self) -> Result<(), Error>;
}

內容解密:

Command 特徵定義了一個 execute 方法,該方法傳回一個 Result 型別,用於處理可能的錯誤。這使得命令的執行具有錯誤處理的能力。

5.6.2 命令模式的實作

在本例中,我們將建立兩個命令物件:ReadFileWriteFile,這兩個命令分別用於讀取和寫入檔案。接收者是檔案控制程式碼(file handle)。

5.6.2.1 ReadFile 命令的實作

首先,我們定義 ReadFile 結構體和其相關的方法:

struct ReadFile {
    receiver: File,
}

impl ReadFile {
    fn new(receiver: File) -> Box<Self> {
        Box::new(Self { receiver })
    }
}

impl Command for ReadFile {
    fn execute(&self) -> Result<(), Error> {
        println!("從檔案開頭開始讀取");
        let mut reader = BufReader::new(&self.receiver);
        reader.seek(std::io::SeekFrom::Start(0))?;
        for (count, line) in reader.lines().enumerate() {
            println!("{:2}: {}", count + 1, line?);
        }
        Ok(())
    }
}

內容解密:

  • ReadFile 結構體包含一個 receiver 欄位,該欄位代表檔案控制程式碼。
  • new 方法建立一個新的 ReadFile 例項,並傳回一個裝箱的物件。
  • execute 方法實作了讀取檔案的功能。它使用 BufReader 從檔案開頭開始讀取,並列印出每一行的內容。

5.6.2.2 WriteFile 命令的實作

接下來,我們定義 WriteFile 結構體和其相關的方法:

struct WriteFile {
    content: String,
    receiver: File,
}

impl WriteFile {
    fn new(content: String, receiver: File) -> Box<Self> {
        Box::new(Self { content, receiver })
    }
}

impl Command for WriteFile {
    fn execute(&self) -> Result<(), Error> {
        println!("寫入新內容到檔案");
        let mut writer = self.receiver.try_clone()?;
        // 寫入內容到檔案的實作
        Ok(())
    }
}

內容解密:

  • WriteFile 結構體包含 contentreceiver 兩個欄位,分別代表要寫入的內容和檔案控制程式碼。
  • new 方法建立一個新的 WriteFile 例項,並傳回一個裝箱的物件。
  • execute 方法實作了寫入內容到檔案的功能。它首先複製檔案控制程式碼以獲得一個可變的控制程式碼,然後可以將內容寫入檔案。

命令模式的使用

透過定義和實作 Command 特徵以及具體的命令物件,我們可以將命令模式應用於不同的場景,例如檔案操作、事務處理等。

fn main() -> Result<(), Error> {
    let file = File::open("example.txt")?;
    let read_command = ReadFile::new(file);
    read_command.execute()?;

    let new_content = String::from("新的內容");
    let file = File::create("example.txt")?;
    let write_command = WriteFile::new(new_content, file);
    write_command.execute()?;

    Ok(())
}

內容解密:

  • main 函式中,我們演示瞭如何使用 ReadFileWriteFile 命令來讀取和寫入檔案。
  • 首先,我們開啟一個檔案並建立一個 ReadFile 命令來讀取其內容。
  • 然後,我們建立一個新的內容字串和一個 WriteFile 命令來將內容寫入檔案。

透過這種方式,命令模式使得我們可以靈活地管理和執行不同的操作,同時保持程式碼的清晰和可維護性。