JAVA異常設計原則

10362 ワード

異常は対象言語に向かって非常に重要な特性であり、良好な異常設計はプログラムの拡張性、維持性、丈夫性に重要である。
JAVAは用途によって、二つの異常を定義しています。
*[b]Checked Exception:[/b]Exceptionのサブクラス、方法の署名に表示されるステートメントthrows、コンパイラはこのような異常またはステートメントthrowsを引き続き上に投げ出すように強制します。
*[b]Uchecked Exception:[/b]RuntimeExceptionのサブクラスで、メソッド署名はthrowsを宣言する必要がなく、コンパイラもこのクラスの異常を強制的に処理することができません。
異常な作用と利点:
1.エラーコードと正常コードを分離し、コードがより簡潔である。
2.データの正確性と完全性を保護し、手順はより厳格である。
3.調整と間違いに便利で、ソフトのメンテナンスがより良いです。
……
多くのJAVA開発者が見たり聞いたりしたことがあると思いますが、この言葉は非常に記憶しやすいですが、「流れ」の定義がないので、作者の意図が分かりにくく、困惑してしまいます。
もし「流れ」がプログラムを含むステップごとに実行されているとしたら、異常はプロセスを制御するためのものだと思います。それはプログラムの正常な流れとエラーフローを区別するためのものです。今新しい問題を持ってきました。プログラムの正常な流れと異常な流れをどうやって区別しますか?本当に判定基準が思いつかないので、例を挙げて説明します。
後のほうが便利な表現のために、異常を二つに分けます。
*[b]システム異常:[/b]ソフトウェアの欠陥、クライアントはこのような異常に対して無力であり、通常はUcheced Exceptionである。
*[b]業務異常:[/b]ユーザーが通常の流れに従わないことによる異常は、すべてChecked Exceptionです。
金貨振替の例
1.需要規定金貨の一回の振替範囲は1~500です。この限度額を超えると、ユーザーに一筆振替の制限を超えた金額を提示します。振替の金額はユーザーがページに入力します。
値はユーザーが入力したものなので、与えられた値が範囲を超えているのは当たり前です。もちろん、それ(入力の値が制限の範囲を超えている)を異常プロセスにまとめることはできません。これは正常プロセスに属し、検証データの完全な機能を提供すべきです。
正しい実装は以下の通りです。
振替金貨の数量が制限範囲を超えているかどうかを判断する方法を提供します。
private static final int MAX_PRE_TRANSFER_COIN = 500;

public boolean isCoinExceedTransferLimits(int coin) {
return coin > MAX_PRE_TRANSFER_COIN;
}
アクションではまず値をチェックして、合法でない場合は、直接に戻ってユーザーに提示します。
2.振込の過程で、金貨の発行数が足りない:
私たちのプログラムは全部同時進行中です。アクションは金貨が十分かどうかを完全に判断できません。判断の後、事務前との間に、他の手数料を掛けて金貨が足りない可能性があるからです。この時は業務異常(Checked Exception)でコントロールする必要があります。
正しい実装は以下の通りです。
CoinNot Enough Excetion.java
//        
public class CoinNotEnoughExcetion extends Exception {
private static final long serialVersionUID = -7867713004171563795L;
private int coin;
public CoinNotEnoughExcetion() {
}

public CoinNotEnoughExcetion(int coin) {
this.coin = coin;
}

public int getCoin() {
return coin;
}

@Override
public String getMessage() {
return coin + " is exceed transfer limit:500";
}
}
//振替方法
private static final int MAX_PRE_TRANSFER_COIN = 500;

public void transferCoin(int coin) throws CoinNotEnoughExcetion{
if (!hasEnoughCoin())
throw new CoinNotEnoughExcetion(coin);
// do transfering coin
}
3.インターフェーストランスファーCoin(int coin)の仕様で契約されています。トランスファーCoinを呼び出す前に、isCoinExceedTrans Limitsを先に呼び出して、値が適法かどうかを判断しなければなりません。
誰もが遵守しなければならない仕様ですが、あくまでも仕様ですので、コンパイラは強制的に制約できません。この時はシステム異常(Uchecked Exception、JDKの標準異常)を使ってプログラムの正確性を保証します。ルールを守らなかったのは全部ソフトウェアバグとして処理します。
正しい実装は以下の通りです。
//振替方法
public void transferCoin(int coin){
if (coin > MAX_PRE_TRANSFER_COIN)
throw new IllegalArgumentException(coin+" is exceed tranfer limits:500");
// do transfering coin
}
ここで、例を挙げてもう終わりました。ここでもう一つ延長してください。業務異常とシステム異常の区別は、調合者が異常を捕まえた後、どうやって処理しますか?
アクションの業務異常処理:操作の具体的な異常類
public String execute() {
try {
userService.transferCoin(coin);
} catch (CoinExceedTransferLimitExcetion e) {
e.getCoin();
}
return SUCCESS;
}
アクションのシステム異常処理:具体的な異常クラスが分かりません。
public String execute() {
try {
userService.transferCoin(coin);
} catch (RuntimeException e) {
LOG.error(e.getMessage());
}
return SUCCESS;
}
運用者が業務異常を捕捉した後、通常はgetMessage()メソッドを呼び出すのではなく、異常な種類に特有の方法を呼び出すので、業務異常類の実現は、特定の、ビジネスに関する方法ではなく、getMessage()メソッドを重視する。
システム異常クラスとは逆に、TrapはgetMessage()を呼び出して異常情報を取得し、エラーログを記録するだけなので、システム異常類(Uncheck Exception)の実装類は、getMessage()方法に対して内容を返すことが大切です。
業務異常もシステム異常も豊富な情報提供が必要です。例えば:データベースアクセス異常、クエリsql文などを提供する必要があります。HTTPインターフェースの呼び出し異常は、アクセスのURLとパラメータリスト(post要求であれば、パラメータリストが提供されなくてもいいです。保守担当者または開発者がパラメータリストを入手するとどうやって使うかによって異なります。)を与える必要があります。
1.Sql文法エラー異常類(springフレームから採取した異常類、Springの異常体系が強いので、一見の価値がある):
public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException {
private String sql;

public BadSqlGrammarException(String task, String sql, SQLException ex) {
super(task + "; bad SQL grammar [" + sql + "]", ex);
this.sql = sql;
}

public SQLException getSQLException() {
return (SQLException) getCause();
}

public String getSql() {
return this.sql;
}
}
2.HTTPインターフェース呼び出し異常類
public class HttpInvokeException extends RuntimeException {
private static final long serialVersionUID = -6477873547070785173L;

public HttpInvokeException(String url, String message) {
super("http interface unavailable [" + url + "];" + message);
}
}
どのように選択しますか?
1.ソフトウェアのバグか業務の異常か、ソフトウェアのバグはUnicheced Exceptionか、そうでないとChecked Exceptionです。
2.この異常をユーザーに投げたら、ユーザーは救済できるかどうか。クライアントが何もできない場合は、Unicheced Exceptionを使用します。そうでなければChecked Exceptionを投げます。
この2つを組み合わせると、2つの異常は混同されなくなります。
[b]
異常設計のいくつかの原則:
1.もし方法が解決できない意外な状況に遭遇したら、一つの異常を投げかける。
2.異常を避ける方法としてよく使われる機能の状況を指摘する。
3.お客様が契約違反(例えば、不正入力パラメータの着信)を発見した場合、非検査型の異常をスローします。
4.方法が契約書を履けない場合、検査型の異常を抛り出しても、非検査型の異常を抛り出すことができます。
5.顧客プログラマが意識的に措置を取る必要があると思うなら、検査型の異常を投げる。
6.異常類はお客様に豊富な情報を提供し、異常類は他の種類と同じで、自分の属性と方法を定義することができます。
7.異常類名と方法はJAVA類名規範と方法名規範に従う。
8.JAVAの他の種類と同様に、余分な方法や変数を定義しないでください。(使用できない変数は定義しないでください。springのBadSql Graammarception.get Sql()は余分です。
以下は私の仕事で出会ったいくつかの書き方です。私も前にこのような間違いを犯したことがあります。
[b]A.全体の業務層には一つの異常類しか定義されていない[/b]
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 8670135969660230761L;

public ServiceException(Exception e) {
super(e);
}

public ServiceException(String message) {
super(message);
}
}
理由:
1.業務異常はUcheced Exceptionではないはずです。
2.具体的な異常クラス名が存在しないのは「ServiceException」です。
解決方法:抽象的な業務異常を定義する「ServiceException」
public abstract class ServiceException extends Exception {
private static final long serialVersionUID = -8411541817140551506L;
}
[b]B.異常[/b]を無視する。
try {
new String(source.getBytes("UTF-8"), "GBK");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
理由:
1.環境がUTF-8またはGBKに対応していないのは明らかです。非常に深刻なバグです。放置できません。
2.スタックの方式でエラー情報を記録するのは不合理で、製品環境が標準出力を記録しないと、このエラー情報が無くなります。製品環境が標準出力を記録している場合、このプログラムがwhileループのスレッドに呼び出された場合、ハードディスク容量がオーバーフローし、最終的にはプログラムの運行が正常ではなく、データが失われる恐れがあります。
解決方法:UnisportedEnccodingExceptionを捕獲し、Unicheced Exceptionにパッケージ化し、上へ投げ、プログラムの実行を中断します。
try {
new String(source.getBytes("UTF-8"), "GBK");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("the base runtime environment does not support 'UTF-8' or 'GBK'");
}
[b]C.トップレベルを捕獲する異常-Exception[/b]
public void transferCoin(int outUid, int inUserUid, int coin) throws CoinNotEnoughException {
final User outUser = userDao.getUser(outUid);
final User inUser = userDao.getUser(inUserUid);
outUser.decreaseCoin(coin);
inUser.increaseCoin(coin);
try {
// BEGIN TRANSACTION
userDao.save(outUser);
userDao.save(inUser);
// END TRANSACTION
// log transfer operate
} catch (Exception e) {
throw new ServiceException(e);
}
}
理由:
1.Serviceは業務異常だけではなく、Serviceも他の異常を投げることができます。
例えば、IllagalAgment Exception、ArayIndexOutOfBounds ExceptionまたはspringフレームのDataAcception
2.多くの場合、DaoはChecked ExceptionをServiceに投げません。すべてのコードが非常に規格化されているなら、Service類にtry{}catchコードが現れてはいけません。
解決方法:try{}catchコードを削除する
public void transferCoin(int outUid, int inUserUid, int coin) throws CoinNotEnoughException {
final User outUser = userDao.getUser(outUid);
final User inUser = userDao.getUser(inUserUid);
outUser.decreaseCoin(coin);
inUser.increaseCoin(coin);
// BEGIN TRANSACTION
userDao.save(outUser);
userDao.save(inUser);
// END TRANSACTION
// log transfer operate
}
[b]D.無意味な異常を作成する[/b]
public class DuplicateUsernameException extends Exception {
}
理由
1.「意味が明確」な名前がある以外には有益な情報がありません。Exceptionは他のJava類と同じであることを忘れずに、クライアントはその中の方法を呼び出してより多くの情報を得ることができます。
解決策:捕獲者に必要な情報を定義する
public class DuplicateUsernameException extends Exception {
private static final long serialVersionUID = -6113064394525919823L;
private String username = null;
private String[] availableNames = new String[0];

public DuplicateUsernameException(String username) {
this.username = username;
}

public DuplicateUsernameException(String username, String[] availableNames) {
this(username);
this.availableNames = availableNames;
}

public String requestedUsername() {
return this.username;
}

public String[] availableNames() {
return this.availableNames;
}
}
[b]E.ユーザに展示されている情報を直接異常情報に入れる。[/b]
public class CoinNotEnoughException2 extends Exception {
private static final long serialVersionUID = 4724424650547006411L;

public CoinNotEnoughException2(String message) {
super(message);
}
}

public void decreaseCoin(int forTransferCoin) throws CoinNotEnoughException2 {
if (this.coin < forTransferCoin)
throw new CoinNotEnoughException2(" ");
this.coin -= forTransferCoin;
}
理由:ユーザーに展示したエラーメッセージは文案の範疇に属しています。文案は変更しやすく、プログラムと混同しないほうがいいです。
解決方法:
エラーメッセージの文案は一つの構成ファイルにまとめられています。異常の種類によって、対応するエラーメッセージの情報を取得します。国際化をサポートする必要があれば、複数の言語のバージョンも提供できます。
[b]F.メソッド署名により、余分なthrows[/b]を宣言した。
理由:コードが簡単ではないので、調整者はtry{}catchコードを追加しなければならない。
解決策:もし方法がこの異常を投げ出すことができないなら、余分なthrowsを削除します。
[b]G.それぞれの異常クラスにError Code[/b]を使用しないと定義します。
理由:一つの機能があれば、一つのメンテナンスコストが多くなります。
解決方法:Error Codeを無駄に定義しないでください。本当に必要でない限り(他の人にインターフェースの呼び出しを提供する場合、異常を計画し分類したほうがいいです。1 xxは金貨関連の異常を表します。2 xxはユーザー関連の異常を表します。…
最後にいくつかの異常設計の原則について紹介します。
1.異常設計
http://www.cnblogs.com/JavaVillage/articles/384483.html(翻訳)
http://www.javaworld.com/jw-07-1998/jw-07-techniques.html(原文)
2.異常処理の最適な実践
http://tech.e800.com.cn/articles/2009/79/1247105040929_1.html(翻訳)
http://onjava.com/pub/a/onjava/2003/11/19/exceptions.html(原文)