プリミティブへの決別:Hack `newtype` でドメインモデルを「型」に閉じ込める
PHPの世界では、`string` や `int` は単なるデータの容れ物に過ぎない。しかし、大規模システムの開発現場において、`string` が「ユーザーID」なのか「メールアドレス」なのか「トークン」なのかを型レベルで区別できないことは、致命的なバグの温床だ。
私はHHVMの深淵で、何千もの型チェックの失敗を見てきた。その結論は明確だ。「プリミティブな型をそのままドメインロジックに晒すな」。
今日は、Hackの `newtype` を使い、コンパイル時にドメインの整合性を保証する「型駆動設計」の奥義を授ける。
—
1. なぜ `type` ではなく `newtype` なのか
Hackには `type`(型エイリアス)と `newtype`(不透明型)がある。
`type UserID = string;` と宣言した場合、それは単なる別名に過ぎず、型チェッカー(hh_client)は `string` と `UserID` を同列に扱う。これでは、誤って `ProductID` を `UserID` として渡してもエラーを吐かない。
一方、`newtype` は不透明(Opaque)だ。モジュールの外からは、それが何で構成されているかを知ることはできない。これにより、型システムは「契約」として機能し始める。
—
2. 実装パターン:ドメインの境界を定義する
以下のコードは、`UserID` をプリミティブから解放するための最小かつ最強の構成例だ。
namespace App\Domain;
// 型定義は公開するが、その実体(string)は隠蔽する
newtype UserID = string;
final class UserIdentity {
// コンストラクタで不変性を担保
public function __construct(private string $value) {}
// ファクトリメソッドでバリデーションを強制
public static function fromString(string $val): UserID {
if (strlen($val) < 8) {
throw new \InvalidArgumentException('UserID must be at least 8 chars');
}
// ここで型キャストではなく、opaqueなUserIDとしてラップして返す
return $val :> UserID;
}
}
重要なアーキテクチャ上の洞察
- `:> (As Operator)` の活用: `newtype` の内部実装へアクセスできるのは、定義されたファイル内のみだ。外部からは値の改竄ができない。
- パフォーマンス: `newtype` はコンパイル時にのみ評価される「型」であるため、HHVMの実行時オーバーヘッドはゼロだ。クラスでラップする手法(Value Object)と異なり、メモリ消費も増えない。型システムの恩恵をタダで受けられるのだ。
—
3. 非同期API連携での活用:型安全な境界線
外部APIとの連携において、JSONの `string` をそのまま関数に渡すのは怠慢だ。`newtype` を使えば、受信したデータをドメイン層へ引き渡す前に、型レベルでバリデーション済みであることを保証できる。
namespace App\Infrastructure;
use App\Domain\UserID;
async function fetchUserData(UserID $uid): Awaitable
// この関数が呼ばれた時点で、uidは既に検証済みのUserIDであることが確定している
// $uidが単なるstringでないことが型システムにより保証されているため、
// バリデーション漏れによるバグは物理的に不可能になる
}
// 利用側のコード
async function main(): Awaitable
$raw_id = $_GET[‘id’] ?? ”;
// ここで境界を超えてUserIDを生成
$uid = UserIdentity::fromString($raw_id);
await fetchUserData($uid);
}
—
4. チーフアーキテクトからの忠告
この手法を導入する際、以下の3点だけは心に刻んでおいてほしい。
1. ライブラリの境界でキャストする: `newtype` はあくまで内部ドメインの整合性を高めるためのものだ。外部(データベースやJSONデコーダー)と通信する際は、必ず境界(Boundary)で `newtype` への変換・検証を行え。
2. 無理な抽象化を避ける: あらゆるプリミティブを `newtype` にする必要はない。ビジネスロジックの核心にある、取り違えが致命的なIDやキーに対してのみ適用せよ。
3. HSL(Hack Standard Library)の型を活用せよ: HSLには堅牢なコレクション型が備わっている。`newtype` と `vec` や `dict` を組み合わせることで、ドメインモデルの表現力は飛躍的に向上する。
結論
`newtype` を使った設計は、単なるコードの美学ではない。それは、「型チェッカーにドメインのルールを教え込む」ということだ。
プログラムを実行する前に、IDEが赤い波線で「それはUserIDではない」と教えてくれる。その安心感こそが、大規模開発を支える真のパフォーマンスであると知れ。
コードは書くものではない。構築するものだ。今日から君の型定義を、より厳格なものへと進化させろ。