コードに意味を与える技術

成功と失敗を明確にする結果オブジェクトパターン - コードで意図を伝えるエラーハンドリングの実践

Tags: エラーハンドリング, 結果オブジェクト, コード設計, 可読性, パターン

はじめに:コードの「結果」が伝える意図

ソフトウェア開発において、関数やメソッドの実行結果は、単なる戻り値だけでは語り尽くせない情報を含んでいる場合があります。特に、処理が成功した場合の値と、何らかの理由で失敗した場合のエラー情報です。これらの「結果」をコード上でどのように表現するかは、そのコードが何を意図しているのか、呼び出し側がどのように応答すべきなのかを伝える上で非常に重要です。

従来の一般的な手法としては、成功時には正常な値を返し、失敗時には特定の戻り値(例: null, -1, 空のコレクション)や例外をスローするといった方法があります。しかし、これらの方法だけでは、コードの意図が曖昧になったり、エラー処理の漏れを引き起こしたりする可能性があります。

この記事では、成功と失敗の両方の可能性を明示的に表現するための「結果オブジェクトパターン」に焦点を当て、これがコードの意図をどのように伝えるのか、具体的なコード例を交えて解説します。

問題提起:従来の戻り値と例外だけでは伝わりにくい意図

関数の実行結果を伝える際、以下のような課題に直面することがあります。

  1. 戻り値によるエラー通知の曖昧さ:

    • エラーを示すためにnullや特定の値を返す場合、呼び出し側はその値がエラーを示すことを「知っている」必要があります。ドキュメントや規約に依存しがちで、コードだけでは意図が読み取りにくいことがあります。
    • 正常な戻り値の型がnullableでない場合でも、エラー時にnullを返すといった規約は、型の安全性と矛盾する可能性があります。
    • 呼び出し側がnullチェックや特定の値のチェックを忘れると、意図しないバグにつながります。

    ```typescript // Before: 戻り値でエラーを通知(nullを返す) function findUserById(id: string): User | null { // ユーザーが見つかれば User オブジェクトを返す // 見つからなければ null を返す if (id === "unknown") { return null; } return { id: id, name: "Test User" }; // 実際にはDBなどから取得 }

    // 呼び出し側 const user = findUserById("some-id"); // nullチェックを忘れる可能性がある // user.name; // nullの場合、実行時エラーになる可能性 ```

  2. 例外によるエラー通知の限界:

    • 例外は「例外的な状況」を示すために使用されるべき、という考え方があります。しかし、「ファイルが見つからない」「ユーザーが見つからない」といった状況が、ビジネスロジック上起こりうる通常の結果である場合、これを例外で表現すると、コードのフローが読みにくくなることがあります。
    • チェック例外(Checked Exception)を持たない言語では、呼び出し側がどのような例外が発生しうるかを知るのが難しく、例外処理の漏れにつながりやすいです。
    • 例外処理(try-catch)は、通常の処理フローとは異なる記述になりがちで、コードの可読性を損なうことがあります。

    ```typescript // Before: 例外でエラーを通知 class UserNotFoundException extends Error {}

    function findUserByIdThrows(id: string): User { if (id === "unknown") { throw new UserNotFoundException(User with id ${id} not found.); } return { id: id, name: "Test User" }; }

    // 呼び出し側 try { const user = findUserByIdThrows("some-id"); // ユーザーが見つかった場合の処理 } catch (e) { if (e instanceof UserNotFoundException) { // ユーザーが見つからなかった場合の処理 } else { // その他の例外処理 throw e; // または別の処理 } } // 正常系と異常系が try-catch で分断され、フローが読みにくくなる場合がある ```

これらの課題に対し、結果オブジェクトパターンは、関数が返しうる全ての可能性(成功時の値と失敗時のエラー情報)を単一の型で表現することで、コードの意図をより明確に伝えるアプローチを提供します。

解決策:結果オブジェクトパターンによる意図の明確化

結果オブジェクトパターンでは、関数の戻り値として、成功状態と失敗状態のどちらか一方を表すオブジェクトを返します。このオブジェクトは、成功時には成功した値を含み、失敗時にはエラー情報を含みます。

概念としては、以下のような型で表現できます。

// 概念的な Result 型の定義例
// これはあくまで概念であり、実際の言語ではクラスや構造体で実現します。
type Result<T, E> = Success<T> | Failure<E>;

interface Success<T> {
  isSuccess: true;
  value: T;
}

interface Failure<E> {
  isSuccess: false;
  error: E;
}

// ファクトリ関数(ヘルパー関数)があると便利です
function success<T, E>(value: T): Result<T, E> {
  return { isSuccess: true, value: value } as Success<T>;
}

function failure<T, E>(error: E): Result<T, E> {
  return { isSuccess: false, error: error } as Failure<E>;
}

このResult<T, E>型は、「成功時には型Tの値を含むか、失敗時には型Eのエラー情報を含む」という関数の意図を、戻り値の型そのもので明確に表現します。呼び出し側は、このResultオブジェクトを受け取った際に、必ず成功か失敗かを判定し、それぞれのケースに応じた処理を記述する必要があります。これにより、エラー処理の漏れを防ぎ、コードの意図(この関数は失敗する可能性がある)を強制的に呼び出し側に伝達できます。

Before/After:結果オブジェクトパターンを適用したコード

先ほどのfindUserById関数に結果オブジェクトパターンを適用してみましょう。

// After: 結果オブジェクトパターンを適用
class UserNotFoundException extends Error {} // エラー情報として使用

type FindUserResult = Result<User, UserNotFoundException>; // ユーザーが見つかるか、見つからないエラー

function findUserByIdResult(id: string): FindUserResult {
  if (id === "unknown") {
    // 失敗の場合、failureファクトリ関数で Failure オブジェクトを返す
    return failure(new UserNotFoundException(`User with id ${id} not found.`));
  }
  // 成功の場合、successファクトリ関数で Success オブジェクトを返す
  return success({ id: id, name: "Test User" });
}

// 呼び出し側
const result = findUserByIdResult("some-id");

// Result オブジェクトのメソッドやプロパティを使って、成功/失敗をチェックし、値やエラーを取り出す
if (result.isSuccess) {
  // 成功した場合の処理
  const user: User = result.value; // 成功時は value プロパティにアクセス
  console.log(`User found: ${user.name}`);
} else {
  // 失敗した場合の処理
  const error: UserNotFoundException = result.error; // 失敗時は error プロパティにアクセス
  console.error(`Error finding user: ${error.message}`);
}

// エラーケースの呼び出し例
const errorResult = findUserByIdResult("unknown");
if (errorResult.isSuccess) {
    console.log(`Unexpectedly found user: ${errorResult.value.name}`);
} else {
    console.error(`Correctly handled error: ${errorResult.error.message}`);
}

このAfterのコードでは、findUserByIdResult関数の戻り値の型がFindUserResult、すなわちResult<User, UserNotFoundException>であることが明確です。これにより、呼び出し側はコードの型情報を見ただけで、「この関数はUserオブジェクトを返すか、UserNotFoundExceptionを含むエラーを返す可能性がある」という意図を理解できます。

呼び出し側では、result.isSuccessのようなプロパティや、map, flatMap, orElseといったヘルパーメソッド(実際のResult型実装ではしばしば提供されます)を使って、成功時と失敗時の処理を明示的に記述する必要があります。これにより、エラー処理の漏れが構造的に防がれます。

結果オブジェクトパターンがコードの意図伝達にもたらす効果

結果オブジェクトパターンを採用することで、以下の点でコードの意図伝達が改善されます。

このパターンは、特に「失敗がビジネスロジックの一部として頻繁に発生しうる」ような場面(例: ユーザー入力の検証、外部API呼び出し、ファイル操作、DBアクセスなど)で有効です。

パターン導入における考慮事項

結果オブジェクトパターンは強力ですが、全ての場面で例外を完全に置き換えるものではありません。

まとめ

コードは、単に処理を実行するだけでなく、その背後にある開発者の意図を伝える重要な手段です。特に、関数やメソッドの実行結果に成功と失敗の両方の可能性がある場合、その「結果」をどのように表現するかは、コードの可読性、保守性、そして正しさそのものに大きく影響します。

結果オブジェクトパターンは、関数の戻り値の型を通じて、発生しうる全ての可能性(成功時の値と失敗時のエラー情報)を明示的に表現することで、コードの意図を強力に伝達します。これにより、呼び出し側はエラー処理の責任を意識せざるを得なくなり、処理の漏れを防ぎ、より予測可能で堅牢なコードを書くことができるようになります。

あなたのコードでエラーや「失敗の可能性のある結果」をどのように扱っているか、ぜひ一度立ち止まって考えてみてください。結果オブジェクトパターンが、コードにさらなる「意味」を与えるための有効な選択肢となるかもしれません。