Hackを掌握する極限の知見:コンストラクタインジェクションと型安全な依存解決の極意
テックリードの私が生現場のコードレビューで最も厳しく見るのは、「依存関係の解決をフレームワークの黒魔術や実行時アノテーションに依存していないか」という点だ。
動的言語上がりのプログラマブルな発想で、コンテナから`get(‘SomeService’)`の文字列キーでインスタンスを取り出すコードを見るたびに、私はこう問いかける。
「その文字列、タイポしたら誰が検知するのか? 本番デプロイ後の例外か?」
HHVM上で動作するHack言語の真価は、その厳格な静的型システム(Strict Mode)と、圧倒的なスループットを叩き出すJITコンパイラにある。依存関係の解決は、ランタイムの負荷であってはならない。そして何より、「コンパイル時に依存の破綻が静的検知されない設計は、技術的負債の温床である」。
今回は、Hackの型システムを限界まで酷使し、実行時オーバーヘッドをゼロにしつつ、コンパイル時に依存関係の整合性を100%保証する「型安全なDIコンテナ設計」の実装パターンを伝授する。
—
1. なぜ「文字列キーのDI」はHackにおいて悪なのか
多くの言語(PHPを含む)におけるDIコンテナは、次のようなキーベースのルックアップを行いがちだ。
// 悪夢のアンチパターン(動的解決)
$container->get(‘UserProcessor’);
このアプローチには、Hackのアーキテクチャにおいて致命的な問題が2つある。
1. 型チェッカーの盲点: `’UserProcessor’` という文字列は単なるプリミティブであり、対応する実体が変更・削除されたとき、HHVMの型チェッカー(`hh_client`)はこれを追跡できない。
2. JIT最適化の阻害: 文字列ベースのマップ引き当ては、HHVMの高度な型推論とメソッドインライン展開(Inlining)の最適化パスを阻害する。
我々が目指すべきは、「型の存在そのものが依存関係のグラフを形成する」世界線だ。
—
2. 徹底的なStrict Mode:型安全DIコンテナの実装
以下のプロダクションコードを見てほしい。これは、フレームワークの魔術に頼らず、Hackの Generics(総称型)と Shapes を駆使して、コンパイル時に依存関係の不整合を完全にシャットアウトするミニマルかつ堅牢なDIコンテナの設計だ。
hh
<
namespace Hack\Architect\DI;
/
- すべてのサービスの基底インターフェース
/
interface IService {}
/
- 型安全な依存関係を解決するためのコンテナ例外
/
class DependencyResolutionException extends \Exception {}
/
- 圧倒的なパフォーマンスと型安全性を両立するコンパイル時指向DIコンテナ
/
final class TypeSafeContainer {
// 各サービスのファクトリ(クロージャ)を型付きで保持
// キーには厳密にClassString(クラス名文字列)を使用する
private dict
// シングルトンインスタンスのキャッシュ
private dict
/
- サービスのファクトリを登録する
- クラスのFQN(完全修飾名)をキーに強制することでタイポをコンパイルエラーにする
/
public function bind
string $className,
(function(TypeSafeContainer): T) $factory,
): void {
// Hackの高度な型システムにより、factoryの戻り値がTであることを保証
$this->factories[$className] = $factory;
}
/
- 型パラメータ T を通じて、キャスト不要かつ静的型安全にインスタンスを取得する
/
public function get
// キャッシュ済みであればそのまま返す(シングルトン保証)
if (C\contains_key($this->instances, $className)) {
$instance = $this->instances[$className];
// 静的解析を通る厳密な型アサーション
Invariant($instance is T, “Resolved instance does not match expected type.”);
return $instance;
}
if (!C\contains_key($this->factories, $className)) {
throw new DependencyResolutionException(
\Str\format(“Service not bound: %s”, $className)
);
}
$factory = $this->factories[$className];
$instance = $factory($this);
// 解決されたインスタンスが期待する型 T と一致しているかを実行時にも担保
Invariant($instance is T, \Str\format(“Factory for %s returned invalid type.”, $className));
$this->instances[$className] = $instance;
return $instance;
}
}
この設計のキモ:
- `string $className` と Generics の融合: 呼び出し側で `->get
(MyService::class)` のように記述することで、IDEの補完と `hh_client` の型チェックが完全に連動する。 - 無駄なリフレクションの排除: 実行時に高コストなリフレクション API (`ReflectionClass`) を叩く必要はない。すべては定義時にクロージャとしてビルドされるため、HHVMのJITコンパイラにとって極めて優しく、ネイティブに近い速度で動作する。
—
3. 実践:非同期API連携を伴うコンポーネント設計
では、上記のコンテナを実際のWebアプリケーション層(例えば非同期APIクライアントとリポジトリの結合)でどう適用するか。実務の現場ですぐに使える美しいコードを示す。
hh
<
namespace Hack\Architect\App;
use namespace Hack\Architect\DI;
use HH\Asio;
interface IHttpClient extends DI\IService {
public async function getAsync(string $endpoint)[]: Awaitable
}
final class ProdHttpClient implements IHttpClient {
public async function getAsync(string $endpoint)[]: Awaitable
// 実際にはcurlやHHVMのAsyncMysqlなどを叩く
// 共変性・非同期境界([] Context)の厳格な維持
await Asio\usleep(10000);
return “{\”status\”: \”success\”, \”data\”: \”payload from ” . $endpoint . “\”}”;
}
}
interface IUserRepository extends DI\IService {
public async function fetchUserDataAsync(int $userId)[]: Awaitable
}
/
- 依存性注入(DI)をコンストラクタ経由で明示的に強制する
/
final class UserRepository implements IUserRepository {
// フィールドの型はインターフェースに依存させ、具象クラスには依存させない
public function __construct(
private IHttpClient $httpClient,
) {}
public async function fetchUserDataAsync(int $userId)[]: Awaitable
$response = await $this->httpClient->getAsync(“/users/” . (string)$userId);
return $response;
}
}
—
4. ワイヤリング(配線)フェーズ:ブートストラップの構築
最後に、これらをどう組み立てるか。ブートストラップの段階で型安全なバインディングを行います。もしここで依存関係の欠落や型のミスマッチがあれば、本番環境どころか手元の `hh_client`(あるいはCIのビルドパイプライン)が一瞬で検知してエラーを吐き出します。
hh
<
namespace Hack\Architect;
use namespace Hack\Architect\DI;
use namespace Hack\Architect\App;
use HH\Asio;
<<__EntryPoint>>
async function main_async()[]: Awaitable
$container = new DI\TypeSafeContainer();
// 1. HttpClientのバインド
$container->bind(
App\IHttpClient::class,
$c ==> new App\ProdHttpClient(),
);
// 2. UserRepositoryのバインド(ここでIHttpClientがコンストラクタインジェクションされる)
$container->bind(
App\IUserRepository::class,
$c ==> new App\UserRepository(
// コンテナから型安全に依存を取り出す。型が違えば即座に静的エラー
$c->get
),
);
// 実行テスト
$userRepo = $container->get
$jsonResult = await $userRepo->fetchUserDataAsync(42);
\fprintf(STDOUT, “Result: %s\n”, $jsonResult);
}
—
テックリードからの最終提言
動的言語のノリを引きずった「とりあえず動くコード」は、システムのスケールとともに開発チームの首を絞める。
Hack言語の厳格な型チェッカーと、今回紹介したようなコンパイル時指向のコンテナ設計を組み合わせることで、「動かないコードはそもそもビルドできない」という強固な要塞を築き上げることが可能だ。
コードレビューでこのパターンを見かけたら、自信を持ってこう言えばいい。
「これが、HHVMのポテンシャルを極限まで引き出す真の型安全だ」と。