【Haskell学習|実務向け】型定義を磨けばテストは減る:データ設計でバグを未然に防ぐ「Unrepresentable」の考え方

1. 導入:なぜテストコードを減らすべきなのか

実務において、テストコードは資産であると同時に「保守コスト」でもあります。仕様変更のたびにテストを書き換えることに疲弊していませんか?実は、テストコードを100行追加するよりも、データ定義(型定義)を10行工夫する方が、バグを根絶する効果が高いケースが多々あります。本記事では、不正な状態を物理的に排除する「Make Illegal States Unrepresentable(不正な状態を表現不可能にする)」という設計思想について解説します。

2. 基礎知識:型システムによる「防御」

多くの開発者が「異常系テスト」に多大な時間を費やします。例えば、未ログインユーザーのIDがnullになる可能性を考慮したテストなどです。しかし、そもそも「型システム」でその状態を表現できなくしてしまえば、それらのテスト自体が不要になります。これが関数型プログラミングにおけるデータ設計の要諦です。

3. 実装・解決策:型によるバリデーション

「文字列でIDを扱う」のではなく、「ID専用の型を作る」、「空の状態をnullではなくOptionやMaybeで包む」といった工夫を行います。これにより、コンパイルを通すこと自体が「データの正当性を保証すること」とイコールになります。

4. サンプルプログラム:TypeScriptによる実装例

以下は、ユーザーの状態を型で安全に管理する例です。状態ごとに型を分けることで、不正な遷移をコンパイルエラーとして検出できます。

// 悪い例:全ての状態を一つのオブジェクトで管理すると、
// 「未登録なのに名前がある」といった不正状態が作れてしまう
type User = {
id: string | null;
name: string | null;
isRegistered: boolean;
};

// 良い例:状態を代数的データ型(タグ付き共用体)で定義する
// これにより、登録済みユーザーには必ず名前が存在することが保証される
type UnregisteredUser = { type: ‘unregistered’; id: string };
type RegisteredUser = { type: ‘registered’; id: string; name: string };

type AppUser = UnregisteredUser | RegisteredUser;

// 処理関数:型ガードにより、登録済みでなければ名前にはアクセスできない
function printUserName(user: AppUser): string {
if (user.type === ‘unregistered’) {
return ‘未登録ユーザーです’;
}
// ここでは自動的に RegisteredUser 型として扱われる
return `ユーザー名: ${user.name}`;
}

// 動作確認
const user: AppUser = { type: ‘registered’, id: ‘001’, name: ‘田中’ };
console.log(printUserName(user)); // 実行結果: ユーザー名: 田中

5. 応用・注意点:現場で活かすために

このアプローチを導入する際、最も注意すべきは「過剰な抽象化」です。あまりに複雑な型定義を行うと、逆に可読性が落ち、チームメンバーの学習コストを跳ね上げます。

現場で役立つポイント:
プリミティブ執着からの脱却:IDやメールアドレスを単なるstringとして扱わず、小さなオブジェクトやクラスに包む(Value Object)だけで、関数の引数ミスなどが激減します。
テストとのバランス:型で防げることは型に任せ、ビジネスロジックの「順序」や「条件分岐」など、型では表現しきれない部分にのみテストリソースを集中させましょう。

「型は最強のテスト仕様書である」という意識を持つだけで、あなたのコードの信頼性は劇的に向上します。ぜひ、次回のデータ定義から取り入れてみてください。

コメント

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