プリミティブへの依存は、ドメインモデルの死を意味する
コードレビューをしていて、最も背筋が凍る瞬間はどんなときか。
それは、決済金額やユーザーID、メールアドレスといったビジネスの根幹をなすドメイン概念が、単なる `int` や `string` という生(プリミティブ)の型で表現されているを見たときだ。
// ⚠️ 最悪のアンチパターン
public function transfer(int $fromAccountId, int $toAccountId, int $amount): void {
// ここで何が起きる?
// $amount に負の値が渡されたら?
// $fromAccountId と $toAccountId を逆に渡しても、型チェッカーは何も文句を言わない。
}
このコードは、型システムの恩恵を自ら捨て去る自殺行為だ。PHPからHackへ移行したチームによく見られる悪癖だが、Hackの厳格な静的型付け(Strict Mode)と型チェッカーのポテンシャルを完全に過小評価していると言わざるを得ない。
HHVMのJITコンパイラとHackの型システムは、適切に設計されたドメインモデルを狂気的なまでの速度と安全性で実行するためにある。今回は、Hackの強力な型機能(新世代の型エイリアスや特性)をフル活用し、「不正な状態を表現すること自体が不可能な(Make illegal states unrepresentable)」 Value Object(値オブジェクト)の設計パターンを叩き込む。
—
1. HackにおけるValue Objectの本質と設計原則
Value Objectとは、ドメイン駆動設計(DDD)における概念であり、以下の特性を持つ。
1. 識別子を持たず、値そのもので等価性が決まる
2. 不変(Immutable)である
3. 自身の中にバリデーションを持ち、生成された時点で常に「正しい」ことが保証される
Hackでこれを実装する場合、単なるクラス(`class`)ではなく、`readonly` プロパティや `final`、さらにはプリミティブ型をラップする「opaque type(不透明型)」の概念、あるいは厳密にカプセル化された不変クラスを使い分ける。
特に実務で最も費用対効果が高いのは、「不変なファイナルクラス+厳格なコンストラクタ」によるパターンだ。
—
2. 実践:プロダクションコードで示す堅牢なValue Object
以下のコードを見てほしい。ECサイトの決済ドメインを想定した、実務でそのまま使えるプロダクション品質のコードだ。
プリミティブ執着(Primitive Obsession)を完全に排除し、型チェッカーにビジネスルールを強制させている。
<
namespace Domain\Payment;
/
- 決済金額を表すValue Object
- 負の値や精度の崩壊を型レベルで完全に防ぐ
/
final class Money {
private int $amount;
const int MIN_AMOUNT = 1;
const int MAX_AMOUNT = 10_000_000;
<<__Rx>>
public function __construct(int $amount) {
if ($amount < self::MIN_AMOUNT || $amount > self::MAX_AMOUNT) {
throw new \InvalidArgumentException(
\Str\format(‘無効な金額です: %d。金額は %d から %d の間でなければなりません。’, $amount, self::MIN_AMOUNT, self::MAX_AMOUNT)
);
}
$this->amount = $amount;
}
<<__Rx>>
public function getAmount(): int {
return $this->amount;
}
<<__Rx>>
public function add(Money $other): Money {
// オーバーフローの検知(実務では必須)
$sum = $this->amount + $other->getAmount();
return new Money($sum); // コンストラクタのバリデーションが再実行されるため安全
}
<<__Rx>>
public function equals(Money $other): bool {
return $this->amount === $other->getAmount();
}
}
/
- 口座IDを表すValue Object
- 単なる string や int ではなく、専用の型を持つことで引数の順序違いミスをコンパイル時に防ぐ
/
final class AccountId {
private string $value;
<<__Rx>>
public function __construct(string $value) {
// UUID v4 の簡易バリデーション
if (!\Preg\match(‘/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i’, $value)) {
throw new \InvalidArgumentException(\Str\format(‘無効な口座ID形式です: %s’, $value));
}
$this->value = $value;
}
<<__Rx>>
public function getValue(): string {
return $this->value;
}
<<__Rx>>
public function equals(AccountId $other): bool {
return $this->value === $other->getValue();
}
}
この設計が優れている理由
1. 不正な値のインスタンス化が不可能
`new Money(-500)` や不正なフォーマットの `AccountId` を生成しようとすると、即座に例外がスローされる。つまり、システム内の他のレイヤー(ユースケース層やインフラ層)では、「この変数に入っているデータは絶対に正しい」という前提でビジネスロジックを組むことができる。バリデーションの冗長なチェインを書く必要がなくなるのだ。
2. 型ミスの完全なコンパイル時検出
冒頭のアンチパターンを思い出してほしい。もし `transfer(AccountId $from, AccountId $to, Money $amount)` というシグネチャであれば、開発者がうっかり `$from` と `$to` を逆に渡したり、金額の代わりに口座IDを渡したりした瞬間、Hackの型チェッカー(`hh_client`)が赤くエラーを吐き出す。IDEレベルでバグが潰れる。
—
3. HHVMアーキテクチャとパフォーマンスの観点
「でも、プリミティブをオブジェクトでラップしたら、インスタンス生成のオーバーヘッドやメモリ消費が増えて遅くなるのでは?」
シニアエンジニアであれば当然そこを懸念するはずだ。結論から言えば、HHVM(HipHop Virtual Machine)の特性を理解していれば、恐れる必要はない。
JITコンパイラとスカラー置換(Scalar Replacement of Aggregates)
HHVMのトランスレータとJITエンジンは極めて高度だ。ライフサイクルが短く、イミュータブルな小規模オブジェクト(Value Object)は、JITの最適化フェーズにおいて「スカラー置換(オブジェクトを分解してプリミティブ変数の集まりに展開する最適化)」のターゲットになり得る。
結果として、ヒープ上のメモリ割り当てやガベージコレクション(GC)の負荷が劇的に軽減されるケースが多い。
ただし、以下のアンチパターンには注意せよ:
- 巨大なループ内で何百万回もValue Objectを新規生成・破棄し続ける
(極端なホットスポットでは、プリミティブの直接操作や、静的なファクトリーメソッドでのキャッシュを検討すべき場合もある。だが、通常のWebリクエストライフサイクルにおいて、DTOやVOの生成コストはボトルネックにならない)
—
4. コードレビューで使える「良いコード・悪いコード」の判断基準
チームメンバーにこの設計を浸透させるための判断基準を共有しよう。
| 評価 | コード例 | 理由 |
| :— | :— | :— |
| ❌ NG | `public function sendEmail(string $email): void` | `string` 型では、単なる文字列(”hello”など)が渡されるのを防げず、ドメインとしての意味が希薄。 |
| ⚠️ 微妙 | クラスはあるが、プロパティが `public` で外部から書き換え可能。 | 不変性(Immutability)が担保されておらず、意図せず値が書き換わるバグの温床になる。 |
| 🏆 最高 | `final class Email { private string $email; … }` | 不変であり、生成された時点で必ず正しいメールアドレスであることが保証されている。 |
—
5. まとめ:型システムはドメインの「言語」である
HackのStrict Modeと強力な型チェッカーは、単に「TypeErrorを防ぐためのお守り」ではない。
それは、ドメインのビジネスルールをコード上の型として表現し、開発者間の共通言語(Ubiquitous Language)をコードに定着させるための最強の武器である。
プリミティブへの執着を断ち切り、Value Objectでドメインをモデル化せよ。型チェッカーがあなたのコードの最初の、そして最も厳格なレビュアーになってくれるはずだ。