Triển khai một Mẫu Thiết kế Hướng đối tượng

Mẫu trạng thái (state pattern) là một mẫu thiết kế hướng đối tượng. Trọng tâm của mẫu này là chúng ta định nghĩa một tập hợp các trạng thái mà một giá trị có thể có bên trong. Các trạng thái được biểu diễn bởi một tập hợp các đối tượng trạng thái, và hành vi của giá trị thay đổi dựa trên trạng thái của nó. Chúng ta sẽ tiến hành thực hiện một ví dụ về một struct bài đăng blog có một trường để lưu trữ trạng thái của nó, trường này sẽ là một đối tượng trạng thái từ tập hợp "nháp", "đang xét duyệt", hoặc "đã xuất bản".

Các đối tượng trạng thái chia sẻ chức năng: trong Rust, tất nhiên, chúng ta sử dụng struct và trait thay vì đối tượng và tính kế thừa. Mỗi đối tượng trạng thái chịu trách nhiệm cho hành vi của chính nó và cho việc quản lý khi nào nó nên chuyển sang trạng thái khác. Giá trị chứa đối tượng trạng thái không biết gì về các hành vi khác nhau của các trạng thái hoặc khi nào chuyển đổi giữa các trạng thái.

Lợi thế của việc sử dụng mẫu trạng thái là khi các yêu cầu nghiệp vụ của chương trình thay đổi, chúng ta sẽ không cần phải thay đổi mã của giá trị đang giữ trạng thái hoặc mã sử dụng giá trị đó. Chúng ta sẽ chỉ cần cập nhật mã bên trong một trong các đối tượng trạng thái để thay đổi quy tắc của nó hoặc có thể thêm nhiều đối tượng trạng thái hơn.

Đầu tiên, chúng ta sẽ triển khai mẫu trạng thái theo cách hướng đối tượng truyền thống hơn, sau đó chúng ta sẽ sử dụng một cách tiếp cận tự nhiên hơn một chút trong Rust. Hãy đi sâu vào việc triển khai dần dần một quy trình làm việc của bài đăng blog bằng cách sử dụng mẫu trạng thái.

Chức năng cuối cùng sẽ trông như thế này:

  1. Một bài đăng blog bắt đầu như một bản nháp trống.
  2. Khi bản nháp hoàn thành, một đánh giá của bài đăng được yêu cầu.
  3. Khi bài đăng được phê duyệt, nó được xuất bản.
  4. Chỉ có các bài đăng blog đã xuất bản mới trả về nội dung để in, vì vậy các bài đăng chưa được phê duyệt không thể vô tình được xuất bản.

Bất kỳ thay đổi nào khác được thử trên một bài đăng sẽ không có hiệu lực. Ví dụ, nếu chúng ta cố gắng phê duyệt một bài đăng blog nháp trước khi chúng ta yêu cầu đánh giá, bài đăng vẫn sẽ là một bản nháp chưa xuất bản.

Danh sách 18-11 hiển thị quy trình làm việc này dưới dạng mã: đây là một ví dụ về việc sử dụng API mà chúng ta sẽ triển khai trong một thư viện crate có tên là blog. Điều này sẽ không biên dịch được vì chúng ta chưa triển khai crate blog.

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

Chúng ta muốn cho phép người dùng tạo một bài đăng blog nháp mới với Post::new. Chúng ta muốn cho phép thêm văn bản vào bài đăng blog. Nếu chúng ta cố gắng lấy nội dung của bài đăng ngay lập tức, trước khi phê duyệt, chúng ta sẽ không nhận được bất kỳ văn bản nào vì bài đăng vẫn đang ở trạng thái nháp. Chúng ta đã thêm assert_eq! trong mã để minh họa. Một bài kiểm tra đơn vị xuất sắc cho trường hợp này sẽ khẳng định rằng một bài đăng blog nháp trả về một chuỗi rỗng từ phương thức content, nhưng chúng ta sẽ không viết kiểm tra cho ví dụ này.

Tiếp theo, chúng ta muốn cho phép yêu cầu đánh giá bài đăng, và chúng ta muốn content trả về một chuỗi rỗng trong khi chờ đánh giá. Khi bài đăng nhận được sự chấp thuận, nó sẽ được xuất bản, nghĩa là văn bản của bài đăng sẽ được trả về khi gọi content.

Lưu ý rằng kiểu duy nhất mà chúng ta tương tác từ crate là kiểu Post. Kiểu này sẽ sử dụng mẫu trạng thái và sẽ chứa một giá trị sẽ là một trong ba đối tượng trạng thái đại diện cho các trạng thái khác nhau mà một bài đăng có thể có—nháp, đang xét duyệt, hoặc đã xuất bản. Việc thay đổi từ trạng thái này sang trạng thái khác sẽ được quản lý nội bộ trong kiểu Post. Các trạng thái thay đổi để đáp ứng với các phương thức được gọi bởi người dùng thư viện của chúng ta trên thực thể Post, nhưng họ không phải quản lý các thay đổi trạng thái trực tiếp. Ngoài ra, người dùng không thể mắc lỗi với các trạng thái, chẳng hạn như xuất bản một bài đăng trước khi nó được xem xét.

Định nghĩa Post và Tạo một Thực thể Mới ở Trạng thái Nháp

Hãy bắt đầu với việc triển khai thư viện! Chúng ta biết rằng chúng ta cần một struct Post công khai chứa một số nội dung, vì vậy chúng ta sẽ bắt đầu với định nghĩa của struct và một hàm new công khai liên quan để tạo một thực thể của Post, như trong Danh sách 18-12. Chúng ta cũng sẽ tạo một trait State riêng tư sẽ định nghĩa hành vi mà tất cả các đối tượng trạng thái cho một Post phải có.

Sau đó, Post sẽ chứa một đối tượng trait của Box<dyn State> bên trong một Option<T> trong một trường riêng tư có tên là state để lưu trữ đối tượng trạng thái. Bạn sẽ thấy tại sao Option<T> là cần thiết sau một chút.

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Trait State định nghĩa hành vi được chia sẻ bởi các trạng thái bài đăng khác nhau. Các đối tượng trạng thái là Draft, PendingReview, và Published, và tất cả chúng sẽ triển khai trait State. Hiện tại, trait không có bất kỳ phương thức nào, và chúng ta sẽ bắt đầu bằng cách định nghĩa chỉ trạng thái Draft vì đó là trạng thái mà chúng ta muốn một bài đăng bắt đầu.

Khi chúng ta tạo một Post mới, chúng ta đặt trường state của nó thành một giá trị Some chứa một Box. Box này trỏ đến một thực thể mới của struct Draft. Điều này đảm bảo rằng bất cứ khi nào chúng ta tạo một thực thể mới của Post, nó sẽ bắt đầu như một bản nháp. Vì trường state của Post là riêng tư, không có cách nào để tạo một Post ở bất kỳ trạng thái nào khác! Trong hàm Post::new, chúng ta đặt trường content thành một String rỗng, mới.

Lưu trữ Văn bản của Nội dung Bài đăng

Chúng ta đã thấy trong Danh sách 18-11 rằng chúng ta muốn có thể gọi một phương thức có tên là add_text và truyền cho nó một &str để sau đó được thêm vào như là nội dung văn bản của bài đăng blog. Chúng ta triển khai điều này như một phương thức, thay vì để lộ trường content dưới dạng pub, để sau này chúng ta có thể triển khai một phương thức sẽ kiểm soát cách dữ liệu của trường content được đọc. Phương thức add_text khá đơn giản, vì vậy hãy thêm triển khai trong Danh sách 18-13 vào khối impl Post.

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Phương thức add_text lấy một tham chiếu có thể thay đổi đến self vì chúng ta đang thay đổi thực thể Post mà chúng ta đang gọi add_text trên đó. Sau đó, chúng ta gọi push_str trên String trong content và truyền đối số text để thêm vào content đã lưu. Hành vi này không phụ thuộc vào trạng thái mà bài đăng đang ở, vì vậy nó không phải là một phần của mẫu trạng thái. Phương thức add_text không tương tác với trường state chút nào, nhưng nó là một phần của hành vi mà chúng ta muốn hỗ trợ.

Đảm bảo Nội dung của một Bài đăng Nháp là Trống

Ngay cả sau khi chúng ta đã gọi add_text và thêm một số nội dung vào bài đăng của chúng ta, chúng ta vẫn muốn phương thức content trả về một lát cắt chuỗi rỗng vì bài đăng vẫn đang ở trạng thái nháp, như được hiển thị trên dòng 7 của Danh sách 18-11. Hiện tại, hãy triển khai phương thức content với thứ đơn giản nhất sẽ hoàn thành yêu cầu này: luôn trả về một lát cắt chuỗi rỗng. Chúng ta sẽ thay đổi điều này sau khi chúng ta triển khai khả năng thay đổi trạng thái của một bài đăng để nó có thể được xuất bản. Cho đến nay, các bài đăng chỉ có thể ở trạng thái nháp, vì vậy nội dung bài đăng sẽ luôn trống. Danh sách 18-14 hiển thị triển khai giữ chỗ này.

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

Với phương thức content đã thêm này, mọi thứ trong Danh sách 18-11 cho đến dòng 7 hoạt động như dự định.

Yêu cầu Đánh giá Thay đổi Trạng thái của Bài đăng

Tiếp theo, chúng ta cần thêm chức năng để yêu cầu đánh giá một bài đăng, điều này sẽ thay đổi trạng thái của nó từ Draft sang PendingReview. Danh sách 18-15 hiển thị mã này.

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

Chúng ta cung cấp cho Post một phương thức công khai có tên là request_review sẽ lấy một tham chiếu có thể thay đổi đến self. Sau đó, chúng ta gọi một phương thức request_review nội bộ trên trạng thái hiện tại của Post, và phương thức request_review thứ hai này tiêu thụ trạng thái hiện tại và trả về một trạng thái mới.

Chúng ta thêm phương thức request_review vào trait State; tất cả các kiểu triển khai trait sẽ cần phải triển khai phương thức request_review. Lưu ý rằng thay vì có self, &self, hoặc &mut self làm tham số đầu tiên của phương thức, chúng ta có self: Box<Self>. Cú pháp này có nghĩa là phương thức chỉ hợp lệ khi được gọi trên một Box chứa kiểu. Cú pháp này lấy quyền sở hữu của Box<Self>, làm mất hiệu lực trạng thái cũ để giá trị trạng thái của Post có thể chuyển đổi thành một trạng thái mới.

Để tiêu thụ trạng thái cũ, phương thức request_review cần lấy quyền sở hữu của giá trị trạng thái. Đây là nơi mà Option trong trường state của Post xuất hiện: chúng ta gọi phương thức take để lấy giá trị Some ra khỏi trường state và để lại một None trong vị trí của nó vì Rust không cho phép chúng ta có các trường không được điền trong struct. Điều này cho phép chúng ta di chuyển giá trị state ra khỏi Post thay vì mượn nó. Sau đó, chúng ta sẽ đặt giá trị state của bài đăng thành kết quả của thao tác này.

Chúng ta cần đặt state thành None tạm thời thay vì đặt nó trực tiếp với mã như self.state = self.state.request_review(); để lấy quyền sở hữu của giá trị state. Điều này đảm bảo Post không thể sử dụng giá trị state cũ sau khi chúng ta đã chuyển đổi nó thành một trạng thái mới.

Phương thức request_review trên Draft trả về một thực thể mới, được đóng hộp của một struct PendingReview mới, đại diện cho trạng thái khi một bài đăng đang chờ đánh giá. Struct PendingReview cũng triển khai phương thức request_review nhưng không thực hiện bất kỳ chuyển đổi nào. Thay vào đó, nó trả về chính nó vì khi chúng ta yêu cầu đánh giá một bài đăng đã ở trạng thái PendingReview, nó nên tiếp tục ở trạng thái PendingReview.

Bây giờ chúng ta có thể bắt đầu thấy những lợi thế của mẫu trạng thái: phương thức request_review trên Post giống nhau bất kể giá trị state của nó là gì. Mỗi trạng thái chịu trách nhiệm cho các quy tắc của riêng nó.

Chúng ta sẽ để phương thức content trên Post như hiện tại, trả về một lát cắt chuỗi rỗng. Bây giờ chúng ta có thể có một Post ở trạng thái PendingReview cũng như ở trạng thái Draft, nhưng chúng ta muốn có hành vi giống nhau ở trạng thái PendingReview. Danh sách 18-11 hiện hoạt động cho đến dòng 10!

Thêm approve để Thay đổi Hành vi của content

Phương thức approve sẽ tương tự như phương thức request_review: nó sẽ đặt state thành giá trị mà trạng thái hiện tại nói rằng nó nên có khi trạng thái đó được phê duyệt, như được hiển thị trong Danh sách 18-16:

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        ""
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

Chúng ta thêm phương thức approve vào trait State và thêm một struct mới triển khai State, trạng thái Published.

Tương tự như cách request_review trên PendingReview hoạt động, nếu chúng ta gọi phương thức approve trên một Draft, nó sẽ không có hiệu lực gì vì approve sẽ trả về self. Khi chúng ta gọi approve trên PendingReview, nó trả về một thực thể mới, được đóng hộp của struct Published. Struct Published triển khai trait State, và đối với cả phương thức request_review và phương thức approve, nó trả về chính nó, vì bài đăng nên ở trong trạng thái Published trong các trường hợp đó.

Bây giờ chúng ta cần cập nhật phương thức content trên Post. Chúng ta muốn giá trị trả về từ content phụ thuộc vào trạng thái hiện tại của Post, vì vậy chúng ta sẽ để Post ủy quyền cho một phương thức content được định nghĩa trên state của nó, như được hiển thị trong Danh sách 18-17:

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    // --snip--
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }
    // --snip--

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

Vì mục tiêu là giữ tất cả các quy tắc này bên trong các struct triển khai State, chúng ta gọi một phương thức content trên giá trị trong state và truyền thực thể bài đăng (tức là self) như một đối số. Sau đó, chúng ta trả về giá trị được trả về từ việc sử dụng phương thức content trên giá trị state.

Chúng ta gọi phương thức as_ref trên Option vì chúng ta muốn một tham chiếu đến giá trị bên trong Option chứ không phải quyền sở hữu của giá trị. Vì state là một Option<Box<dyn State>>, khi chúng ta gọi as_ref, một Option<&Box<dyn State>> được trả về. Nếu chúng ta không gọi as_ref, chúng ta sẽ gặp lỗi vì chúng ta không thể di chuyển state ra khỏi &self được mượn của tham số hàm.

Sau đó, chúng ta gọi phương thức unwrap, mà chúng ta biết sẽ không bao giờ gây panic, vì chúng ta biết các phương thức trên Post đảm bảo rằng state sẽ luôn chứa một giá trị Some khi các phương thức đó hoàn thành. Đây là một trong những trường hợp chúng ta đã nói đến trong "Các trường hợp Trong Đó Bạn Có Thêm Thông tin Hơn Trình biên dịch" trong Chương 9 khi chúng ta biết rằng một giá trị None không bao giờ có thể xảy ra, mặc dù trình biên dịch không thể hiểu điều đó.

Ở thời điểm này, khi chúng ta gọi content trên &Box<dyn State>, ép buộc giải tham chiếu sẽ có hiệu lực trên &Box để phương thức content cuối cùng sẽ được gọi trên kiểu triển khai trait State. Điều đó có nghĩa là chúng ta cần thêm content vào định nghĩa trait State, và đó là nơi chúng ta sẽ đặt logic cho nội dung nào sẽ trả về tùy thuộc vào trạng thái nào chúng ta có, như được hiển thị trong Danh sách 18-18:

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(self)
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

// --snip--

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    // --snip--
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

Chúng ta thêm một triển khai mặc định cho phương thức content trả về một lát cắt chuỗi rỗng. Điều đó có nghĩa là chúng ta không cần triển khai content trên các struct DraftPendingReview. Struct Published sẽ ghi đè phương thức content và trả về giá trị trong post.content.

Lưu ý rằng chúng ta cần chú thích thời gian sống cho phương thức này, như chúng ta đã thảo luận trong Chương 10. Chúng ta đang lấy một tham chiếu đến một post làm đối số và trả về một tham chiếu đến một phần của post đó, vì vậy thời gian sống của tham chiếu trả về có liên quan đến thời gian sống của đối số post.

Và chúng ta đã hoàn thành—tất cả Danh sách 18-11 hiện hoạt động! Chúng ta đã triển khai mẫu trạng thái với các quy tắc của quy trình làm việc bài đăng blog. Logic liên quan đến các quy tắc nằm trong các đối tượng trạng thái thay vì bị phân tán khắp Post.

Tại sao Không Phải Một Enum?

Bạn có thể đã tự hỏi tại sao chúng ta không sử dụng một enum với các biến thể trạng thái bài đăng khác nhau. Đó chắc chắn là một giải pháp có thể; hãy thử nó và so sánh kết quả cuối cùng để xem bạn thích cái nào hơn! Một nhược điểm của việc sử dụng enum là mỗi nơi kiểm tra giá trị của enum sẽ cần một biểu thức match hoặc tương tự để xử lý mọi biến thể có thể. Điều này có thể trở nên lặp lại hơn so với giải pháp đối tượng trait này.

Nhược điểm của Mẫu Trạng thái

Chúng ta đã chỉ ra rằng Rust có khả năng triển khai mẫu trạng thái hướng đối tượng để đóng gói các loại hành vi khác nhau mà một bài đăng nên có trong mỗi trạng thái. Các phương thức trên Post không biết gì về các hành vi khác nhau. Cách chúng ta tổ chức mã, chúng ta chỉ phải nhìn vào một nơi để biết các cách khác nhau mà một bài đăng đã xuất bản có thể hành xử: triển khai của trait State trên struct Published.

Nếu chúng ta tạo một triển khai thay thế không sử dụng mẫu trạng thái, thay vào đó chúng ta có thể sử dụng các biểu thức match trong các phương thức trên Post hoặc thậm chí trong mã main kiểm tra trạng thái của bài đăng và thay đổi hành vi ở những nơi đó. Điều đó có nghĩa là chúng ta sẽ phải nhìn vào nhiều nơi để hiểu tất cả các hàm ý của việc một bài đăng ở trạng thái đã xuất bản! Điều này sẽ chỉ tăng lên khi chúng ta thêm nhiều trạng thái hơn: mỗi biểu thức match đó sẽ cần thêm một nhánh.

Với mẫu trạng thái, các phương thức Post và những nơi chúng ta sử dụng Post không cần các biểu thức match, và để thêm một trạng thái mới, chúng ta chỉ cần thêm một struct mới và triển khai các phương thức trait trên struct đó.

Việc triển khai sử dụng mẫu trạng thái rất dễ mở rộng để thêm chức năng. Để thấy sự đơn giản của việc duy trì mã sử dụng mẫu trạng thái, hãy thử một vài gợi ý này:

  • Thêm một phương thức reject thay đổi trạng thái của bài đăng từ PendingReview trở lại Draft.
  • Yêu cầu hai lần gọi approve trước khi trạng thái có thể được thay đổi thành Published.
  • Chỉ cho phép người dùng thêm nội dung văn bản khi bài đăng ở trạng thái Draft. Gợi ý: để đối tượng trạng thái chịu trách nhiệm cho những gì có thể thay đổi về nội dung nhưng không chịu trách nhiệm sửa đổi Post.

Một nhược điểm của mẫu trạng thái là, vì các trạng thái triển khai các chuyển đổi giữa các trạng thái, một số trạng thái được kết nối với nhau. Nếu chúng ta thêm một trạng thái khác giữa PendingReviewPublished, chẳng hạn như Scheduled, chúng ta sẽ phải thay đổi mã trong PendingReview để chuyển sang Scheduled thay vì Published. Sẽ ít công việc hơn nếu PendingReview không cần thay đổi với việc thêm một trạng thái mới, nhưng điều đó có nghĩa là chuyển sang một mẫu thiết kế khác.

Một nhược điểm khác là chúng ta đã lặp lại một số logic. Để loại bỏ một số sự lặp lại, chúng ta có thể thử tạo các triển khai mặc định cho các phương thức request_reviewapprove trên trait State trả về self; tuy nhiên, điều này sẽ không hoạt động: khi sử dụng State như một đối tượng trait, trait không biết self cụ thể sẽ là gì chính xác, vì vậy kiểu trả về không được biết tại thời điểm biên dịch. (Đây là một trong những quy tắc tương thích dyn được đề cập trước đó.)

Sự lặp lại khác bao gồm các triển khai tương tự của các phương thức request_reviewapprove trên Post. Cả hai phương thức đều sử dụng Option::take với trường state của Post, và nếu stateSome, chúng ủy quyền cho triển khai của cùng một phương thức trong giá trị được bọc và đặt giá trị mới của trường state thành kết quả. Nếu chúng ta có nhiều phương thức trên Post tuân theo mẫu này, chúng ta có thể cân nhắc định nghĩa một macro để loại bỏ sự lặp lại (xem "Macro" trong Chương 20).

Bằng cách triển khai mẫu trạng thái chính xác như được định nghĩa cho các ngôn ngữ hướng đối tượng, chúng ta không tận dụng hết các điểm mạnh của Rust như chúng ta có thể. Hãy xem xét một số thay đổi chúng ta có thể thực hiện đối với crate blog có thể biến các trạng thái và chuyển đổi không hợp lệ thành các lỗi thời điểm biên dịch.

Mã hóa Trạng thái và Hành vi dưới dạng Kiểu

Chúng ta sẽ chỉ cho bạn cách suy nghĩ lại về mẫu trạng thái để có được một tập hợp các đánh đổi khác. Thay vì đóng gói hoàn toàn các trạng thái và chuyển đổi để mã bên ngoài không có kiến thức về chúng, chúng ta sẽ mã hóa các trạng thái thành các kiểu khác nhau. Do đó, hệ thống kiểm tra kiểu của Rust sẽ ngăn chặn các nỗ lực sử dụng bài đăng nháp ở những nơi chỉ có bài đăng đã xuất bản được phép bằng cách đưa ra lỗi trình biên dịch.

Hãy xem xét phần đầu tiên của main trong Danh sách 18-11:

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

Chúng ta vẫn cho phép tạo bài đăng mới ở trạng thái nháp bằng Post::new và khả năng thêm văn bản vào nội dung của bài đăng. Nhưng thay vì có một phương thức content trên bài đăng nháp trả về một chuỗi rỗng, chúng ta sẽ làm cho bài đăng nháp không có phương thức content chút nào. Bằng cách đó, nếu chúng ta cố gắng lấy nội dung của một bài đăng nháp, chúng ta sẽ nhận được lỗi trình biên dịch cho chúng ta biết rằng phương thức không tồn tại. Kết quả là, sẽ không thể cho chúng ta vô tình hiển thị nội dung bài đăng nháp trong sản xuất vì mã đó thậm chí sẽ không biên dịch. Danh sách 18-19 hiển thị định nghĩa của một struct Post và một struct DraftPost, cũng như các phương thức trên mỗi struct.

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

Cả hai struct PostDraftPost đều có một trường riêng tư content lưu trữ văn bản của bài đăng blog. Các struct không còn có trường state vì chúng ta đang di chuyển việc mã hóa trạng thái vào các kiểu của struct. Struct Post sẽ đại diện cho một bài đăng đã xuất bản, và nó có một phương thức content trả về content.

Chúng ta vẫn có một hàm Post::new, nhưng thay vì trả về một thực thể của Post, nó trả về một thực thể của DraftPost. Vì content là riêng tư và không có bất kỳ hàm nào trả về Post, hiện tại không thể tạo một thực thể của Post.

Struct DraftPost có một phương thức add_text, vì vậy chúng ta có thể thêm văn bản vào content như trước, nhưng lưu ý rằng DraftPost không có phương thức content được định nghĩa! Vì vậy, bây giờ chương trình đảm bảo tất cả các bài đăng bắt đầu là bài đăng nháp, và bài đăng nháp không có nội dung của chúng sẵn có để hiển thị. Bất kỳ nỗ lực nào để vượt qua các ràng buộc này sẽ dẫn đến lỗi trình biên dịch.

Triển khai Chuyển đổi dưới dạng Chuyển đổi thành Các Kiểu Khác nhau

Vậy làm thế nào để chúng ta có được một bài đăng đã xuất bản? Chúng ta muốn thực thi quy tắc rằng một bài đăng nháp phải được xem xét và phê duyệt trước khi nó có thể được xuất bản. Một bài đăng trong trạng thái chờ xem xét vẫn không nên hiển thị bất kỳ nội dung nào. Hãy triển khai các ràng buộc này bằng cách thêm một struct khác, PendingReviewPost, định nghĩa phương thức request_review trên DraftPost để trả về một PendingReviewPost và định nghĩa một phương thức approve trên PendingReviewPost để trả về một Post, như được hiển thị trong Danh sách 18-20.

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    // --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}

Các phương thức request_reviewapprove lấy quyền sở hữu của self, do đó tiêu thụ các thực thể DraftPostPendingReviewPost và chuyển đổi chúng thành một PendingReviewPost và một Post đã xuất bản, tương ứng. Bằng cách này, chúng ta sẽ không có bất kỳ thực thể DraftPost nào còn tồn tại sau khi chúng ta đã gọi request_review trên chúng, và tương tự. Struct PendingReviewPost không có phương thức content được định nghĩa trên nó, vì vậy việc cố gắng đọc nội dung của nó dẫn đến lỗi trình biên dịch, cũng giống như với DraftPost. Vì cách duy nhất để có được một thực thể Post đã xuất bản có phương thức content được định nghĩa là gọi phương thức approve trên một PendingReviewPost, và cách duy nhất để có được một PendingReviewPost là gọi phương thức request_review trên một DraftPost, chúng ta đã mã hóa quy trình làm việc của bài đăng blog vào hệ thống kiểu.

Nhưng chúng ta cũng phải thực hiện một số thay đổi nhỏ đối với main. Các phương thức request_reviewapprove trả về các thực thể mới thay vì sửa đổi struct mà chúng được gọi trên, vì vậy chúng ta cần thêm nhiều phép gán let post = che khuất để lưu các thực thể trả về. Chúng ta cũng không thể có các xác nhận về nội dung của bài đăng nháp và bài đăng đang chờ xem xét là các chuỗi rỗng, và chúng ta cũng không cần chúng: chúng ta không thể biên dịch mã cố gắng sử dụng nội dung của bài đăng ở những trạng thái đó nữa. Mã đã cập nhật trong main được hiển thị trong Danh sách 18-21.

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

Các thay đổi chúng ta cần thực hiện đối với main để gán lại post có nghĩa là việc triển khai này không hoàn toàn tuân theo mẫu trạng thái hướng đối tượng nữa: các chuyển đổi giữa các trạng thái không còn được đóng gói hoàn toàn trong triển khai Post. Tuy nhiên, lợi ích của chúng ta là các trạng thái không hợp lệ hiện không thể xảy ra nhờ vào hệ thống kiểu và việc kiểm tra kiểu xảy ra tại thời điểm biên dịch! Điều này đảm bảo rằng các lỗi nhất định, chẳng hạn như hiển thị nội dung của một bài đăng chưa xuất bản, sẽ được phát hiện trước khi chúng xuất hiện trong sản xuất.

Thử các nhiệm vụ được đề xuất ở đầu phần này trên crate blog như nó là sau Danh sách 18-21 để xem bạn nghĩ gì về thiết kế của phiên bản mã này. Lưu ý rằng một số nhiệm vụ có thể đã được hoàn thành trong thiết kế này.

Chúng ta đã thấy rằng mặc dù Rust có khả năng triển khai các mẫu thiết kế hướng đối tượng, các mẫu khác, chẳng hạn như mã hóa trạng thái vào hệ thống kiểu, cũng có sẵn trong Rust. Các mẫu này có các đánh đổi khác nhau. Mặc dù bạn có thể rất quen thuộc với các mẫu hướng đối tượng, việc suy nghĩ lại về vấn đề để tận dụng các tính năng của Rust có thể mang lại lợi ích, chẳng hạn như ngăn chặn một số lỗi tại thời điểm biên dịch. Các mẫu hướng đối tượng sẽ không phải lúc nào cũng là giải pháp tốt nhất trong Rust do một số tính năng nhất định, như quyền sở hữu, mà các ngôn ngữ hướng đối tượng không có.

Tóm tắt

Bất kể việc bạn có cho rằng Rust là một ngôn ngữ hướng đối tượng sau khi đọc chương này hay không, bây giờ bạn biết rằng bạn có thể sử dụng các đối tượng trait để có được một số tính năng hướng đối tượng trong Rust. Điều phối động có thể cung cấp cho mã của bạn một số tính linh hoạt để đổi lấy một chút hiệu suất thời gian chạy. Bạn có thể sử dụng tính linh hoạt này để triển khai các mẫu hướng đối tượng có thể giúp cải thiện khả năng bảo trì của mã. Rust cũng có các tính năng khác, như quyền sở hữu, mà các ngôn ngữ hướng đối tượng không có. Một mẫu hướng đối tượng sẽ không phải lúc nào cũng là cách tốt nhất để tận dụng các điểm mạnh của Rust, nhưng đó là một tùy chọn có sẵn.

Tiếp theo, chúng ta sẽ xem xét các mẫu, là một tính năng khác của Rust cho phép nhiều tính linh hoạt. Chúng ta đã xem xét chúng một cách ngắn gọn trong suốt cuốn sách nhưng chưa thấy được khả năng đầy đủ của chúng. Hãy tiếp tục!