ドメイン型を強化するRust newtype パターン
プリミティブを1要素タプル構造体でラップし、UserId と OrderId を型システムに区別させます。実行時コストゼロで強力な型付けが得られます。
Patterns
詳細な説明
serde と newtype パターン
newtype はちょうど1つのフィールドを持つタプル構造体です。最初は無意味に見える(UserId は中身が i64 でしかない)かもしれませんが、UserId を期待する関数に order_id を渡すコードをコンパイラが拒否してくれます。
起点となるJSON
{
"user_id": 1234567,
"order_id": 9876543
}
自動生成された出力
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub user_id: i32,
pub order_id: i32,
}
newtype に昇格
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct UserId(pub i64);
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct OrderId(pub i64);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub user_id: UserId,
pub order_id: OrderId,
}
transparent シリアライズ
デフォルトでは serde はタプル構造体を1要素配列としてシリアライズするため、UserId(1) は [1] になります。内側の値そのもの(1)として出力するには #[serde(transparent)] を付けます。
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UserId(pub i64);
これで JSON 上は素の i64 と区別できなくなり、Rust 側だけが誤った混同を防いでくれます。
ドメインメソッドの追加
newtype にもメソッド、トレイト、コンストラクタを定義できます。
impl UserId {
pub fn new(id: i64) -> Result<Self, &'static str> {
if id <= 0 { return Err("id must be positive"); }
Ok(UserId(id))
}
}
これは "parse, don't validate"(検証ではなく解釈する)という設計指針の基盤です。UserId を持っているということ自体が「正しい値」を保証するからです。
ユースケース
ドメイン駆動設計、金融計算、複数の ID 型を扱うコードベースは newtype の恩恵を受けます。「間違った関数に間違った ID を渡す」種類のバグを丸ごとコンパイル時に潰せます。