PAYATHON 2026

Rust 中不违反 OCP 的多状态 typestate pattern

技术老齐

结论

实现方式不止一种。Rust 没有通用方案可以在增加 typestate 维度后,让所有类型定义和实现保持不变。

新增状态只要参与编译期检查,就需要在类型系统的某个位置描述这个维度。实际使用时,可以按下面的方式组织:

  • Car 只保留一个状态参数 S;
  • 用具体类型表示完整状态;
  • 将状态约束和状态转换拆成多个小 trait;
  • 对外提供 type alias,避免调用方直接书写复杂的状态类型;
  • 将新增维度所需的修改集中在状态表示层,尽量不影响 Car 的业务方法。

这种结构通常比把所有关联类型都放进一个庞大的 CarState trait 更容易维护。

trait 不能当作具体状态类型

下面的返回类型无法成立:

fn build_new_car() -> Car<CarState<DoorsClosed, RoofClosed>>

主要有两个原因:

  1. CarState 是 trait,不能像泛型结构体 State<D, R> 一样实例化。
  2. typestate 使用具体的静态类型表示当前状态。改用 dyn CarState 会擦除这些类型信息,而且包含关联类型的 trait object 还必须明确指定所有关联类型。

因此,函数需要返回一个具体状态类型,或者返回该具体类型的别名。

将状态能力拆成小 trait

CarState 可以只作为完整状态的标记,每种能力则由单独的 trait 描述:

use std::marker::PhantomData;

pub trait DoorState {}
pub struct DoorsOpen;
pub struct DoorsClosed;

impl DoorState for DoorsOpen {}
impl DoorState for DoorsClosed {}

pub trait RoofState {}
pub struct RoofOpen;
pub struct RoofClosed;

impl RoofState for RoofOpen {}
impl RoofState for RoofClosed {}

pub trait CarState {}

pub struct State<D, R>(PhantomData<(D, R)>);

impl<D, R> CarState for State<D, R>
where
    D: DoorState,
    R: RoofState,
{
}

接着分别定义“门已关闭”和“门已打开”等能力。每个关联类型只描述对应转换的结果:

pub trait DoorsAreClosed: CarState {
    type WithDoorsOpen: CarState;
}

pub trait DoorsAreOpen: CarState {
    type WithDoorsClosed: CarState;
}

impl<R> DoorsAreClosed for State<DoorsClosed, R>
where
    R: RoofState,
{
    type WithDoorsOpen = State<DoorsOpen, R>;
}

impl<R> DoorsAreOpen for State<DoorsOpen, R>
where
    R: RoofState,
{
    type WithDoorsClosed = State<DoorsClosed, R>;
}

Roof 维度也按同样的方式定义:

pub trait RoofIsClosed: CarState {
    type WithRoofOpen: CarState;
}

pub trait RoofIsOpen: CarState {
    type WithRoofClosed: CarState;
}

impl<D> RoofIsClosed for State<D, RoofClosed>
where
    D: DoorState,
{
    type WithRoofOpen = State<D, RoofOpen>;
}

impl<D> RoofIsOpen for State<D, RoofOpen>
where
    D: DoorState,
{
    type WithRoofClosed = State<D, RoofClosed>;
}

Car 的方法只依赖所需能力

Car 仍然只需要一个泛型参数:

pub struct Car<S: CarState> {
    model: String,
    _state: PhantomData<S>,
}

impl<S: CarState> Car<S> {
    fn change_state<T: CarState>(self) -> Car<T> {
        Car {
            model: self.model,
            _state: PhantomData,
        }
    }
}

门相关的方法只关心门的状态,无须了解 Roof 维度:

impl<S> Car<S>
where
    S: DoorsAreClosed,
{
    pub fn open_doors(self) -> Car<S::WithDoorsOpen> {
        self.change_state()
    }

    pub fn drive(&self) {
        println!(
            "The {} is driving safely with its doors closed.",
            self.model
        );
    }
}

impl<S> Car<S>
where
    S: DoorsAreOpen,
{
    pub fn close_doors(self) -> Car<S::WithDoorsClosed> {
        self.change_state()
    }
}

Roof 方法同样只依赖对应的能力:

impl<S> Car<S>
where
    S: RoofIsClosed,
{
    pub fn open_roof(self) -> Car<S::WithRoofOpen> {
        self.change_state()
    }
}

impl<S> Car<S>
where
    S: RoofIsOpen,
{
    pub fn close_roof(self) -> Car<S::WithRoofClosed> {
        self.change_state()
    }
}

这样,每个业务方法只声明自己需要的约束。

用类型别名简化对外 API

调用方无须反复书写完整的 State<...>:

pub type NewCar = Car<State<DoorsClosed, RoofClosed>>;

impl Car<State<DoorsClosed, RoofClosed>> {
    pub fn new(model: impl Into<String>) -> Self {
        Self {
            model: model.into(),
            _state: PhantomData,
        }
    }
}

pub fn build_new_car() -> NewCar {
    Car::new("Mymodel")
}

类型别名不会创建新的 nominal type,但可以隐藏大部分泛型细节。还可以根据 API 的含义提供其他别名:

pub type ClosedCar = Car<State<DoorsClosed, RoofClosed>>;
pub type OpenTopCar = Car<State<DoorsClosed, RoofOpen>>;

如果不希望外部代码构造任意状态,可以保持 State 的字段私有,只公开允许使用的别名和构造函数。

增加新维度时需要修改什么

假设以后增加 Engine 状态,完整状态可能变为:

pub trait EngineState {}

pub struct EngineRunning;
pub struct EngineStopped;

impl EngineState for EngineRunning {}
impl EngineState for EngineStopped {}

pub struct State<D, R, E>(PhantomData<(D, R, E)>);

这时需要修改 State 的实现,以及各转换 trait 的底层实现,让转换过程保留 E:

impl<D, R, E> CarState for State<D, R, E>
where
    D: DoorState,
    R: RoofState,
    E: EngineState,
{
}

impl<R, E> DoorsAreClosed for State<DoorsClosed, R, E>
where
    R: RoofState,
    E: EngineState,
{
    type WithDoorsOpen = State<DoorsOpen, R, E>;
}

原有业务方法则不需要改动:

impl<S> Car<S>
where
    S: DoorsAreClosed,
{
    pub fn open_doors(self) -> Car<S::WithDoorsOpen> {
        self.change_state()
    }
}

所需修改被限制在状态表示和转换适配层,不会扩散到每个 Car 方法。对实际项目来说,这通常是比较合理的 OCP 边界。

为什么无法做到完全开放扩展

Doors × Roof 与 Doors × Roof × Engine 属于不同的类型空间。增加维度后,编译器需要知道:

  • 如何在类型中保存新状态;
  • 旧转换是否需要保留新状态;
  • 哪些操作依赖新状态;
  • 构造函数应当选择哪个初始状态。

因此,增加编译期状态维度时,通常无法让所有旧代码都保持不变。Rust 也没有内建的可扩展 type-level record 或 row polymorphism,不能自动向任意状态集合追加字段并推导全部转换。

在这里,OCP 的实际作用是控制修改范围。加入一个会影响类型不变量的维度,本身就会改变状态模型。

什么时候不适合继续使用 typestate

当状态维度频繁增加、组合数量很大,或者对象需要放进同一个集合时,运行时状态通常更合适:

pub enum DoorState {
    Open,
    Closed,
}

pub enum RoofState {
    Open,
    Closed,
}

pub struct Car {
    model: String,
    doors: DoorState,
    roof: RoofState,
}

这种设计会把检查从编译期移到运行时,但能让 API 更稳定,也更便于序列化、存储和动态分发。

typestate 适合状态数量较少、转换规则稳定,并且需要在编译期拒绝非法操作的场景。如果主要目的是减少样板代码,可以使用 declarative macro 或 procedural macro 生成状态类型与转换实现。宏能减少重复代码,却无法消除状态模型本身的变更。

备注:内容仅供参考。