Typestateパターン: 問題

ある値に対して、その現在の状態に応じた有効な操作だけを許可するには、どうすればよいでしょうか?

// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

use std::fmt::Write as _;

#[derive(Default)]
struct Serializer {
    output: String,
}

impl Serializer {
    fn serialize_struct_start(&mut self, name: &str) {
        let _ = writeln!(&mut self.output, "{name} {{");
    }

    fn serialize_struct_field(&mut self, key: &str, value: &str) {
        let _ = writeln!(&mut self.output, "  {key}={value};");
    }

    fn serialize_struct_end(&mut self) {
        self.output.push_str("}\n");
    }

    fn finish(self) -> String {
        self.output
    }
}

fn main() {
    let mut serializer = Serializer::default();
    serializer.serialize_struct_start("User");
    serializer.serialize_struct_field("id", "42");
    serializer.serialize_struct_field("name", "Alice");

    // serializer.serialize_struct_end(); // ← しまった、忘れていた

    println!("{}", serializer.finish());
}
  • この Serializer は、構造化された値を書き出すことを目的としています。

  • しかし、この例では finish() の前に serialize_struct_end() を呼ぶのを忘れています。その結果、シリアライズされた出力は不完全になったり、構文的に不正になったりします。

  • これを修正する 1 つの方法は、内部状態を手動で追跡し、現在の状態が不正であれば serialize_struct_field()finish() のようなメソッドから Result を返すことです。

  • しかし、これには欠点があります:

    • 実装者にとって間違えやすい方法です。Rust の型システムは、状態遷移の正しさを強制する助けになりません。

    • また、実行時ではなくソースコード上で誤用されている操作に対してまで、ユーザーが Result 値を処理しなければならず、不要な負担も増えます。

  • よりよい解決策は、有効な状態遷移を型システム内に直接モデル化することです。

    次のスライドでは、typestateパターン を適用してコンパイル時に正しい使用法を強制し、互換性のないメソッドを呼び出したり、必要な操作をし忘れたりすることを不可能にします。