外部APIの荒波から型システムを守れ:Hackの `Shape` 型で実現する、ノーペナルティなAPIレスポンス・マッピング戦略
コードレビューをしていると、外部APIから返ってきたレスポンスを `array
「動的言語のノリ」をHackに持ち込むのはもうやめよう。我々が使っているのは、HHVMという極限まで最適化された仮想マシン上で、厳格な静的型チェッカー(hhvm)の恩恵をフルに受けることのできるHackなのだから。
外部からのJSONという「不確実な外部世界」のデータを、いかにしてアプリケーション層の「堅牢なドメインモデル」へと安全に流し込むか。今回は、Hackの `shape` 型を武器にした、妥協なきAPIマッピングの極意を伝授する。
—
なぜ `array` でも `class` でもなく `shape` なのか?
APIレスポンスの構造を定義する際、以下の選択肢で悩んだことはないか?
1. `array
2. 通常の `class`(DTO) : インスタンス化のオーバーヘッドがあり、単なる構造データに対してボイラープレート(冗長なコード)が多すぎる。
3. `shape` 型 : 構造的サブタイピングを持ち、実行時のメモリオーバヘッドを最小限(プレーンな配列と同等)に抑えつつ、静的型チェッカーの完全な保護を受けられる。
HHVMのアーキテクチャにおいて、`shape` は内部的には最適化されたハッシュマップ(またはインデックス配列)として扱われる。つまり、クラスのインスタンス生成コストを払うことなく、オブジェクト指向を凌駕する厳密なキーと値の型制約を得られる。これを使わない手はない。
—
実践:プロダクションコードで示す堅牢なマッピング設計
以下のコードは、外部の決済APIから取得したユーザーおよび請求データを、Hackの `strict` モードで完全に型安全にハンドリングする設計パターンである。
単に型を定義するだけでなく、「境界でのバリデーション(Untrusted -> Trusted)」をどう行うかの実例を示す。
hhfile
<<__Strict>>
namespace Acme\Api;
/
- 外部APIから返却されるJSONの構造を定義するShape群。
- オプショナルなキーには ? を付与し、予期せぬ欠損に備える。
/
type RawAddressShape = shape(
‘zip_code’ => string,
‘street’ => string,
‘city’ => string,
);
type RawUserResponseShape = shape(
‘id’ => int,
‘email’ => string,
‘is_active’ => bool,
‘address’ => ?RawAddressShape,
‘metadata’ => ?dict
);
/
- アプリケーション内部でドメイン層として扱うイミュータブルなDTO。
- ここではあえてクラスを使い、振る舞いを持たせる。
/
class UserProfile {
public function __construct(
public int $id,
public string $email,
public bool $isActive,
public string $city,
) {}
}
class ApiMapper {
/
- 外部の不確実な入力を受け取り、型安全なShapeへキャストしつつ検証する。
- 万が一、APIの仕様変更で型が一致しない場合は即座に例外を投げる。
/
public static function mapUser(mixed $rawJson): UserProfile {
// 1. データの根幹がdict/arrayであることを保証
if (!\is_dict($rawJson) && !\is_array($rawJson)) {
throw new \InvalidArgumentException(“Invalid API response format: expected map.”);
}
// 2. 厳密な Shape へのダウンキャストとキー存在確認
// HHVMの型チェッカーと連携し、ここで静的・動的な安全性を担保する
$data = Shapes::idx($rawJson, ‘id’);
if (!\is_int($data)) {
throw new \InvalidArgumentException(“Field ‘id’ must be an integer.”);
}
// ※実際のプロダクションでは、再帰的なバリデーションヘルパーや
// 型アサーションライブラリを通じて一括でshapeにバインドする。
// ここでは安全にキャストされたと仮定したshapeデータを生成
$shape = shape(
‘id’ => (int)Shapes::idx($rawJson, ‘id’, 0),
‘email’ => (string)Shapes::idx($rawJson, ‘email’, ”),
‘is_active’ => (bool)Shapes::idx($rawJson, ‘is_active’, false),
‘address’ => self::extractAddress(Shapes::idx($rawJson, ‘address’)),
);
return self::toDomain($shape);
}
private static function extractAddress(mixed $rawAddress): ?RawAddressShape {
if (!\is_dict($rawAddress) && !\is_array($rawAddress)) {
return null;
}
return shape(
‘zip_code’ => (string)Shapes::idx($rawAddress, ‘zip_code’, ”),
‘street’ => (string)Shapes::idx($rawAddress, ‘street’, ”),
‘city’ => (string)Shapes::idx($rawAddress, ‘city’, ‘Unknown’),
);
}
private static function toDomain(RawUserResponseShape $shape): UserProfile {
$city = $shape[‘address’]?[‘city’] ?? ‘Unknown’;
return new UserProfile(
$shape[‘id’],
$shape[‘email’],
$shape[‘is_active’],
$city,
);
}
}
—
アーキテクチャ上の重要な注意点:パフォーマンスとメモリ
HHVM環境において、APIレスポンスのマッピング処理を行う際は以下の2点に気を配らなければならない。
1. 不要なオブジェクトアロケーションの回避
大規模なリストAPI(例: 1万件のトランザクション履歴)を処理する場合、すべてのレコードに対して重いクラスインスタンスを生成すると、Garbage Collector(GC)に過大な負荷がかかる。
データ集計やフィルタリングのパイプラインの途中段階では `shape` 型のまま扱い、最終的にビューやドメイン境界を越える直前のみオブジェクトに変換する、あるいはそのまま `shape` を維持するのが最も効率的である。
2. `Shapes::idx` と直接アクセスの使い分け
`$shape[‘key’]` による直接アクセスは、型チェッカーがキーの存在を保証している場合(非オプショナルキー)にのみO(1)の高速なハッシュルックアップとして動作する。
オプショナルキー(`?type`)に対しては、必ず `Shapes::idx()` やガード節を用いて安全にアクセスすること。さもないと、実行時エラー(Undefined index)の罠を踏むことになる。
—
チーフアーキテクトからの提言
「APIから来たデータをそのまま変数に突っ込んでよしとする」ような開発スタイルは、Hackの世界では技術的負債の最上位に位置する。
`shape` 型をマスターすることは、「外部の混沌(カオス)」と「内部の秩序(ストリクト)」の境界線に堅牢な関所を築くことと同義だ。
型チェッカーを味方につけ、実行時エラーをコンパイルタイム(静的解析時)に駆逐せよ。それこそが、Hack言語を使いこなすエンジニアの特権であり、義務である。