みなさん、エラー処理を正しく書いてますか?
趣味で書いているコードは別として、仕事でプロダクト用に書いたコードでは、エラー処理が非常に重要な意味を持っています。
エラー処理を正しく実装しないと、ソフトウェアがダウンしたり予期せぬ結果を招き、環境の破壊や情報漏えいなど、取り返しのつかない重大なインシデントに繋がる可能性もあります。
エラー処理は重要!ということで、以下の言語でエラー処理を実装してみます。
- C
- Java
- Scala
関数の定義
以下の3つの関数があります。関数は、前の関数の処理結果を引数で受け取り、処理結果を返します。
- process1
- process2
- process3
上記の関数を順番に呼び出し、最後の関数(process3)の結果を返す処理を実装します。
また、エラーとなった場合に途中で処理を中断し、エラーの内容を標準出力に出力します。
C言語での実装
まずは、if文で処理結果が正常な場合は、次の関数を呼び出し、エラーの場合は、エラーメッセージを表示するといったコードを書いてみます。
int err;
struct result *r1, *r2, *r3;
r1 = process1(err);
if(err == 0) {
r2 = process2(r1, err);
if(err == 0) {
r3 = process3(r2, err);
if(err == 0) {
printf("All processes completed.\n");
} else {
printf("process3 failed.\n");
}
} else {
printf("process2 failed.\n");
}
} else {
printf("process1 failed.\n");
}
if文のネストが深くなって非常に見ずらいコードですね。
このようなコードを書く人はいないだろと思うかもしれません。
ですが、私が関わった非常に重要なプロダクトのコードで、このようなコードを見たことがあります。
このときはCではなくRubyでしたが、リファクタリングしてやりました!
では、if文のネストをなくすように修正してみます。
int err;
struct result *r1, *r2, *r3;
do {
r1 = process1(null, &err);
if(err != 0) {
printf("process1 failed.\n");
break;
}
r2 = process2(r1, &err);
if(err != 0) {
printf("process2 failed.\n");
break;
}
r3 = process1(r2, &err);
if(err != 0) {
printf("process3 failed.\n");
break;
}
printf("All processes completed.\n");
} while(1);
多重にネストした部分が解消され、処理の流れが見やすいコードになりました。
do-whileは、最低一度は処理を行い、while内の条件が正の場合、再度ループの先頭から処理をします。
上記コードではwhileの条件は、偽にしておき、エラーが起きた場合、breakでdo-whileのブロックを抜けることで、if文のネストを削減しています。
Javaでの実装
Javaでは、例外をスローして呼び出し元で例外をキャッチする方法が提供されています。
Javaでは、スローする例外の型を以下のように定義しています。
- Exception
回復する可能性のある例外 - Error
回復不可能な例外
それでは、例外をスローする方法で実装してみます。
呼び出す関数を以下のように定義します。
public Result processn(Result r) throws HogeException;
上記関数でエラーを検出した場合は、HogeExceptionをスローします。
throw new HogeException();
関数の呼び出し元では、以下のように呼び出す関数をtry-catchで囲みます。
呼び出し先で例外がスローされた場合は、catchブロック内に処理が移動します。
try {
Result r1 = process1(null);
Result r2 = process2(r1);
Result r3 = process3(r2);
System.out.println("All processes completed.")
} catch(HogeException e) {
System.out.println(e.getMessage());
}
長い間、例外をスローする方法は、エラーを処理する方法の主流でした。
ですが、例外をスローする方法は、以下の理由により多くのバグを生んできました。
- 処理の流れがわかりにくくなる
- もともとの例外を握りつぶしてしまう
- キャッチしていない
Scalaでの実装
では、次にScalaでのエラー処理を見てみます。
ScalaでもJavaと同様に例外をスローしてキャッチすることもできます。
Javaと違うところは、try-catchが値を返すところと、catchした例外はパターンマッチによって、区別するところです。
val result = try {
val r1 = process1(null);
val r2 = process2(r1);
val r3 = process3(r2);
"All processes completed."
} catch {
case e: HogeException => e.getMessage()
}
println(result)
上記方法では、Javaと同様の問題があるので、別の方法を見てみます。
Scalaには、正常終了と異常終了を表すことができるEitherというものがあります。
Eitherは、正常な場合はRight、異常な場合はLeftになります。
Eitherを使って関数を以下のように定義します。
def processn(val: Result): Either[String, Result]
上記関数は、以下のようにflatMapでつなげていきます。
val result = process1().flatMap(r1 => process2(r1).flatMap(r2 => process3(r2)))
result match {
case Right(msg) => println(msg)
case Left(msg) => println(msg)
}
resultは、途中でエラーが発生した場合は、最後まで正常に終了した場合はRight、途中でエラーとなった場合は、Leftになります。
エラーとなった場合でも途中でエラーが握りつぶされることもありません。
ですが、上記のようなコードはとても読みやすいコードとは言えません。
そこで、for式の登場です。
先程のコードをfor式を使用して書き直します。
val result = for {
r1 <- process1()
r2 <- process2(r1)
r3 <- process3(r2)
} yield r3
result match {
case Right(msg) => println(msg)
case Left(msg) => println(msg)
}
ネストがなくなり、スッキリとしたコードになりました。
エラー処理の実装は重要ですが、とても煩わしい作業です。
ともすると、とても読みにくく、保守しにくいコードになってしまいがちです。
みなさん、Scalaを使ってエラー処理を書いてみましょう。
明日は、Advent Calendarの最終日です。
空いているようなのでどなたか記事を書きましょう。