Hackの `newtype` で実現する「型によるドメイン駆動設計」:プリミティブ執着からの脱却
コードレビューをしていると、未だに多くのプロジェクトで `string` や `int` の海に溺れているコードを見かける。「IDは単なる文字列だから `string` でいい」という慢心は、システムの規模が大きくなるにつれ、必ず致命的なバグとして帰ってくる。
Hackの `newtype` を使いこなせば、コンパイル時に型チェッカーが「意味的な誤用」を弾き飛ばしてくれる。今回は、実務レベルで即座に採用すべき、型安全を極めるためのドメインプリミティブ構築術を伝授する。
—
1. なぜ `type` ではなく `newtype` なのか?
Hackには `type`(型エイリアス)と `newtype`(不透明型)が存在する。多くのエンジニアが混同しているが、両者の境界は「抽象化の強制力」にある。
- `type`: 単なる別名。`type UserId = string` と定義しても、`UserId` は `string` と同義であり、`string` を要求する関数に `UserId` を渡しても何ら文句は言われない。
- `newtype`: 不透明型。定義したモジュール内でのみ内部構造が見える。外部からは「何らかの型」としてしか扱えず、明示的な変換関数を通さない限り、元の `string` や `int` と混ぜることは不可能だ。
これこそが、ドメインロジックにおける「最強の防壁」である。
—
2. 実践:強固なドメインプリミティブの構築
ユーザーIDと注文IDが共に `string` である場合、引数の順序を間違えても型チェッカーは黙認する。これを `newtype` で隔離しよう。
namespace Domain;
// 外部からは内部の string 構造は見えない
newtype UserId = string;
newtype OrderId = string;
final class IdFactory {
// コンストラクタを隠蔽し、ファクトリ経由で生成させるのが鉄則
public static function fromString(string $id): UserId {
// ここでバリデーションを挟むのが肝
if ($id === ”) throw new \InvalidArgumentException(‘ID cannot be empty’);
return $id;
}
}
function processOrder(UserId $user, OrderId $order): void {
// 誤って OrderId を UserId の位置に渡すと、HHVMが即座にエラーを吐く
// 開発者のミスをコンパイル時に潰せる、これぞ型安全の極み
}
—
3. パフォーマンスとランタイムへの影響
「型を複雑にするとHHVMの実行速度が落ちるのではないか」という懸念を持つ者がいるが、答えはNOだ。
HHVMのJITコンパイラは、`newtype` を型消去(Type Erasure)する。実行時には単なる `string` や `int` としてメモリに展開されるため、実行時のオーバーヘッドはゼロに近い。
ただし、注意点が一つ。「境界での検証」を怠るな。
外部APIからの入力やDBからの読み出し時に、必ず `from…` 系の静的メソッドを通し、型を昇格(Lift)させよ。そこさえ通過すれば、アプリケーションの深部では「型によって保証された正しい値」だけが流通する。
—
4. 実務で光る「型変換」の設計パターン
`newtype` を扱う際、最も面倒なのが「型をアンラップして元の値に戻す」処理だ。これを各所に散らばらせるとコードが汚染される。以下のように `asString()` メソッド等のインターフェースを統一せよ。
newtype EmailAddress = string;
final class Email {
public static function create(string $raw): EmailAddress {
if (!filter_var($raw, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException(‘Invalid email format’);
}
return $raw;
}
// 外部出力用(シリアライズやDB保存)
public static function toString(EmailAddress $email): string {
return $email;
}
}
この設計により、ビジネスロジックは「`EmailAddress` という概念」を操作することに集中でき、データ構造の詳細は境界線上に押し込めることができる。
—
結論:型はドキュメントを超えた「契約」である
コメントで「この変数は `UserId` です」と書くのは時間の無駄だ。そんなものは型チェッカーが理解できない。`newtype` を使えば、それはコンパイラレベルで強制される「契約」へと昇華される。
君たちがコードレビューで見るべきは、複雑なロジックそのものではない。「プリミティブな型が、いかにドメインの言葉へと変換されているか」だ。
型を惜しむな。`newtype` で境界を定義せよ。そうすれば、深夜3時のデプロイで、君が頭を抱えることはなくなるはずだ。
—
Hackを掌握せよ。型は、君を自由にするための最も強力な武器なのだから。