Hackにおける例外の「静的型付け」:型レベルでバグを殲滅する設計論
Hackのコードベースで、`try-catch`が乱立し、どの関数がどの例外を投げるのかがブラックボックス化している――そんな現状に絶望しているのなら、今すぐ考えを改める必要がある。
Javaのチェック例外(Checked Exceptions)がなぜ敬遠されたのか? それは「例外」という制御フローを、疎結合であるべきアーキテクチャに過度に押し付けたからだ。しかし、Hackの型システムは違う。我々は、「例外はデータである」という認識からスタートする。
今日は、HHVMの型チェッカーを最大限に活用し、例外処理を型レベルで契約(Contract)として定義する、極めて「Hackらしい」設計パターンを伝授しよう。
—
1. なぜ「例外」を型定義に含めるべきなのか
多くのエンジニアは例外を「想定外の事態」と呼ぶが、それは間違いだ。API通信におけるタイムアウト、DBのロック競合、バリデーションエラー。これらは「発生することが予期されている分岐」に過ぎない。
Hackの`<<__Override>>`やリファクタリングを駆使する際、例外が型定義に含まれていないと、呼び出し元は「何をキャッチすべきか」をコンパイラに尋ねることができない。その結果、`catch (\Exception $e)` と書かれた巨大なゴミ箱のようなコードが生まれる。これは型システムへの冒涜だ。
—
2. 厳格な例外設計:`` に頼らないために
Hackにおいて例外を扱う際、最も美しいのは「Result型パターン」と「明示的な型アノテーション」のハイブリッドだ。
以下のコードを見てほしい。これは、特定の例外を投げることが保証されたサービス層の設計例だ。
namespace App\Service;
/
- 例外を型レベルで分類するMarker Interface
/
interface ServiceException extends \Exception {}
class DatabaseTimeoutException extends \Exception implements ServiceException {}
class InsufficientFundsException extends \Exception implements ServiceException {}
final class PaymentProcessor {
/
- このメソッドは特定のビジネス例外を投げることを明示する。
- Hackには現時点でJavaのような throws 宣言はないが、
- PHPDocと型システムを組み合わせることで、静的解析を強制する。
- @throws DatabaseTimeoutException
- @throws InsufficientFundsException
/
public function charge(int $amount): void {
if ($this->isDatabaseBusy()) {
throw new DatabaseTimeoutException(“DB is busy”);
}
// … logic
}
}
—
3. 実務で「例外の漏れ」を物理的に防ぐ実装パターン
コードレビューでよく見かけるのは、「例外を投げているのに、呼び出し元がそれを知る由もない」ケースだ。これを防ぐには、例外をDTO(Data Transfer Object)のように扱い、呼び出し側に「ハンドリングを強制する」設計に切り替える必要がある。
実践:例外のラップと排他的キャッチ
namespace App\Controller;
use App\Service\{PaymentProcessor, DatabaseTimeoutException, InsufficientFundsException};
final class CheckoutController {
public function __construct(private PaymentProcessor $processor) {}
public function execute(): void {
try {
$this->processor.charge(1000);
} catch (DatabaseTimeoutException $e) {
// 一時的なエラー:リトライを検討
$this->handleRetry();
} catch (InsufficientFundsException $e) {
// ビジネスエラー:ユーザーに通知
$this->showError(“残高が不足しています”);
}
// 予期せぬ例外はあえてキャッチしない(上位のグローバルハンドラに任せる)
}
}
ここが重要:
もし将来、`PaymentProcessor`が新しい例外(例: `CurrencyMismatchException`)を投げるように変更されたらどうなるか?
適切な型定義と静的解析ツール(`hh_client`)をCIに組み込んでいれば、`catch`ブロックの網羅性をチェックするカスタムリントルールを適用することも可能だ。
—
4. パフォーマンスの真実:HHVMの内部構造から見ると
例外は、スタックトレースを生成するコストが非常に重い。HHVMにおいて `throw` は、VMが現在の実行状態(プロシージャレコード)を巻き戻し、例外ハンドラを探す検索プロセスをトリガーする。
- 多用すべきでないケース: ループ内での例外発生。これはパフォーマンスを劇的に劣化させる。
- 設計の指針: 「制御フローとしての例外」は避けるべきだ。バリデーションエラーのような頻繁に起こりうるものは、例外ではなく `Result
` 型や `Option ` を返すべきである。
例外はあくまで「回復不可能な、あるいは極めて稀な異常系」のために温存せよ。
—
結論:コードは「対話」である
Hackの型システムは、単なるバグ除けのフェンスではない。それは、あなたが書いたコードが「将来の自分やチームに対して、どう振る舞うべきか」を伝えるための強力な言語だ。
1. `throws`アノテーションをサボるな。 ドキュメントは嘘をつくが、型とシグネチャは嘘をつかない。
2. 例外をドメインオブジェクトとして定義せよ。 `\Exception` を直投げするコードは、レビューで即座に拒絶すべきだ。
3. キャッチの範囲を絞れ。 `\Throwable` をキャッチするのは、アプリケーションの最上位階層のみだ。
この設計を徹底すれば、あなたのシステムは「何が起きてもクラッシュしない」という確固たる自信の上に成り立つ。それが、Hackを操るエンジニアの矜持だ。
さあ、IDEを開いて、曖昧な `try-catch` を全て型で定義し直すことから始めよう。世界最高峰のアーキテクチャが、君の背中を押してくれるはずだ。