|
|
|
@ -2,13 +2,17 @@
|
|
|
|
|
|
|
|
|
|
> [ch17-03-oo-design-patterns.md](https://github.com/rust-lang/book/blob/main/src/ch17-03-oo-design-patterns.md)
|
|
|
|
|
> <br>
|
|
|
|
|
> commit 851449061b74d8b15adca936350a3fca6160ff39
|
|
|
|
|
> commit 937784b8708c24314707378ad42faeb12a334bbd
|
|
|
|
|
|
|
|
|
|
**状态模式**(*state pattern*)是一个面向对象设计模式。该模式的关键在于一个值有某些内部状态,体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。
|
|
|
|
|
**状态模式**(*state pattern*)是一个面向对象设计模式。该模式的关键在于定义一系列值的内含状态。这些状态体现为一系列的 **状态对象**,同时值的行为随着其内部状态而改变。我们将编写一个博客发布结构体的例子,它拥有一个包含其状态的字段,这是一个有着 "draft"、"review" 或 "published" 的状态对象
|
|
|
|
|
|
|
|
|
|
使用状态模式意味着当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。让我们看看一个有关状态模式和如何在 Rust 中使用它的例子。
|
|
|
|
|
状态对象共享功能:当然,在 Rust 中使用结构体和 trait 而不是对象和继承。每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态。持有一个状态对象的值对于不同状态的行为以及何时状态转移毫不知情。
|
|
|
|
|
|
|
|
|
|
为了探索这个概念,我们将实现一个增量式的发布博文的工作流。这个博客的最终功能看起来像这样:
|
|
|
|
|
使用状态模式的优点在于,程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。
|
|
|
|
|
|
|
|
|
|
首先我们将以一种更加传统的面向对象的方式实现状态模式,接着使用一种更 Rust 一点的方式。让我们使用状态模式增量式地实现一个发布博文的工作流以探索这个概念。
|
|
|
|
|
|
|
|
|
|
这个博客的最终功能看起来像这样:
|
|
|
|
|
|
|
|
|
|
1. 博文从空白的草案开始。
|
|
|
|
|
2. 一旦草案完成,请求审核博文。
|
|
|
|
@ -35,7 +39,9 @@
|
|
|
|
|
|
|
|
|
|
### 定义 `Post` 并新建一个草案状态的实例
|
|
|
|
|
|
|
|
|
|
让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如示例 17-12 所示。还需定义一个私有 trait `State`。`Post` 将在私有字段 `state` 中存放一个 `Option<T>` 类型的 trait 对象 `Box<dyn State>`。稍后将会看到为何 `Option<T>` 是必须的。
|
|
|
|
|
让我们开始实现这个库吧!我们知道需要一个公有 `Post` 结构体来存放一些文本,所以让我们从结构体的定义和一个创建 `Post` 实例的公有关联函数 `new` 开始,如示例 17-12 所示。还需定义一个私有 trait `State`。
|
|
|
|
|
|
|
|
|
|
`Post` 将在私有字段 `state` 中存放一个 `Option<T>` 类型的 trait 对象 `Box<dyn State>`。稍后将会看到为何 `Option<T>` 是必须的。
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/lib.rs</span>
|
|
|
|
|
|
|
|
|
@ -45,7 +51,7 @@
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 17-12: `Post` 结构体的定义和新建 `Post` 实例的 `new` 函数,`State` trait 和结构体 `Draft`</span>
|
|
|
|
|
|
|
|
|
|
`State` trait 定义了所有不同状态的博文所共享的行为,同时 `Draft`、`PendingReview` 和 `Published` 状态都会实现 `State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态。
|
|
|
|
|
`State` trait 定义了所有不同状态的博文所共享的行为,这个状态对象是 `Draft`、`PendingReview` 和 `Published`,它们都会实现 `State` 状态。现在这个 trait 并没有任何方法,同时开始将只定义 `Draft` 状态因为这是我们希望博文的初始状态。
|
|
|
|
|
|
|
|
|
|
当创建新的 `Post` 时,我们将其 `state` 字段设置为一个存放了 `Box` 的 `Some` 值。这个 `Box` 指向一个 `Draft` 结构体新实例。这确保了无论何时新建一个 `Post` 实例,它都会从草案开始。因为 `Post` 的 `state` 字段是私有的,也就无法创建任何其他状态的 `Post` 了!。`Post::new` 函数中将 `content` 设置为新建的空 `String`。
|
|
|
|
|
|
|
|
|
@ -151,6 +157,10 @@
|
|
|
|
|
|
|
|
|
|
现在示例完成了 —— 现在示例 17-11 中所有的代码都能工作!我们通过发布博文工作流的规则实现了状态模式。围绕这些规则的逻辑都存在于状态对象中而不是分散在 `Post` 之中。
|
|
|
|
|
|
|
|
|
|
> #### 为什么不用枚举?
|
|
|
|
|
>
|
|
|
|
|
> 你可能会好奇为什么不用包含不同可能的博文状态的 `enum` 作为变量。这确实是一个可能的方案,尝试实现并对比最终结果来看看哪一种更适合你!使用枚举的一个缺点是每一个检查枚举值的地方都需要一个 `match` 表达式或类似的代码来处理所有可能的成员。这相比 trait 对象模式可能显得更重复。
|
|
|
|
|
|
|
|
|
|
### 状态模式的权衡取舍
|
|
|
|
|
|
|
|
|
|
我们展示了 Rust 是能够实现面向对象的状态模式的,以便能根据博文所处的状态来封装不同类型的行为。`Post` 的方法并不知道这些不同类型的行为。通过这种组织代码的方式,要找到所有已发布博文的不同行为只需查看一处代码:`Published` 的 `State` trait 的实现。
|
|
|
|
@ -227,7 +237,7 @@
|
|
|
|
|
|
|
|
|
|
不得不修改 `main` 来重新赋值 `post` 使得这个实现不再完全遵守面向对象的状态模式:状态间的转换不再完全封装在 `Post` 实现中。然而,得益于类型系统和编译时类型检查,我们得到了的是无效状态是不可能的!这确保了某些特定的 bug,比如显示未发布博文的内容,将在部署到生产环境之前被发现。
|
|
|
|
|
|
|
|
|
|
尝试为示例 17-20 之后的 `blog` crate 实现这一部分开始所建议的增加额外需求的任务来体会使用这个版本的代码是何感觉。注意在这个设计中一些需求可能已经完成了。
|
|
|
|
|
尝试为示例 17-21 之后的 `blog` crate 实现这一部分开始所建议的任务来体会使用这个版本的代码是何感觉。注意在这个设计中一些需求可能已经完成了。
|
|
|
|
|
|
|
|
|
|
即便 Rust 能够实现面向对象设计模式,也有其他像将状态编码进类型这样的模式存在。这些模式有着不同的权衡取舍。虽然你可能非常熟悉面向对象模式,重新思考这些问题来利用 Rust 提供的像在编译时避免一些 bug 这样有益功能。在 Rust 中面向对象模式并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能。
|
|
|
|
|
|
|
|
|
|