对照OOP
浅谈【类型状态】设计模式
类型状态·设计模式Type State Pattern
也被称作“泛型·即是·类的类型(约束)Generic as Type Class (Constraint)
”。它是基于Rust
独有语言特性
单态化
monomorphization
move
赋值语义
的新颖设计模式。其中,【move
赋值语义】为Rust
所独有的原因是
一方面,
GC
类计算机语言已经将内存托管给vm
,所以他们就没再搞出这类复杂的内存管理概念,和增加开发者的心智负担。另一方面,
Cpp
的move
赋值语义仅只是对历史包袱的【妥协方案】。这体现在所以,和
Rust
相比,Cpp
的move
赋值语义至多就是一个“弟弟”。其功能相当于Rust
标准库提供的std::mem::take(&T) -> T
内存操作 — 使用【类型·默认值】置换出【引用】内存位置上的值;同时,保留·原变量·的【所有权】不被消耗掉和可以被接着使用。Cpp
的move
语义是: 用空指针nullptr
换走原变量的值;但,原变量依旧可访问。这哪里是move
,分明是swap
呀!Rust
的move
语义是:拿走原变量的值;同时,作废原变量。这个操作也被称为“消耗consuming
”。
此外,
move
也不是Cpp
变量赋值的默认语义。相反 ,开发者得显示地编码std::move(ptr)
函数调用和将lvalue
转换为rvalue
。Cpp
的std::move(ptr)
函数调用是【零·运行时·成本】的。在编译之后,编译器会将其从机器码内扣掉。其“辅助线”般的功能有些类似于Rust
中的std::marker::PhantomData<T>
。
名词解释
为了避免后续文章内容过于啰嗦,首先定义五个词条,包括:
泛型类型
泛型类型参数
泛型类型形参
泛型类型实参
泛型类型参数限定条件
一图抵千词,大家来看下图吧:
基本概念
【类型状态·模式】的初衷是:将【运行时】对象状态的(动态)信息·编码入·【编译时】对象类型的(静态)定义里。进而,借助现成且完备的Rust
【类型系统】,在【编译】过程中,确保:
处于不同状态的(泛型类型)实例·拥有不一样的(【成员方法】+【关联函数】+【字段】)集合。
(泛型类型)实例·仅能在毗邻的状态之间进行“状态·过渡”,而不能“跳变”。
排查出·跨状态的成员方法调用。比如,
A
状态的实例调用了仅在B
状态才有效的成员方法。 而不是,让这类错误潜伏着和等【测试覆盖】或抛出【运行时·异常】。
以【订单系统】为例,【编译器】就能筛查出代码里
对【无效订单】实例的【发货】成员方法调用
对【出库订单】实例的【完成】成员方法调用 — 还未经历【发货】与【收款】两个状态
相对于传统的OOP
程序,Rust
【类型状态】设计模式将【对象·状态】的【运行时】检查前置于【编译环节】。进而带来的好处包括但不限于:
将【运行时】程序崩溃“无害化”为【编译时】错误。
就开发者而言,这意味着更短的【思考
+
试错】反馈回路。就应用程序而言,这意味着更高的性能,更健壮的可靠性,和更重的应用程序大小 — 【单态化】的本质就是以空间换时间。
允许
IDE
提供更有价值的代码提示。即,仅智能地列出对当前状态实例有效的【成员方法】,而不是罗列全部成员方法。比如,当开发者“点”一个【无效订单】实例时,IDE
就不应该提示出【发货】成员方法。这才是对开发者最实在的帮助。
代码套路
从操作细节来说,为了采用【类型状态·设计模式】,我们需要:
将每个【状态】分别映射为独立的【结构体】(比如,
struct State1
)。和在结构体内,定义【状态】独有的:字段。(见伪码#1
注释)而不是,使用一个【枚举类】
enum State {...}
笼统地描述所有【状态】后文称这类【结构体】为【状态·类型】。
以【泛型·类型】
+
【泛型·类型·形参】的结构体定义(比如,struct Type1<S1>
),抽象所有【状态】共有的:字段。(见伪码#2
注释)以【泛型·类型】
+
【泛型·类型·形参】的实现块(比如,impl<S1> Type1<S1>
),抽象所有【状态】共有的:成员方法,关联函数,关联常量,和关联类型。(见伪码#3
注释)以【泛型·类型】
+
【泛型·类型·实参】的实现块(比如,impl Type1<State1>
),定制每个【状态】独有的:成员方法,关联函数,关联常量,和关联类型。(见伪码#4
注释)
/// #1 【状态·类型】
struct State1 {
private_field1: String // 定义【状态】独有【字段】
}
struct State2 {
private_field2: String // 定义【状态】独有【字段】
}
/// #2 【泛型·类型】+【泛型·类型·形参】
struct Type1<S1> { // <- 被参数化的【状态·类型】既作为【泛型·类型·参数】,
state: S1, // <- 也作为【状态·字段】的字段类型
com_field0: String // 抽象全部【状态】共有【字段】
}
/// #3 【泛型·类型】+【泛型·类型·形参】
impl<S1> Type1<S1> { // 抽象全部【状态】共有的【成员方法】
fn com_function0(&self) -> String {
"全状态可调用".to_string()
}
}
/// #4 【泛型·类型】+【泛型·类型·实参】
impl Type1<State1> { // 定制【状态】`State1`独有【成员方法】
fn private_function1(&self) -> String {
"仅 State1 状态可调用".to_string()
}
}
impl Type1<State2> { // 定制【状态】`State2`独有【成员方法】
fn private_function2(&self) -> String {
"仅 State2 状态可调用".to_string()
}
}
承上段代码,在【泛型·类型】struct Type1<S1>
中,被参数化的【状态·类型】S1
既作为【泛型·类型·参数】也作为【状态·字段】state
的字段类型(这是由Generic Struct
定义要求的 — 在结构体定义中,被声明的泛型参数必须被使用)。
// 继续前面的代码
let type1_state1 = Type1 {
com_field0: "对所有状态都看得到的,共用字段值