Implement Design Pattern แบบ OOP
State pattern คือ design pattern แบบ object-oriented แก่นของ pattern คือเรานิยามชุดของ state ที่ค่ามีภายในได้ State ถูก represent โดยชุดของ state object และพฤติกรรมของค่าเปลี่ยนตาม state ของมัน เราจะทำตัวอย่าง struct blog post ที่มี field บรรจุ state ซึ่งจะเป็น state object จากชุด “draft”, “review” หรือ “published”
State object แชร์ functionality — ใน Rust แน่นอน เราใช้ struct และ trait แทน object และ inheritance State object แต่ละตัวรับผิดชอบพฤติกรรมของ ตัวเองและการกำกับว่าเมื่อไรควรเปลี่ยนเป็น state อื่น ค่าที่บรรจุ state object ไม่รู้อะไรเกี่ยวกับพฤติกรรมต่างกันของ state หรือเมื่อจะ transition ระหว่าง state
ข้อได้เปรียบของการใช้ state pattern คือ เมื่อข้อกำหนด business ของ โปรแกรมเปลี่ยน เราไม่ต้องเปลี่ยนโค้ดของค่าที่บรรจุ state หรือโค้ดที่ใช้ ค่า เราจะต้องอัพเดทโค้ดภายในหนึ่งใน state object เพื่อเปลี่ยนกฎของมัน หรือเพิ่ม state object เท่านั้น
ก่อนอื่น เราจะ implement state pattern ในแบบ object-oriented ดั้งเดิม มากขึ้น แล้วเราจะใช้แนวทางที่ natural กว่าใน Rust เล็กน้อย มาเจาะดู implement workflow blog post แบบเพิ่มทีละขั้นโดยใช้ state pattern
Functionality สุดท้ายจะเป็นแบบนี้:
- blog post เริ่มเป็น draft ว่าง
- เมื่อ draft เสร็จ มีการขอ review ของ post
- เมื่อ post ถูก approve มันถูก publish
- เพียง blog post ที่ publish แล้ว return content เพื่อ print ดังนั้น post ที่ไม่ approve ไม่สามารถถูก publish โดยบังเอิญได้
การเปลี่ยนแปลงอื่นใดที่พยายามทำบน post ไม่ควรมีผล ตัวอย่างเช่น ถ้าเรา พยายาม approve blog post ที่เป็น draft ก่อนเราขอ review post ควรยังเป็น draft ที่ไม่ได้ publish
ลองสไตล์ Object-Oriented แบบดั้งเดิม
มีวิธีไม่จำกัดในการจัดโครงสร้างโค้ดเพื่อแก้ปัญหาเดียวกัน แต่ละวิธีมี trade-off ต่างกัน Implementation ของส่วนนี้เป็นสไตล์ object-oriented แบบดั้งเดิมมากกว่า ซึ่งเป็นไปได้ในการเขียนใน Rust แต่ไม่ใช้ประโยชน์จาก จุดแข็งบางอย่างของ Rust ภายหลัง เราจะแสดงวิธีแก้ที่ต่างที่ยังใช้ design pattern แบบ object-oriented แต่ถูกจัดโครงสร้างในวิธีที่อาจดูคุ้นเคยน้อย กว่ากับ programmer ที่มีประสบการณ์ object-oriented เราจะเปรียบเทียบสอง วิธีแก้เพื่อสัมผัส trade-off ของการออกแบบโค้ด Rust ต่างจากโค้ดในภาษาอื่น
Listing 18-11 แสดง workflow นี้ในรูปแบบโค้ด — นี่คือตัวอย่างการใช้ API
ที่เราจะ implement ใน library crate ชื่อ blog นี่จะยังไม่ compile
เพราะเรายังไม่ได้ implement 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());
}
blog ของเรามีเราต้องการอนุญาตให้ user สร้าง blog post draft ใหม่ด้วย Post::new เรา
ต้องการอนุญาตให้เพิ่ม text ให้ blog post ถ้าเราพยายามได้ content ของ
post ทันที ก่อน approval เราไม่ควรได้ text ใดเพราะ post ยังเป็น draft
เราเพิ่ม assert_eq! ในโค้ดเพื่อจุดประสงค์สาธิต unit test ที่ยอดเยี่ยม
สำหรับสิ่งนี้คือ assert ว่า blog post draft return string ว่างจากเมธอด
content แต่เราจะไม่เขียน test สำหรับตัวอย่างนี้
ถัดไป เราต้องการเปิดใช้คำขอ review ของ post และเราต้องการให้ content
return string ว่างระหว่างรอ review เมื่อ post ได้รับการ approve มันควร
ถูก publish หมายความว่า text ของ post จะถูก return เมื่อ content ถูก
เรียก
สังเกตว่า type เดียวที่เรา interact กับ crate คือ type Post Type นี้
จะใช้ state pattern และจะบรรจุค่าที่จะเป็นหนึ่งใน state object สามตัวที่
represent state ต่างกันที่ post เป็นได้ — draft, review หรือ published
การเปลี่ยนจาก state หนึ่งเป็นอีกอันจะถูกจัดการภายในใน type Post
State เปลี่ยน response ต่อเมธอดที่เรียกโดย user ของ library ของเราบน
instance Post แต่พวกเขาไม่ต้องจัดการการเปลี่ยน state โดยตรง นอกจากนี้
user ไม่สามารถทำผิดกับ state เช่น publishing post ก่อนมันถูก review
นิยาม Post และสร้าง Instance ใหม่
มาเริ่ม implement library กัน! เรารู้ว่าเราต้องการ struct Post แบบ
public ที่บรรจุ content ดังนั้นเราจะเริ่มด้วยนิยามของ struct และฟังก์ชัน
public new ที่ associate เพื่อสร้าง instance ของ Post ดังที่แสดงใน
Listing 18-12 เราจะทำ trait private State ที่จะนิยามพฤติกรรมที่
state object ทั้งหมดสำหรับ Post ต้องมีด้วย
แล้ว Post จะบรรจุ trait object ของ Box<dyn State> ภายใน Option<T>
ใน field private ชื่อ state เพื่อบรรจุ state object คุณจะเห็นว่า
ทำไม Option<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 {}
Post และฟังก์ชัน new ที่สร้าง instance Post ใหม่, trait State และ struct DraftTrait State นิยามพฤติกรรมที่แชร์โดย post state ต่างกัน State object คือ
Draft, PendingReview และ Published และพวกมันทั้งหมดจะ implement
trait State ตอนนี้ trait ยังไม่มีเมธอด และเราจะเริ่มโดยนิยามเพียง
state Draft เพราะนั่นคือ state ที่เราต้องการให้ post เริ่ม
เมื่อเราสร้าง Post ใหม่ เราตั้ง field state ของมันเป็นค่า Some ที่
บรรจุ Box Box นี้ point ไปยัง instance ใหม่ของ struct Draft นี่
รับประกันว่าเมื่อใดก็ตามที่เราสร้าง instance ใหม่ของ Post มันจะเริ่ม
เป็น draft เพราะ field state ของ Post เป็น private ไม่มีวิธีสร้าง
Post ใน state อื่นใด! ในฟังก์ชัน Post::new เราตั้ง field content
เป็น String ว่างใหม่
เก็บ Text ของ Content ของ Post
เราเห็นใน Listing 18-11 ว่าเราต้องการสามารถเรียกเมธอดชื่อ add_text และ
ส่ง &str ที่ถูกเพิ่มเป็น text content ของ blog post เรา implement
สิ่งนี้เป็นเมธอด แทนที่จะเปิดเผย field content เป็น pub ดังนั้น
ภายหลังเราสามารถ implement เมธอดที่จะควบคุมวิธีที่ data ของ field
content ถูกอ่าน เมธอด add_text ค่อนข้างตรงไปตรงมา ดังนั้นมาเพิ่ม
implementation ใน Listing 18-13 ให้ block 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 {}
add_text เพื่อเพิ่ม text ให้ content ของ postเมธอด add_text รับ mutable reference ของ self เพราะเรากำลังเปลี่ยน
instance Post ที่เราเรียก add_text บนมัน แล้วเราเรียก push_str บน
String ใน content และส่ง argument text เพื่อเพิ่มให้ content ที่
save พฤติกรรมนี้ไม่ขึ้นกับ state ที่ post เป็น ดังนั้นมันไม่ใช่ส่วนของ
state pattern เมธอด add_text ไม่ interact กับ field state เลย แต่
มันเป็นส่วนของพฤติกรรมที่เราต้องการสนับสนุน
ทำให้แน่ใจว่า Content ของ Draft Post ว่าง
แม้หลังจากที่เราเรียก add_text และเพิ่ม content บ้างให้ post ของเรา
เรายังต้องการให้เมธอด content return string slice ว่างเพราะ post ยัง
อยู่ใน state draft ดังที่แสดงโดย assert_eq! ตัวแรกใน Listing 18-11
ตอนนี้ มา implement เมธอด content ด้วยสิ่งที่ง่ายที่สุดที่จะ fulfill
ข้อกำหนดนี้ — return string slice ว่างเสมอ เราจะเปลี่ยนนี่ภายหลังเมื่อ
เรา implement ความสามารถในการเปลี่ยน state ของ post ได้เพื่อให้มันถูก
publish ตอนนี้ post ได้เพียงใน state draft ดังนั้น post content ควร
ว่างเสมอ Listing 18-14 แสดง implementation ตัวยืน
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 {}
content บน Post ที่ return string slice ว่างเสมอด้วยเมธอด content ที่เพิ่มนี้ ทุกอย่างใน Listing 18-11 ผ่าน assert_eq!
ตัวแรกทำงานตามตั้งใจ
Request Review ที่เปลี่ยน State ของ Post
ถัดไป เราต้องเพิ่ม functionality เพื่อขอ review ของ post ซึ่งควรเปลี่ยน
state ของมันจาก Draft เป็น PendingReview Listing 18-15 แสดงโค้ดนี้
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
}
}
request_review บน Post และ trait Stateเราให้ Post เมธอด public ชื่อ request_review ที่จะรับ mutable
reference ของ self แล้วเราเรียกเมธอด request_review ภายในบน state
ปัจจุบันของ Post และเมธอด request_review ตัวที่สองนี้ consume state
ปัจจุบันและ return state ใหม่
เราเพิ่มเมธอด request_review ให้ trait State — type ทั้งหมดที่
implement trait จะต้อง implement เมธอด request_review แล้ว สังเกตว่า
แทนที่จะมี self, &self หรือ &mut self เป็น parameter แรกของเมธอด
เรามี self: Box<Self> Syntax นี้หมายความว่าเมธอด valid เพียงเมื่อ
เรียกบน Box ที่บรรจุ type Syntax นี้รับ ownership ของ Box<Self>
ทำให้ state เก่า invalid เพื่อให้ค่า state ของ Post transform เป็น
state ใหม่ได้
เพื่อ consume state เก่า เมธอด request_review ต้องรับ ownership ของ
ค่า state นี่คือที่ Option ใน field state ของ Post เข้ามา — เรา
เรียกเมธอด take เพื่อรับค่า Some ออกจาก field state และทิ้ง None
ในที่ของมันเพราะ Rust ไม่ให้เรามี field ที่ไม่มีค่าใน struct นี่ให้เรา
ย้ายค่า state ออกจาก Post แทนที่จะ borrow มัน แล้วเราจะตั้งค่า
state ของ post เป็นผลของ operation นี้
เราต้องตั้ง state เป็น None ชั่วคราวแทนที่จะตั้งโดยตรงด้วยโค้ดเช่น
self.state = self.state.request_review(); เพื่อรับ ownership ของค่า
state นี่รับประกันว่า Post ไม่สามารถใช้ค่า state เก่าหลังจากที่
เรา transform มันเป็น state ใหม่
เมธอด request_review บน Draft return instance boxed ใหม่ของ struct
PendingReview ใหม่ ซึ่ง represent state เมื่อ post รอ review struct
PendingReview ก็ implement เมธอด request_review แต่ไม่ทำการ
transformation ใด แทน มัน return ตัวเองเพราะเมื่อเราขอ review บน post
ที่อยู่ใน state PendingReview แล้ว มันควรอยู่ใน state PendingReview
ตอนนี้เราเริ่มเห็นข้อได้เปรียบของ state pattern — เมธอด request_review
บน Post เหมือนกันไม่ว่าค่า state ของมันจะเป็นอะไร State แต่ละตัวรับผิดชอบ
กฎของตัวเอง
เราจะปล่อยเมธอด content บน Post เป็นเหมือนเดิม return string slice
ว่าง ตอนนี้เราสามารถมี Post ใน state PendingReview รวมทั้งใน state
Draft แต่เราต้องการพฤติกรรมเดียวกันใน state PendingReview Listing
18-11 ตอนนี้ทำงานได้ถึง call assert_eq! ตัวที่สอง!
เพิ่ม approve เพื่อเปลี่ยนพฤติกรรมของ content
เมธอด approve จะคล้ายกับเมธอด request_review — มันจะตั้ง state เป็น
ค่าที่ state ปัจจุบันบอกว่ามันควรมีเมื่อ state นั้นถูก approve ดังที่
แสดงใน Listing 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
}
}
approve บน Post และ trait Stateเราเพิ่มเมธอด approve ให้ trait State และเพิ่ม struct ใหม่ที่
implement State, state Published
คล้ายกับวิธีที่ request_review บน PendingReview ทำงาน ถ้าเราเรียก
เมธอด approve บน Draft มันจะไม่มีผลเพราะ approve จะ return self
เมื่อเราเรียก approve บน PendingReview มัน return instance boxed
ใหม่ของ struct Published struct Published implement trait State
และสำหรับทั้งเมธอด request_review และเมธอด approve มัน return
ตัวเองเพราะ post ควรอยู่ใน state Published ในกรณีเหล่านั้น
ตอนนี้เราต้องอัพเดทเมธอด content บน Post เราต้องการให้ค่าที่ return
จาก content ขึ้นกับ state ปัจจุบันของ Post ดังนั้นเราจะให้ Post
delegate ให้เมธอด content ที่นิยามบน state ของมัน ดังที่แสดงใน
Listing 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
}
}
content บน Post เพื่อ delegate ให้เมธอด content บน Stateเพราะเป้าหมายคือให้กฎทั้งหมดเหล่านี้อยู่ภายใน struct ที่ implement
State เราเรียกเมธอด content บนค่าใน state และส่ง instance post
(นั่นคือ self) เป็น argument แล้วเรา return ค่าที่ return จากการใช้
เมธอด content บนค่า state
เราเรียกเมธอด as_ref บน Option เพราะเราต้องการ reference ของค่า
ภายใน Option แทนที่จะเป็น ownership ของค่า เพราะ state เป็น
Option<Box<dyn State>> เมื่อเราเรียก as_ref, Option<&Box<dyn State>>
ถูก return ถ้าเราไม่เรียก as_ref เราจะได้ error เพราะเราไม่สามารถย้าย
state ออกจาก &self ที่ถูก borrow ของ parameter ฟังก์ชัน
แล้วเราเรียกเมธอด unwrap ซึ่งเรารู้ว่าจะไม่ panic เพราะเรารู้ว่าเมธอด
บน Post รับประกันว่า state จะบรรจุค่า Some เสมอเมื่อเมธอดเหล่านั้น
เสร็จ นี่คือหนึ่งในกรณีที่เราพูดถึงในส่วน “เมื่อคุณมีข้อมูลมากกว่า
Compiler” ของบทที่ 9 เมื่อเรารู้
ว่าค่า None ไม่เป็นไปได้ แม้ compiler ไม่สามารถเข้าใจสิ่งนั้น
ที่จุดนี้ เมื่อเราเรียก content บน &Box<dyn State> deref coercion
จะมีผลบน & และ Box ดังนั้นเมธอด content จะถูกเรียกท้ายสุดบน type
ที่ implement trait State นั่นหมายความว่าเราต้องเพิ่ม content ให้
นิยาม trait State และนั่นคือที่เราจะใส่ logic สำหรับ content ที่จะ
return ขึ้นกับ state ใดที่เรามี ดังที่แสดงใน Listing 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
}
}
content ให้ trait Stateเราเพิ่ม implementation เริ่มต้นสำหรับเมธอด content ที่ return string
slice ว่าง นั่นหมายความว่าเราไม่ต้อง implement content บน struct
Draft และ PendingReview struct Published จะ override เมธอด
content และ return ค่าใน post.content แม้จะสะดวก การมีเมธอด
content บน State ตัดสิน content ของ Post คือทำให้เส้นระหว่างความ
รับผิดชอบของ State และความรับผิดชอบของ Post เบลอ
สังเกตว่าเราต้องการ lifetime annotation บนเมธอดนี้ ตามที่เราพูดถึงใน
บทที่ 10 เรากำลังรับ reference ของ post เป็น argument และ return
reference ของส่วนของ post นั้น ดังนั้น lifetime ของ reference ที่
return เกี่ยวกับ lifetime ของ argument post
และเราเสร็จแล้ว — Listing 18-11 ทั้งหมดทำงานแล้ว! เรา implement state
pattern ด้วยกฎของ workflow blog post Logic ที่เกี่ยวกับกฎอยู่ใน state
object แทนที่จะกระจายผ่าน Post
ทำไมไม่ใช้ Enum?
คุณอาจสงสัยว่าทำไมเราไม่ใช้ enum กับ post state ที่เป็นไปได้ต่างกัน
เป็น variant นั่นเป็นวิธีแก้ที่เป็นไปได้แน่นอน ลองมันและเปรียบเทียบ
ผลสุดท้ายเพื่อดูว่าคุณชอบอันไหน! ข้อเสียอย่างหนึ่งของการใช้ enum คือ
ทุกที่ที่ check ค่าของ enum จะต้องการ expression match หรือคล้ายกัน
เพื่อจัดการทุก variant ที่เป็นไปได้ นี่ซ้ำซ้อนกว่าวิธีแก้ trait
object นี้ได้
ประเมิน State Pattern
เราแสดงว่า Rust สามารถ implement state pattern แบบ object-oriented ได้
เพื่อ encapsulate พฤติกรรมต่างชนิดที่ post ควรมีในแต่ละ state เมธอดบน
Post ไม่รู้อะไรเกี่ยวกับพฤติกรรมต่างกัน เพราะวิธีที่เราจัด organize
โค้ด เราต้องดูเพียงที่เดียวเพื่อรู้วิธีต่างกันที่ published post ทำตัว —
implementation ของ trait State บน struct Published
ถ้าเราจะสร้าง implementation ทางเลือกที่ไม่ใช้ state pattern เราอาจใช้
expression match ในเมธอดบน Post หรือแม้ในโค้ด main ที่ check
state ของ post และเปลี่ยนพฤติกรรมในที่เหล่านั้นแทน นั่นหมายความว่าเราจะ
ต้องดูในหลายที่เพื่อเข้าใจ implication ทั้งหมดของ post ที่อยู่ใน state
published
ด้วย state pattern เมธอด Post และที่ที่เราใช้ Post ไม่ต้องการ
expression match และเพื่อเพิ่ม state ใหม่ เราจะต้องการเพียงเพิ่ม
struct ใหม่และ implement เมธอด trait บน struct นั้นในที่เดียว
Implementation ที่ใช้ state pattern ขยายเพื่อเพิ่ม functionality ได้ ง่าย เพื่อดูความเรียบง่ายของการ maintain โค้ดที่ใช้ state pattern ลอง ข้อแนะนำเหล่านี้บ้าง:
- เพิ่มเมธอด
rejectที่เปลี่ยน state ของ post จากPendingReviewกลับ ไปDraft - ต้องการการเรียก
approveสองครั้งก่อน state สามารถเปลี่ยนเป็นPublished - อนุญาตให้ user เพิ่ม text content เพียงเมื่อ post อยู่ใน state
DraftHint: ให้ state object รับผิดชอบสิ่งที่อาจเปลี่ยนเกี่ยวกับ content แต่ไม่รับผิดชอบการแก้Post
ข้อเสียอย่างหนึ่งของ state pattern คือ เพราะ state implement transition
ระหว่าง state บาง state ถูก couple กันและกัน ถ้าเราเพิ่ม state อีกอัน
ระหว่าง PendingReview และ Published เช่น Scheduled เราจะต้อง
เปลี่ยนโค้ดใน PendingReview เพื่อ transition เป็น Scheduled แทน
งานจะน้อยกว่าถ้า PendingReview ไม่ต้องเปลี่ยนกับการเพิ่ม state ใหม่
แต่นั่นจะหมายความว่าต้อง switch ไป design pattern อื่น
ข้อเสียอีกอย่างคือเรา duplicate logic บางส่วน เพื่อตัด duplication
บางส่วน เราอาจลองทำ implementation เริ่มต้นสำหรับเมธอด request_review
และ approve บน trait State ที่ return self อย่างไรก็ตาม นี่จะไม่
ทำงาน — เมื่อใช้ State เป็น trait object trait ไม่รู้ว่า self
concrete จะเป็นอะไรแน่ ๆ ดังนั้น return type ไม่รู้ที่ compile time
(นี่คือหนึ่งในกฎ dyn compatibility ที่กล่าวก่อนหน้า)
Duplication อื่นรวม implementation คล้ายกันของเมธอด request_review
และ approve บน Post ทั้งเมธอดใช้ Option::take กับ field state
ของ Post และถ้า state เป็น Some พวกมัน delegate ให้ implementation
ของเมธอดเดียวกันของค่า wrap และตั้งค่าใหม่ของ field state เป็นผล ถ้า
เรามีเมธอดเยอะบน Post ที่ตามแบบนี้ เราอาจพิจารณานิยาม macro เพื่อ
ขจัด repetition (ดูส่วน “Macros” ในบทที่ 20)
โดยการ implement state pattern ตรงตามที่นิยามสำหรับภาษา object-oriented
เราไม่ใช้ประโยชน์จากจุดแข็งของ Rust เต็มที่เท่าที่เราทำได้ มาดูการ
เปลี่ยนแปลงบ้างที่เราทำกับ crate blog ได้ที่ทำให้ state และ
transition ที่ invalid เป็น compile-time error
Encode State และพฤติกรรมเป็น Type
เราจะแสดงให้คุณเห็นว่าคิดใหม่ state pattern เพื่อได้ชุด trade-off ต่างยังไง แทนที่จะ encapsulate state และ transition โดยสมบูรณ์เพื่อให้โค้ดภายนอก ไม่มีความรู้เกี่ยวกับพวกมัน เราจะ encode state ลงใน type ต่างกัน ตามนั้น ระบบ type-checking ของ Rust จะป้องกันความพยายามใช้ draft post ที่เพียง published post ถูกอนุญาตโดยออก error compiler
มาพิจารณาส่วนแรกของ main ใน Listing 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());
}
เรายังเปิดใช้การสร้าง post ใหม่ใน state draft โดยใช้ Post::new และ
ความสามารถในการเพิ่ม text ให้ content ของ post แต่แทนที่จะมีเมธอด
content บน draft post ที่ return string ว่าง เราจะทำให้ draft post
ไม่มีเมธอด content เลย ด้วยวิธีนั้น ถ้าเราพยายามได้ content ของ draft
post เราจะได้ error compiler บอกเราว่าเมธอดไม่มี ผลคือ มันจะเป็นไปไม่ได้
สำหรับเราที่จะแสดง content draft post โดยบังเอิญใน production เพราะโค้ด
นั้นจะไม่ compile เลย Listing 18-19 แสดงนิยามของ struct Post และ
struct DraftPost รวมทั้งเมธอดบนแต่ละ
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);
}
}
Post ที่มีเมธอด content และ DraftPost โดยไม่มีเมธอด contentทั้ง struct Post และ DraftPost มี field private content ที่เก็บ
text blog post struct ไม่มี field state แล้วเพราะเราย้าย encoding
ของ state ไปยัง type ของ struct struct Post จะ represent published
post และมีเมธอด content ที่ return content
เรายังมีฟังก์ชัน Post::new แต่แทนที่จะ return instance ของ Post มัน
return instance ของ DraftPost เพราะ content เป็น private และไม่มี
ฟังก์ชันใดที่ return Post มันไม่เป็นไปได้ที่จะสร้าง instance ของ
Post ตอนนี้
Struct DraftPost มีเมธอด add_text ดังนั้นเราเพิ่ม text ให้ content
เหมือนเดิมได้ แต่สังเกตว่า DraftPost ไม่มีเมธอด content ที่นิยาม!
ดังนั้นตอนนี้โปรแกรมรับประกันว่า post ทั้งหมดเริ่มเป็น draft post และ
draft post ไม่มี content ที่ใช้ได้สำหรับการแสดง ความพยายามใด ๆ
ที่จะอ้อมข้อจำกัดเหล่านี้จะให้ผลเป็น error compiler
แล้วเราจะได้ published post ยังไง? เราต้องการบังคับกฎที่ draft post
ต้องถูก review และ approve ก่อนมันถูก publish post ใน state pending
review ควรยังไม่แสดง content ใด มา implement ข้อจำกัดเหล่านี้โดยเพิ่ม
struct อีกอัน PendingReviewPost นิยามเมธอด request_review บน
DraftPost ให้ return PendingReviewPost และนิยามเมธอด approve บน
PendingReviewPost ให้ return Post ดังที่แสดงใน Listing 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,
}
}
}
PendingReviewPost ที่ถูกสร้างโดยเรียก request_review บน DraftPost และเมธอด approve ที่เปลี่ยน PendingReviewPost เป็น Post ที่ publishedเมธอด request_review และ approve รับ ownership ของ self ดังนั้น
consume instance DraftPost และ PendingReviewPost และ transform
พวกมันเป็น PendingReviewPost และ Post ที่ published ตามลำดับ ด้วย
วิธีนี้ เราจะไม่มี instance DraftPost ที่ค้างหลังจากที่เราเรียก
request_review บนพวกมัน และอื่น ๆ Struct PendingReviewPost ไม่มี
เมธอด content ที่นิยามบนมัน ดังนั้นความพยายามอ่าน content ของมันให้ผล
เป็น error compiler เช่นเดียวกับ DraftPost เพราะวิธีเดียวที่จะได้
instance Post ที่ published ที่มีเมธอด content ที่นิยามคือเรียกเมธอด
approve บน PendingReviewPost และวิธีเดียวที่จะได้ PendingReviewPost
คือเรียกเมธอด request_review บน DraftPost ตอนนี้เรา encode workflow
blog post ลงในระบบ type แล้ว
แต่เราต้องทำการเปลี่ยนเล็กน้อยให้ main ด้วย เมธอด request_review และ
approve return instance ใหม่แทนที่จะแก้ struct ที่ถูกเรียกบน ดังนั้น
เราต้องเพิ่ม shadowing assignment let post = อีกเพื่อ save instance
ที่ return เราไม่สามารถมี assertion ว่า content ของ draft และ pending
review post เป็น string ว่าง และเราไม่ต้องการพวกมัน — เราไม่สามารถ
compile โค้ดที่พยายามใช้ content ของ post ใน state เหล่านั้นได้อีกแล้ว
โค้ดอัพเดทใน main แสดงใน Listing 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());
}
main เพื่อใช้ implementation ใหม่ของ workflow blog postการเปลี่ยนที่เราต้องทำให้ main เพื่อ reassign post หมายความว่า
implementation นี้ไม่ตามแบบ state pattern แบบ object-oriented อีกแล้ว —
การ transformation ระหว่าง state ไม่ถูก encapsulate ทั้งหมดภายใน
implementation ของ Post อีก อย่างไรก็ตาม สิ่งที่เราได้คือ state ที่
invalid ตอนนี้เป็นไปไม่ได้เพราะระบบ type และ type checking ที่เกิดที่
compile time! นี่รับประกันว่า bug บางอย่าง เช่น การแสดง content ของ
post ที่ไม่ published จะถูกค้นพบก่อนพวกมันไปถึง production
ลองงานที่แนะนำในตอนเริ่มของส่วนนี้บน crate blog ตามที่เป็นหลัง Listing
18-21 เพื่อดูสิ่งที่คุณคิดเกี่ยวกับการออกแบบของ version นี้ของโค้ด
สังเกตว่างานบางอย่างอาจ complete แล้วในการออกแบบนี้
เราเห็นว่าแม้ Rust สามารถ implement design pattern แบบ object-oriented ได้ pattern อื่น เช่น encoding state ลงในระบบ type ก็ใช้ได้ใน Rust ด้วย Pattern เหล่านี้มี trade-off ต่างกัน แม้คุณอาจคุ้นเคยมากกับ pattern object-oriented การคิดใหม่ปัญหาเพื่อใช้ประโยชน์จากฟีเจอร์ของ Rust ให้ประโยชน์ได้ เช่นป้องกัน bug บางอย่างที่ compile time Pattern object-oriented จะไม่ใช่วิธีแก้ที่ดีที่สุดใน Rust เสมอเพราะฟีเจอร์ บางอย่าง เช่น ownership ที่ภาษา object-oriented ไม่มี
สรุป
ไม่ว่าคุณคิดว่า Rust เป็นภาษา object-oriented หรือไม่หลังจากอ่านบทนี้ ตอนนี้คุณรู้ว่าคุณใช้ trait object เพื่อได้ฟีเจอร์ object-oriented บางส่วนใน Rust ได้ Dynamic dispatch ให้โค้ดของคุณความยืดหยุ่นบ้างเพื่อ แลกกับ performance ที่ runtime เล็กน้อย คุณใช้ความยืดหยุ่นนี้เพื่อ implement pattern object-oriented ที่ช่วย maintainability ของโค้ดของ คุณได้ Rust ยังมีฟีเจอร์อื่น เช่น ownership ที่ภาษา object-oriented ไม่ มี Pattern object-oriented จะไม่ใช่วิธีที่ดีที่สุดในการใช้ประโยชน์จาก จุดแข็งของ Rust เสมอ แต่มันเป็นตัวเลือกที่ใช้ได้
ถัดไป เราจะดู pattern ซึ่งเป็นอีกฟีเจอร์ของ Rust ที่เปิดใช้ความยืดหยุ่น มาก เราดูพวกมันสั้น ๆ ผ่านหนังสือ แต่ยังไม่เห็นความสามารถเต็มของพวกมัน ไปกันเลย!