patternrustMinor
Designing posts: state machine
Viewed 0 times
machinepostsdesigningstate
Problem
In the Object-Oriented Design Pattern Implementation chapter of the second edition of The Rust Programming Language, the basic code structure is given and it suggests trying to add features:
This implementation is easy to extend to add more functionality. Here
are some changes you can try making to the code in this section to see
for yourself what it's like to maintain code using this pattern over
time:
Please critique my approach:
```
struct Post {
content: String
}
struct DraftPost {
content: String
}
struct PendingPost {
content: String,
approvals: u32
}
impl Post {
fn new() -> DraftPost {
DraftPost { content: String::new() }
}
fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
fn req_review(self) -> PendingPost {
PendingPost {
content: self.content,
approvals: 0
}
}
fn add_text(&mut self, content: &str) {
self.content.push_str(content);
}
}
enum PublishResult {
PendingPost(PendingPost),
Post(Post)
}
impl PendingPost {
fn approve(&mut self) {
self.approvals += 1;
}
fn reject(self) -> DraftPost {
DraftPost { content: self.content }
}
fn publish(self) -> PublishResult {
if self.approvals > 1 {
PublishResult::Post(Post{content: self.content})
} else {
PublishResult::PendingPost(self)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn publish_workflow() {
let mut draft = Post::new();
draft.add_text("ashish first post");
let mut pending = draft.req_review();
pending.approve();
pending.approve();
let publish = pending.p
This implementation is easy to extend to add more functionality. Here
are some changes you can try making to the code in this section to see
for yourself what it's like to maintain code using this pattern over
time:
- Only allow adding text content when a post is in the
Draftstate
- Add a
rejectmethod that changes the post's state fromPendingReviewback toDraft
- Require two calls to
approvebefore changing the state toPublished
Please critique my approach:
```
struct Post {
content: String
}
struct DraftPost {
content: String
}
struct PendingPost {
content: String,
approvals: u32
}
impl Post {
fn new() -> DraftPost {
DraftPost { content: String::new() }
}
fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
fn req_review(self) -> PendingPost {
PendingPost {
content: self.content,
approvals: 0
}
}
fn add_text(&mut self, content: &str) {
self.content.push_str(content);
}
}
enum PublishResult {
PendingPost(PendingPost),
Post(Post)
}
impl PendingPost {
fn approve(&mut self) {
self.approvals += 1;
}
fn reject(self) -> DraftPost {
DraftPost { content: self.content }
}
fn publish(self) -> PublishResult {
if self.approvals > 1 {
PublishResult::Post(Post{content: self.content})
} else {
PublishResult::PendingPost(self)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn publish_workflow() {
let mut draft = Post::new();
draft.add_text("ashish first post");
let mut pending = draft.req_review();
pending.approve();
pending.approve();
let publish = pending.p
Solution
Overall, everything seems fine. Some minor nits and suggestions:
-
Stylistically, you should have:
-
-
Instead of matching in your tests and having the not-very-useful
-
Dnt ndlssly abbr mthds (Don't needlessly abbreviate methods). Call it
-
It's strange to have a test (
-
Stylistically, you should have:
- trailing commas
- spaces on struct literals (
Post { content: self.content }
-
PublishResult makes me think it's a type alias of std::result::Result. I'd advocate for picking a different name.-
Instead of matching in your tests and having the not-very-useful
assert!(true) and assert!(false), add some helper methods to PublishResult to convert to an expected variant. This is a common pattern for this style of enum. Then you can expect with text in your tests-
Dnt ndlssly abbr mthds (Don't needlessly abbreviate methods). Call it
request_review instead of req_review.-
It's strange to have a test (
reject_workflow) with no assertions. I don't think anything's wrong with it, as it's testing the methods and types, it's just a bit implicit. struct Post {
content: String,
}
struct DraftPost {
content: String,
}
struct PendingPost {
content: String,
approvals: u32,
}
impl Post {
fn new() -> DraftPost {
DraftPost { content: String::new() }
}
fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
fn request_review(self) -> PendingPost {
PendingPost {
content: self.content,
approvals: 0,
}
}
fn add_text(&mut self, content: &str) {
self.content.push_str(content);
}
}
enum PublishResult {
PendingPost(PendingPost),
Post(Post),
}
impl PublishResult {
fn into_pending_post(self) -> Option {
use PublishResult::*;
match self {
PendingPost(v) => Some(v),
_ => None,
}
}
fn into_post(self) -> Option {
use PublishResult::*;
match self {
Post(v) => Some(v),
_ => None,
}
}
}
impl PendingPost {
fn approve(&mut self) {
self.approvals += 1;
}
fn reject(self) -> DraftPost {
DraftPost { content: self.content }
}
fn publish(self) -> PublishResult {
if self.approvals > 1 {
PublishResult::Post(Post { content: self.content })
} else {
PublishResult::PendingPost(self)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn publish_workflow() {
let mut draft = Post::new();
draft.add_text("ashish first post");
let mut pending = draft.request_review();
pending.approve();
pending.approve();
let post = pending.publish().into_post().expect("not a post");
assert_eq!(post.content(), "ashish first post");
}
#[test]
fn reject_workflow() {
let mut draft = Post::new();
draft.add_text("ashish first post");
let pending = draft.request_review();
let mut again_draft = pending.reject();
again_draft.add_text(".. after first one..");
}
#[test]
fn two_approvals_workflow() {
let mut draft = Post::new();
draft.add_text("ashish first post");
let mut pending = draft.request_review();
pending.approve();
pending
.publish()
.into_pending_post()
.expect("Not a pending post");
}
}Code Snippets
struct Post {
content: String,
}
struct DraftPost {
content: String,
}
struct PendingPost {
content: String,
approvals: u32,
}
impl Post {
fn new() -> DraftPost {
DraftPost { content: String::new() }
}
fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
fn request_review(self) -> PendingPost {
PendingPost {
content: self.content,
approvals: 0,
}
}
fn add_text(&mut self, content: &str) {
self.content.push_str(content);
}
}
enum PublishResult {
PendingPost(PendingPost),
Post(Post),
}
impl PublishResult {
fn into_pending_post(self) -> Option<PendingPost> {
use PublishResult::*;
match self {
PendingPost(v) => Some(v),
_ => None,
}
}
fn into_post(self) -> Option<Post> {
use PublishResult::*;
match self {
Post(v) => Some(v),
_ => None,
}
}
}
impl PendingPost {
fn approve(&mut self) {
self.approvals += 1;
}
fn reject(self) -> DraftPost {
DraftPost { content: self.content }
}
fn publish(self) -> PublishResult {
if self.approvals > 1 {
PublishResult::Post(Post { content: self.content })
} else {
PublishResult::PendingPost(self)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn publish_workflow() {
let mut draft = Post::new();
draft.add_text("ashish first post");
let mut pending = draft.request_review();
pending.approve();
pending.approve();
let post = pending.publish().into_post().expect("not a post");
assert_eq!(post.content(), "ashish first post");
}
#[test]
fn reject_workflow() {
let mut draft = Post::new();
draft.add_text("ashish first post");
let pending = draft.request_review();
let mut again_draft = pending.reject();
again_draft.add_text(".. after first one..");
}
#[test]
fn two_approvals_workflow() {
let mut draft = Post::new();
draft.add_text("ashish first post");
let mut pending = draft.request_review();
pending.approve();
pending
.publish()
.into_pending_post()
.expect("Not a pending post");
}
}Context
StackExchange Code Review Q#162751, answer score: 2
Revisions (0)
No revisions yet.