型システムを「ドメインの言語」へ昇華させる:Hackにおける型安全なValue Object設計
コードレビューでよく見かける光景がある。関数の引数に `string $userId` や `int $amount` が並び、メソッド内部でバリデーションを繰り返すコードだ。
「なぜ、型システムがあるのにプリミティブな型で世界を記述するのか?」
Hackの `strict` モードは、単なる「エラーを防ぐためのガードレール」ではない。ドメインの制約をコンパイル時に強制し、実行時のランタイムチェックを極限まで排除するための武器だ。今回は、HackにおけるValue Objectパターンを駆使し、バグの混入余地をゼロにする設計思想を伝授する。
—
1. プリミティブ中毒(Primitive Obsession)からの脱却
多くのエンジニアは、`int` や `string` が「万能な型」であると錯覚している。だが、それは間違いだ。`UserId` も `ProductSku` も、メモリ上は単なる数値や文字列かもしれないが、ドメイン上の意味は全く異なる。
Hackの `newtype` と `opaque` 型を使い、これらを別々のドメインとして定義せよ。
実装例:型レベルでドメインを分離する
namespace App\Domain;
// opaqueキーワードにより、外部からは内部の実体(int)が見えない
// これにより、UserIdに誤ってAmountを渡すバグをコンパイル時に防げる
newtype UserId = int;
final class UserAccount {
public function __construct(
private UserId $id,
private string $email,
) {}
public function getId(): UserId {
return $this->id;
}
}
// 利用側のコード
function processUser(UserId $id): void { / … / }
$id = 123; // これはコンパイルエラーになる。UserId型として明示的に変換が必要
processUser(123);
このように `opaque` を使うことで、型チェッカーは「UserId型」と「int型」を厳格に区別する。`UserId` を受け取る関数に単なる `int` を渡せば、即座にコンパイルエラーだ。これが「バグを設計で殺す」ということだ。
—
2. HHVMの最適化を味方につける:パフォーマンスの真実
「オブジェクトを大量に生成するとパフォーマンスが落ちるのでは?」という懸念を持つエンジニアもいるだろう。しかし、HHVMのアーキテクチャを理解していれば、その不安は杞憂であるとわかる。
Hackの `newtype` は、コンパイル時のみ存在するメタデータだ。HHVMが生成するバイトコードや、その先のJITコンパイルにおいて、これらはプリミティブな型にまでインライン展開される。つまり、ドメイン駆動の抽象化を行っても、実行時のメモリ消費やCPUサイクルは、プリミティブ型を直接扱う場合と一切変わらない。
抽象化によるパフォーマンスの劣化を恐れる必要はない。恐れるべきは、複雑なロジックの中で「これがUserIdなのか、単なるカウンタなのか」を読み解くためにエンジニアが払う認知コストだ。
—
3. 実践:不変性を担保する「Value Object」パターン
非同期API連携や複雑なコンポーネント設計では、データの状態がいつ、どこで書き換わったかを追跡するのが困難になる。これを防ぐには、Value Objectを「不変(Immutable)」にするのが鉄則だ。
堅牢なValue Objectコード例
namespace App\Domain;
/
- 金額を扱うValue Object
- 不変性を担保し、型レベルで安全な演算を行う
/
final class Money {
public function __construct(
private int $amount,
private string $currency = ‘JPY’,
) {
invariant($amount >= 0, ‘金額は0以上である必要があります’);
}
public function add(Money $other): Money {
invariant($this->currency === $other->currency, ‘通貨単位が異なります’);
return new Money($this->amount + $other->amount, $this->currency);
}
public function getAmount(): int {
return $this->amount;
}
}
この実装が美しい理由は3つある:
1. invariantの活用: コンストラクタで制約を強制し、不正な状態のオブジェクト生成を許さない。
2. 不変性: `add` メソッドは新しいインスタンスを返すため、副作用によるバグが構造的に発生しない。
3. カプセル化: 内部の `$amount` は外部から直接変更不可能であり、ドメインの整合性が常に保たれる。
—
4. エンジニアへの提言:型は「ドキュメント」以上の存在である
多くのエンジニアにとって、型定義は「IDEの補完を出すためのヒント」でしかない。しかし、Hackを使いこなす我々にとって、型は「システムが守るべき不変の法則」である。
もしあなたがコードレビューで、`string` や `int` が関数の引数に並んでいるのを見たら、こう問いかけてほしい。
> 「このプリミティブな値は、他の値と混ざっても安全なのか? それとも、ドメインとしての『名前』を与えるべきなのか?」
型システムを正しく設計すれば、テストコードの数も劇的に減る。なぜなら、「不正なデータがシステムに入り込む」という事象そのものが、型チェッカーによって事前に排除されるからだ。
Hackの力を信じろ。そして、プリミティブの海から抜け出し、型という名のドメイン言語を設計せよ。それが、スケールし、メンテナンスされ続けるプロダクションコードへの唯一の道だ。