【実務・中級編】Hackの『Value Object』設計パターン:型システムでドメインを表現する – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

型システムを「ドメインの言語」へ昇華させる: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の力を信じろ。そして、プリミティブの海から抜け出し、型という名のドメイン言語を設計せよ。それが、スケールし、メンテナンスされ続けるプロダクションコードへの唯一の道だ。

タイトルとURLをコピーしました