소개
객체 지향 디자인 패턴 구현에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 게시된 블로그 게시물만 콘텐츠를 반환할 수 있도록 동작을 기반으로 서로 다른 상태 (초안, 검토 및 게시됨) 를 전환하는 블로그 게시물 구조체를 생성하기 위해 객체 지향 디자인에서 상태 패턴 (state pattern) 을 구현합니다.
객체 지향 디자인 패턴 구현
*상태 패턴 (state pattern)*은 객체 지향 디자인 패턴입니다. 이 패턴의 핵심은 값이 내부적으로 가질 수 있는 일련의 상태를 정의하는 것입니다. 상태는 일련의 *상태 객체 (state objects)*로 표현되며, 값의 동작은 해당 상태에 따라 변경됩니다. 우리는 상태를 저장하는 필드를 가진 블로그 게시물 구조체의 예제를 살펴볼 것입니다. 이 필드는 "초안 (draft)", "검토 (review)", 또는 "게시됨 (published)" 중 하나의 상태 객체가 될 것입니다.
상태 객체는 기능을 공유합니다. 물론 Rust 에서는 객체와 상속 대신 구조체 (structs) 와 트레이트 (traits) 를 사용합니다. 각 상태 객체는 자체 동작을 담당하고 다른 상태로 변경되어야 하는 시기를 관리합니다. 상태 객체를 보유하는 값은 상태의 다른 동작이나 상태 간의 전환 시기에 대해 아무것도 알지 못합니다.
상태 패턴을 사용하는 장점은 프로그램의 비즈니스 요구 사항이 변경될 때 상태를 보유하는 값의 코드나 해당 값을 사용하는 코드를 변경할 필요가 없다는 것입니다. 규칙을 변경하거나 더 많은 상태 객체를 추가하려면 상태 객체 중 하나의 내부 코드만 업데이트하면 됩니다.
먼저, 더 전통적인 객체 지향 방식으로 상태 패턴을 구현한 다음, Rust 에서 좀 더 자연스러운 접근 방식을 사용할 것입니다. 상태 패턴을 사용하여 블로그 게시물 워크플로우를 점진적으로 구현해 보겠습니다.
최종 기능은 다음과 같습니다.
- 블로그 게시물은 빈 초안으로 시작합니다.
- 초안이 완료되면 게시물 검토가 요청됩니다.
- 게시물이 승인되면 게시됩니다.
- 게시된 블로그 게시물만 인쇄할 콘텐츠를 반환하므로 승인되지 않은 게시물은 실수로 게시될 수 없습니다.
게시물에 대한 다른 변경 시도는 아무런 효과가 없어야 합니다. 예를 들어, 검토를 요청하기 전에 초안 블로그 게시물을 승인하려고 하면 게시물은 게시되지 않은 초안 상태로 유지되어야 합니다.
Listing 17-11 은 이 워크플로우를 코드 형태로 보여줍니다. 이것은 blog라는 라이브러리 크레이트 (crate) 에서 구현할 API 의 사용 예입니다. blog 크레이트를 구현하지 않았으므로 아직 컴파일되지 않습니다.
파일 이름: src/main.rs
use blog::Post;
fn main() {
1 let mut post = Post::new();
2 post.add_text("I ate a salad for lunch today");
3 assert_eq!("", post.content());
4 post.request_review();
5 assert_eq!("", post.content());
6 post.approve();
7 assert_eq!("I ate a salad for lunch today", post.content());
}
Listing 17-11: blog 크레이트가 가져야 할 원하는 동작을 보여주는 코드
사용자가 Post::new [1]을 사용하여 새 초안 블로그 게시물을 만들 수 있도록 하려고 합니다. 텍스트를 블로그 게시물에 추가할 수 있도록 하려고 합니다 [2]. 승인 전에 게시물의 콘텐츠를 즉시 가져오려고 하면 게시물이 아직 초안 상태이므로 텍스트가 반환되지 않아야 합니다. 데모 목적으로 코드에 assert_eq!를 추가했습니다 [3]. 이에 대한 훌륭한 단위 테스트는 초안 블로그 게시물이 content 메서드에서 빈 문자열을 반환하는지 확인하는 것이지만, 이 예제에 대한 테스트는 작성하지 않겠습니다.
다음으로, 게시물 검토 요청을 활성화하고 [4], 검토를 기다리는 동안 content가 빈 문자열을 반환하도록 하려고 합니다 [5]. 게시물이 승인을 받으면 [6], 게시되어야 합니다. 즉, content가 호출될 때 게시물의 텍스트가 반환됩니다 [7].
크레이트에서 상호 작용하는 유일한 유형은 Post 유형입니다. 이 유형은 상태 패턴을 사용하고 게시물이 가질 수 있는 다양한 상태 (초안, 검토 또는 게시됨) 를 나타내는 세 가지 상태 객체 중 하나가 될 값을 보유합니다. 한 상태에서 다른 상태로의 변경은 Post 유형 내에서 내부적으로 관리됩니다. 상태는 라이브러리 사용자가 Post 인스턴스에서 호출하는 메서드에 대한 응답으로 변경되지만 상태 변경을 직접 관리할 필요는 없습니다. 또한 사용자는 게시물을 검토 전에 게시하는 등 상태에 실수를 할 수 없습니다.
Post 정의 및 초안 상태에서 새 인스턴스 생성
라이브러리 구현을 시작해 봅시다! 우리는 일부 콘텐츠를 보유하는 공개 Post 구조체가 필요하다는 것을 알고 있으므로, Listing 17-12 에 표시된 대로 구조체 정의와 Post의 인스턴스를 생성하는 관련 공개 new 함수로 시작합니다. 또한 모든 Post에 대한 모든 상태 객체가 가져야 하는 동작을 정의하는 비공개 State 트레이트도 만들 것입니다.
그런 다음 Post는 상태 객체를 보유하기 위해 state라는 비공개 필드에서 Option<T> 내부에 Box<dyn State>의 트레이트 객체를 보유합니다. 잠시 후에 Option<T>가 필요한 이유를 알게 될 것입니다.
파일 이름: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
1 state: Some(Box::new(Draft {})),
2 content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
Listing 17-12: Post 구조체 정의 및 새 Post 인스턴스를 생성하는 new 함수, State 트레이트 및 Draft 구조체
State 트레이트는 서로 다른 게시물 상태에서 공유되는 동작을 정의합니다. 상태 객체는 Draft, PendingReview 및 Published이며, 모두 State 트레이트를 구현합니다. 현재 트레이트에는 메서드가 없으며, 게시물이 시작되기를 원하는 상태인 Draft 상태를 먼저 정의하는 것으로 시작합니다.
새 Post를 만들 때 state 필드를 Box를 보유하는 Some 값으로 설정합니다 [1]. 이 Box는 Draft 구조체의 새 인스턴스를 가리킵니다. 이렇게 하면 새 Post 인스턴스를 만들 때마다 초안으로 시작됩니다. Post의 state 필드는 비공개이므로 다른 상태에서 Post를 만들 방법이 없습니다! Post::new 함수에서 content 필드를 새롭고 빈 String으로 설정합니다 [2].
게시물 콘텐츠 텍스트 저장
Listing 17-11 에서 add_text라는 메서드를 호출하고 &str을 전달하여 블로그 게시물의 텍스트 콘텐츠로 추가할 수 있기를 원한다는 것을 확인했습니다. 나중에 content 필드의 데이터를 읽는 방법을 제어하는 메서드를 구현할 수 있도록 content 필드를 pub로 노출하는 대신 이 메서드를 구현합니다. add_text 메서드는 매우 간단하므로 Listing 17-13 의 구현을 impl Post 블록에 추가해 보겠습니다.
파일 이름: src/lib.rs
impl Post {
--snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Listing 17-13: 게시물의 content에 텍스트를 추가하는 add_text 메서드 구현
add_text 메서드는 add_text를 호출하는 Post 인스턴스를 변경하므로 self에 대한 가변 참조를 사용합니다. 그런 다음 content의 String에서 push_str을 호출하고 저장된 content에 추가할 text 인수를 전달합니다. 이 동작은 게시물의 상태에 의존하지 않으므로 상태 패턴의 일부가 아닙니다. add_text 메서드는 state 필드와 전혀 상호 작용하지 않지만 지원하려는 동작의 일부입니다.
초안 게시물의 콘텐츠가 비어 있는지 확인
add_text를 호출하고 게시물에 일부 콘텐츠를 추가한 후에도 게시물이 여전히 초안 상태이므로 (Listing 17-11 의 [3] 참조) content 메서드가 빈 문자열 슬라이스를 반환하기를 원합니다. 현재로서는 이 요구 사항을 충족하는 가장 간단한 방법, 즉 항상 빈 문자열 슬라이스를 반환하는 방식으로 content 메서드를 구현해 보겠습니다. 나중에 게시물의 상태를 변경하여 게시할 수 있도록 하는 기능을 구현한 후 이 부분을 변경할 것입니다. 지금까지 게시물은 초안 상태일 수 있으므로 게시물 콘텐츠는 항상 비어 있어야 합니다. Listing 17-14 는 이 자리 표시자 구현을 보여줍니다.
파일 이름: src/lib.rs
impl Post {
--snip--
pub fn content(&self) -> &str {
""
}
}
Listing 17-14: 항상 빈 문자열 슬라이스를 반환하는 Post의 content 메서드에 대한 자리 표시자 구현 추가
이 추가된 content 메서드를 사용하면 Listing 17-11 의 [3] 줄까지 모든 것이 의도한 대로 작동합니다.
검토 요청은 게시물의 상태를 변경합니다
다음으로, 게시물 검토를 요청하는 기능을 추가해야 합니다. 이 기능은 게시물의 상태를 Draft에서 PendingReview로 변경해야 합니다. Listing 17-15 는 이 코드를 보여줍니다.
파일 이름: src/lib.rs
impl Post {
--snip--
1 pub fn request_review(&mut self) {
2 if let Some(s) = self.state.take() {
3 self.state = Some(s.request_review())
}
}
}
trait State {
4 fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
5 Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
6 self
}
}
Listing 17-15: Post 및 State 트레이트에 request_review 메서드 구현
Post에 self에 대한 가변 참조를 사용하는 request_review라는 public 메서드를 제공합니다 [1]. 그런 다음 Post의 현재 상태에서 내부 request_review 메서드를 호출합니다 [3]. 이 두 번째 request_review 메서드는 현재 상태를 소비하고 새 상태를 반환합니다.
State 트레이트에 request_review 메서드를 추가합니다 [4]. 트레이트를 구현하는 모든 타입은 이제 request_review 메서드를 구현해야 합니다. 메서드의 첫 번째 매개변수로 self, &self, 또는 &mut self 대신 self: Box<Self>를 사용합니다. 이 구문은 해당 타입이 포함된 Box에서 호출될 때만 메서드가 유효함을 의미합니다. 이 구문은 Box<Self>의 소유권을 가져와 이전 상태를 무효화하므로 Post의 상태 값을 새 상태로 변환할 수 있습니다.
이전 상태를 소비하기 위해 request_review 메서드는 상태 값의 소유권을 가져야 합니다. 이것이 Post의 state 필드에 있는 Option이 사용되는 곳입니다. take 메서드를 호출하여 state 필드에서 Some 값을 가져오고 그 자리에 None을 남겨둡니다. Rust 는 구조체에 채워지지 않은 필드를 허용하지 않기 때문입니다 [2]. 이렇게 하면 state 값을 빌리는 대신 Post에서 이동할 수 있습니다. 그런 다음 게시물의 state 값을 이 작업의 결과로 설정합니다.
state 값을 소유하려면 self.state = self.state.request_review();와 같은 코드로 직접 설정하는 대신 임시로 state를 None으로 설정해야 합니다. 이렇게 하면 Post가 이전 state 값을 새 상태로 변환한 후 사용할 수 없게 됩니다.
Draft의 request_review 메서드는 새 PendingReview 구조체의 새, boxed 인스턴스를 반환합니다 [5]. 이 구조체는 게시물이 검토를 기다리는 상태를 나타냅니다. PendingReview 구조체도 request_review 메서드를 구현하지만 변환을 수행하지 않습니다. 대신 자체를 반환합니다 [6]. PendingReview 상태인 게시물에 대해 검토를 요청하면 PendingReview 상태로 유지되어야 하기 때문입니다.
이제 상태 패턴의 장점을 보기 시작할 수 있습니다. Post의 request_review 메서드는 state 값에 관계없이 동일합니다. 각 상태는 자체 규칙을 담당합니다.
Post의 content 메서드는 빈 문자열 슬라이스를 반환하는 그대로 둡니다. 이제 PendingReview 상태와 Draft 상태의 Post를 가질 수 있지만, PendingReview 상태에서도 동일한 동작을 원합니다. Listing 17-11 은 이제 [5] 줄까지 작동합니다!
content 의 동작을 변경하기 위해 승인 추가
approve 메서드는 request_review 메서드와 유사합니다. Listing 17-16 에 표시된 것처럼, 현재 상태가 승인되었을 때 현재 상태가 가져야 한다고 말하는 값으로 state를 설정합니다.
파일 이름: src/lib.rs
impl Post {
--snip--
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 approve(self: Box<Self>) -> Box<dyn State> {
1 self
}
}
struct PendingReview {}
impl State for PendingReview {
--snip--
fn approve(self: Box<Self>) -> Box<dyn State> {
2 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
}
}
Listing 17-16: Post 및 State 트레이트에 approve 메서드 구현
State 트레이트에 approve 메서드를 추가하고 State를 구현하는 새 구조체인 Published 상태를 추가합니다.
PendingReview에서 request_review가 작동하는 방식과 유사하게, Draft에서 approve 메서드를 호출하면 approve가 self를 반환하므로 아무런 효과가 없습니다 [1]. PendingReview에서 approve를 호출하면 Published 구조체의 새, boxed 인스턴스를 반환합니다 [2]. Published 구조체는 State 트레이트를 구현하며, request_review 메서드와 approve 메서드 모두에 대해 자체를 반환합니다. 게시물이 해당 경우에 Published 상태로 유지되어야 하기 때문입니다.
이제 Post에서 content 메서드를 업데이트해야 합니다. content에서 반환되는 값이 Post의 현재 상태에 따라 달라지도록 하므로, Listing 17-17 에 표시된 것처럼 Post가 state에서 정의된 content 메서드에 위임하도록 할 것입니다.
파일 이름: src/lib.rs
impl Post {
--snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
--snip--
}
Listing 17-17: Post에서 content 메서드를 업데이트하여 State의 content 메서드로 위임
목표는 이러한 모든 규칙을 State를 구현하는 구조체 내에 유지하는 것이므로, state의 값에서 content 메서드를 호출하고 게시물 인스턴스 (즉, self) 를 인수로 전달합니다. 그런 다음 state 값에서 content 메서드를 사용하여 반환된 값을 반환합니다.
Option 내의 값에 대한 참조를 원하고 값의 소유권을 원하지 않으므로 Option에서 as_ref 메서드를 호출합니다. state가 Option<Box<dyn State>>이므로 as_ref를 호출하면 Option<&Box<dyn State>>가 반환됩니다. as_ref를 호출하지 않으면 함수 매개변수의 빌린 &self에서 state를 이동할 수 없으므로 오류가 발생합니다.
그런 다음 unwrap 메서드를 호출합니다. Post의 메서드가 해당 메서드가 완료될 때 state가 항상 Some 값을 포함하도록 보장하므로 이 메서드가 절대 패닉하지 않는다는 것을 알고 있습니다. 이것은 컴파일러가 이해할 수 없더라도 None 값이 불가능하다는 것을 알고 있는 "컴파일러보다 더 많은 정보를 가지고 있는 경우"에서 이야기했던 경우 중 하나입니다.
이 시점에서 &Box<dyn State>에서 content를 호출하면 역참조 강제 변환이 & 및 Box에 적용되므로 content 메서드는 궁극적으로 State 트레이트를 구현하는 타입에서 호출됩니다. 즉, State 트레이트 정의에 content를 추가해야 하며, Listing 17-18 에 표시된 것처럼 어떤 콘텐츠를 반환할지 로직을 넣을 것입니다.
파일 이름: src/lib.rs
trait State {
--snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
1 ""
}
}
--snip--
struct Published {}
impl State for Published {
--snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
2 &post.content
}
}
Listing 17-18: State 트레이트에 content 메서드 추가
빈 문자열 슬라이스를 반환하는 content 메서드에 대한 기본 구현을 추가합니다 [1]. 즉, Draft 및 PendingReview 구조체에서 content를 구현할 필요가 없습니다. Published 구조체는 content 메서드를 재정의하고 post.content의 값을 반환합니다 [2].
10 장에서 논의했듯이 이 메서드에는 수명 주기 주석이 필요합니다. post에 대한 참조를 인수로 사용하고 해당 post의 일부에 대한 참조를 반환하므로 반환된 참조의 수명은 post 인수의 수명과 관련이 있습니다.
이제 완료되었습니다. Listing 17-11 의 모든 것이 이제 작동합니다! 블로그 게시물 워크플로우의 규칙으로 상태 패턴을 구현했습니다. 규칙과 관련된 로직은 Post 전체에 분산되지 않고 상태 객체에 있습니다.
Enum 을 사용하지 않는 이유는 무엇입니까?
다양한 가능한 게시물 상태를 변형으로 사용하는
enum을 사용하지 않은 이유가 궁금했을 것입니다. 물론 가능한 해결책입니다. 시도해 보고 최종 결과를 비교하여 어떤 것을 선호하는지 확인하십시오!enum을 사용하는 한 가지 단점은enum의 값을 확인하는 모든 위치에서 가능한 모든 변형을 처리하기 위해match표현식 또는 유사한 표현식이 필요하다는 것입니다. 이것은 이 트레이트 객체 솔루션보다 더 반복적일 수 있습니다.
상태 패턴의 장단점
Rust 가 게시물이 각 상태에서 가져야 하는 다양한 종류의 동작을 캡슐화하기 위해 객체 지향 상태 패턴을 구현할 수 있음을 보여주었습니다. Post의 메서드는 다양한 동작에 대해 아무것도 모릅니다. 코드를 구성한 방식에 따라 게시된 게시물이 동작할 수 있는 다양한 방식을 알기 위해 한 곳만 살펴보면 됩니다. 즉, Published 구조체에서 State 트레이트를 구현하는 것입니다.
상태 패턴을 사용하지 않는 대체 구현을 생성하는 경우, 대신 Post의 메서드 또는 게시물의 상태를 확인하고 해당 위치에서 동작을 변경하는 main 코드에서 match 표현식을 사용할 수 있습니다. 즉, 게시물이 게시된 상태에 있는 모든 의미를 이해하기 위해 여러 곳을 살펴봐야 합니다! 이것은 더 많은 상태를 추가할수록 증가할 것입니다. 각 match 표현식에는 다른 arm 이 필요합니다.
상태 패턴을 사용하면 Post 메서드와 Post를 사용하는 위치에 match 표현식이 필요하지 않으며, 새 상태를 추가하려면 새 구조체를 추가하고 해당 구조체에서 트레이트 메서드를 구현하기만 하면 됩니다.
상태 패턴을 사용하는 구현은 더 많은 기능을 추가하기 위해 쉽게 확장할 수 있습니다. 상태 패턴을 사용하는 코드를 유지 관리하는 단순성을 확인하려면 다음 제안 사항을 시도해 보십시오.
- 게시물의 상태를
PendingReview에서Draft로 변경하는reject메서드를 추가합니다. - 상태를
Published로 변경하기 전에approve를 두 번 호출해야 합니다. - 게시물이
Draft상태일 때만 사용자가 텍스트 콘텐츠를 추가할 수 있도록 허용합니다. 힌트: 콘텐츠에 대해 변경될 수 있는 사항을 담당하지만Post를 수정하는 것은 담당하지 않는 상태 객체를 사용합니다.
상태 패턴의 한 가지 단점은 상태가 상태 간의 전환을 구현하기 때문에 일부 상태가 서로 결합된다는 것입니다. PendingReview와 Published 사이에 Scheduled와 같은 다른 상태를 추가하면 PendingReview의 코드를 변경하여 대신 Scheduled로 전환해야 합니다. PendingReview가 새 상태를 추가할 때 변경할 필요가 없다면 작업량이 줄어들겠지만, 이는 다른 디자인 패턴으로 전환해야 함을 의미합니다.
또 다른 단점은 일부 로직을 중복했다는 것입니다. 중복을 제거하기 위해 self를 반환하는 State 트레이트에서 request_review 및 approve 메서드에 대한 기본 구현을 시도할 수 있습니다. 그러나 이것은 작동하지 않습니다. State를 트레이트 객체로 사용하는 경우 트레이트는 구체적인 self가 정확히 무엇인지 알 수 없으므로 반환 타입은 컴파일 시간에 알 수 없습니다.
다른 중복에는 Post에서 request_review 및 approve 메서드의 유사한 구현이 포함됩니다. 두 메서드 모두 Option의 state 필드에 있는 값에서 동일한 메서드의 구현으로 위임하고 state 필드의 새 값을 결과로 설정합니다. 이 패턴을 따르는 Post에 많은 메서드가 있는 경우 반복을 제거하기 위해 매크로를 정의하는 것을 고려할 수 있습니다 ("매크로" 참조).
객체 지향 언어에 대해 정의된 대로 상태 패턴을 정확하게 구현함으로써 Rust 의 강점을 최대한 활용하지 못하고 있습니다. blog 크레이트에 대한 몇 가지 변경 사항을 살펴보고 잘못된 상태와 전환을 컴파일 시간 오류로 만들 수 있습니다.
상태 및 동작을 타입으로 인코딩
상태 패턴을 재고하여 다른 일련의 장단점을 얻는 방법을 보여드리겠습니다. 외부 코드가 상태와 전환에 대해 전혀 알 수 없도록 상태와 전환을 완전히 캡슐화하는 대신, 상태를 다른 타입으로 인코딩합니다. 결과적으로 Rust 의 타입 검사 시스템은 컴파일러 오류를 발생시켜 게시된 게시물만 허용되는 곳에서 초안 게시물을 사용하려는 시도를 방지합니다.
Listing 17-11 의 main의 첫 번째 부분을 살펴보겠습니다.
파일 이름: src/main.rs
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
}
Post::new를 사용하여 초안 상태에서 새 게시물을 생성하고 게시물의 내용에 텍스트를 추가하는 기능을 계속 사용할 수 있습니다. 그러나 빈 문자열을 반환하는 초안 게시물에 content 메서드를 갖는 대신, 초안 게시물에 content 메서드가 전혀 없도록 만들 것입니다. 그렇게 하면 초안 게시물의 내용을 얻으려고 하면 메서드가 존재하지 않는다는 컴파일러 오류가 발생합니다. 결과적으로 해당 코드가 컴파일되지 않으므로 프로덕션에서 초안 게시물 내용을 실수로 표시하는 것은 불가능합니다. Listing 17-19 는 Post 구조체와 DraftPost 구조체의 정의와 각 구조체의 메서드를 보여줍니다.
파일 이름: src/lib.rs
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
1 pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
2 pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
3 pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Listing 17-19: content 메서드가 있는 Post와 content 메서드가 없는 DraftPost
Post 및 DraftPost 구조체 모두 블로그 게시물 텍스트를 저장하는 private content 필드를 가지고 있습니다. 구조체는 상태를 구조체의 타입으로 인코딩하므로 더 이상 state 필드를 갖지 않습니다. Post 구조체는 게시된 게시물을 나타내며 content를 반환하는 content 메서드를 갖습니다 [2].
여전히 Post::new 함수가 있지만, Post의 인스턴스를 반환하는 대신 DraftPost의 인스턴스를 반환합니다 [1]. content가 private 이고 Post를 반환하는 함수가 없으므로 지금은 Post의 인스턴스를 생성할 수 없습니다.
DraftPost 구조체에는 add_text 메서드가 있으므로 이전과 같이 content에 텍스트를 추가할 수 있지만 [3], DraftPost에는 content 메서드가 정의되어 있지 않습니다! 따라서 이제 프로그램은 모든 게시물이 초안 게시물로 시작되도록 보장하며, 초안 게시물은 표시할 수 있는 콘텐츠를 갖지 않습니다. 이러한 제약 조건을 우회하려는 모든 시도는 컴파일러 오류를 발생시킵니다.
전환을 다른 타입으로 변환하여 구현
그렇다면 게시된 게시물을 어떻게 얻을 수 있을까요? 초안 게시물은 게시되기 전에 검토 및 승인되어야 한다는 규칙을 적용하려고 합니다. 검토 대기 중인 게시물은 여전히 콘텐츠를 표시해서는 안 됩니다. Listing 17-20 에 표시된 대로 DraftPost에서 PendingReviewPost를 반환하는 request_review 메서드를 정의하고 PendingReviewPost에서 Post를 반환하는 approve 메서드를 정의하여 이러한 제약 조건을 구현해 보겠습니다.
파일 이름: src/lib.rs
impl DraftPost {
--snip--
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,
}
}
}
Listing 17-20: DraftPost에서 request_review를 호출하여 생성된 PendingReviewPost와 PendingReviewPost를 게시된 Post로 변환하는 approve 메서드
request_review 및 approve 메서드는 self의 소유권을 가져와 DraftPost 및 PendingReviewPost 인스턴스를 소비하고 각각 PendingReviewPost 및 게시된 Post로 변환합니다. 이렇게 하면 request_review를 호출한 후에도 DraftPost 인스턴스가 남아 있지 않게 됩니다. PendingReviewPost 구조체에는 정의된 content 메서드가 없으므로 해당 콘텐츠를 읽으려고 하면 DraftPost와 마찬가지로 컴파일러 오류가 발생합니다. content 메서드가 정의된 게시된 Post 인스턴스를 얻는 유일한 방법은 PendingReviewPost에서 approve 메서드를 호출하는 것이고, PendingReviewPost를 얻는 유일한 방법은 DraftPost에서 request_review 메서드를 호출하는 것이므로, 이제 블로그 게시물 워크플로우를 타입 시스템에 인코딩했습니다.
하지만 main에도 몇 가지 작은 변경 사항을 적용해야 합니다. request_review 및 approve 메서드는 호출되는 구조체를 수정하는 대신 새 인스턴스를 반환하므로 반환된 인스턴스를 저장하기 위해 더 많은 let post = 섀도잉 할당을 추가해야 합니다. 또한 초안 및 검토 대기 중인 게시물의 내용에 대한 어설션이 빈 문자열일 수 없으며 필요하지도 않습니다. 더 이상 해당 상태의 게시물 내용을 사용하려는 코드를 컴파일할 수 없습니다. main의 업데이트된 코드는 Listing 17-21 에 나와 있습니다.
파일 이름: src/main.rs
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());
}
Listing 17-21: 블로그 게시물 워크플로우의 새로운 구현을 사용하도록 main을 수정
post를 다시 할당하기 위해 main에 적용해야 하는 변경 사항은 이 구현이 더 이상 객체 지향 상태 패턴을 완전히 따르지 않는다는 것을 의미합니다. 상태 간의 변환은 더 이상 Post 구현 내에 완전히 캡슐화되지 않습니다. 그러나 우리가 얻는 것은 타입 시스템과 컴파일 시간에 발생하는 타입 검사로 인해 잘못된 상태가 이제 불가능하다는 것입니다! 이렇게 하면 게시되지 않은 게시물의 내용 표시와 같은 특정 버그가 프로덕션에 들어가기 전에 발견됩니다.
Listing 17-21 이후의 blog 크레이트에서 이 섹션의 시작 부분에서 제안된 작업을 시도하여 이 버전의 코드 디자인에 대해 어떻게 생각하는지 확인하십시오. 일부 작업은 이 디자인에서 이미 완료되었을 수 있습니다.
Rust 가 객체 지향 디자인 패턴을 구현할 수 있지만, 상태를 타입 시스템에 인코딩하는 것과 같은 다른 패턴도 Rust 에서 사용할 수 있음을 확인했습니다. 이러한 패턴은 서로 다른 장단점을 가지고 있습니다. 객체 지향 패턴에 매우 익숙할 수 있지만, Rust 의 기능을 활용하기 위해 문제를 다시 생각하면 컴파일 시간에 일부 버그를 방지하는 것과 같은 이점을 얻을 수 있습니다. 객체 지향 언어에는 없는 소유권과 같은 특정 기능으로 인해 객체 지향 패턴이 Rust 에서 항상 최선의 해결책이 아닐 것입니다.
요약
축하합니다! 객체 지향 디자인 패턴 구현 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.