Clean Code#7エラー処理


フィードバック、エラーへようこそ
ほとんどのコードベースは、エラー処理コードに完全に依存します.ここで左右の言い方は,コードベースがエラーのみを処理するという意味ではない.これは,あちこちに散らばったエラー処理コードのため,実際のコードが行っていることを把握することはほとんど不可能であることを意味する.
この章では、クリーンで堅牢なコードへの一歩として、エラーを優雅に処理するテクニックと注意事項を紹介します.
エラー・コードではなく例外の使用
少し前までは例外をサポートしていないプログラミング言語がたくさんありました.この場合、エラーフラグを設定したり、エラーコードを呼び出し元に返したりする方法がすべてです.
しかし,このようにエラーコードを返すと,呼び出し者コードは複雑になる.関数を呼び出すと、すぐにエラーをチェックする必要があります.したがって,エラーが発生した場合は例外を投げ出す.その側の論理はエラー処理コードと混ざっていない.
Try-Catch-Filly文から
いくつかの態様では、try blockはトランザクションと類似している.tryブロックに何が起こってもcatchブロックはプログラム状態の一貫性を保たなければならない.
public List < RecordedGrip > retrieveSection(String sectionName) {
     try {
         FileInputStream stream = new FileInputStream(sectionName)
     } catch (Exception e) {
         throw new StorageException("retrieval error", e);
     }
     return new ArrayList < RecordedGrip > ();
 }
この時点で再構築が可能です.catchブロックで例外タイプを縮小することで、実際にFileInputStream作成者が投げ出したFileNotFountExcentionを見つけることができます.
public List<RecordedGrip> retrieveSection(String sectionName) {
    try {
        FileInputStream stream = new FileInputStream(sectionName);
        stream.close();
    } catch (FileNotFoundException e) {
        throw new StorageException("retrieval error”, e);
    }
    return new ArrayList<RecordedGrip>();
}
try-catch構造を使用して範囲を定義するため、TDDを使用して必要な残りの論理を追加します.残りのロジックは、FileInputStreamを生成するコードとclose呼び出し文の間に配置され、エラーや例外は発生しないと仮定します.
まず、強制的に異常を発生させるテスト例を作成し、テストによってコードを記述することをお勧めします.
これにより、tryブロックのトランザクション範囲から自然に実装され、トランザクションの本質を範囲内で維持しやすくなります.
未確認の例外の使用
大規模なシステム呼び出しの方法を想像してみてください.トップレベル関数は、次の関数を呼び出します.次の関数は、次の関数を呼び出します.レベルが下がるにつれて、呼び出される関数が増加します.次に,最下位関数を変更することによって新しいエラーを投げ出すと仮定する.その結果、最下級から最高級にかけて連鎖的な修正が発生します!
さらに,すべての関数は最下層関数で投げ出された例外を知らなければならず,カプセル化が破られる.
確認された例外も役に立つ場合があります.重要なライブラリを作成する場合は、すべての例外を除外する必要があります.しかし、通常のアプリケーション依存性のコストは収益よりも大きい.
例外に意味を与える
例外を出すときは、前後の状況を十分に説明します.
これにより、エラーが発生した原因と位置が容易に見つかります.Javaは、すべての例外に対して呼び出しスタックを提供します.しかし,失敗コードの意図を理解するには,スタックを呼び出すだけでは不十分である.
エラー情報には情報が含まれ、例外とともに投げ出されます.失敗した演算名と失敗タイプも言及した.
呼び出し元の例外クラスの定義
次は誤りをひどく分類した事例です.
ACMEPort port = new ACMEPort(12);
try {
    port.open();
} catch (DeviceResponseException e) {
    reportPortError(e);
    logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
    reportPortError(e);
    logger.log("Unlock exception", e);
} catch (GMXError e) {
    reportPortError(e);
    logger.log("Device response exception");
} finally {
	...
}
ほとんどの場合、私たちはエラーを処理する方法が固定されています.
1.記録エラー.
2.プログラムの実行を続行できるかどうかを確認します.
上記の場合、例外に対処する方法はほとんど例外タイプとは無関係である.コードは簡潔に修正されやすいです.
LocalPort port = new LocalPort(12);
try {
    port.open();
} catch (PortDeviceFailure e) {
    reportError(e);
    logger.log(e.getMessage(), e);
} finally {
	...
}
ここで、LocalPortクラスは、ACMEPortクラスが投げ出す例外のWrapperクラスをキャプチャして変換するだけである.
public class LocalPort {
    private ACMEPort innerPort;
    public LocalPort(int portNumber) {
        innerPort = new ACMEPort(portNumber);
    }
    public void open() {
        try {
            innerPort.open();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e);
        } catch (GMXError e) {
            throw new PortDeviceFailure(e);
        }
    }
    ...
}
LocalPortのようにACMEPortを囲むクラスはとても役に立ちます.実際,外部APIを用いる場合,包囲技術を用いることが望ましい.外部APIを囲むと、外部ライブラリとプログラム間の依存性が大幅に減少します.
通常、1つの例外クラスだけで十分なコードがあります.例外クラスに含まれる情報を使用してエラーを区別できる場合は、このようにします.1つの例外を除外して別の例外を無視できる場合は、複数の例外クラスが使用されます.
通常フローの定義
次の例を示します.これは、課金アプリケーションで総勘定を計算する粗末なコードです.
try [
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
    m_total += getMealPerDiem();
}
上記のコードでは、例外は論理に従うことを困難にします.特別な状況を処理する必要がなければ、もっといいのではないでしょうか.次の内容を見てみましょう.
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
上のような簡潔なコードは使えますか?
かもしれません.ExpenseReportDAOを修復し、常にMealExpenceオブジェクトに戻ります.
これを特殊事例パターンと呼ぶ.これは、クラスまたは操作オブジェクトを作成することによって特殊な事例を処理する方法です.これにより、クライアントコードは例外を処理する必要がなくなります.
nullを返さないで
nullを返すコードは、作業を増やすだけでなく、問題を呼び出し元に転嫁します.null確認を漏らすと、アプリケーションが暴走する可能性があります.
多くの場合,特殊な事例対象は容易に解決できる方法である.
nullを渡さないで
nullを返す方法もよくありませんが、nullを伝える方法はもっと悪いです.これが通常のパラメータとしてnullを所望するAPIでない場合、nullのコードをメソッドで伝達することはできるだけ避けられる.
ほとんどのプログラミング言語では、呼び出し者が誤って渡したnullを正しく処理できません.
では、nullを超える政策を最初から禁止するのは合理的です.すなわち,式がnullであるとコードに問題が生じる.このような政策では、不注意やミスを犯す確率も小さくなります.
n/a.結論
きれいなコードは可読性がよく、安定性も高い.この2つは互いに衝突する目標ではない.エラー処理をプログラムロジックから分離し、独立したイベントと見なすと、丈夫できれいなコードを書くことができます.エラー処理をプログラムロジックから分離すると,独立した推定が可能となり,コードのメンテナンス性が大幅に向上する.