読者です 読者をやめる 読者になる 読者になる

かとじゅんの技術日誌

技術の話をするところ

「検査例外はアジャイルやオブジェクト指向の考えに反するという事実」について一部誤解あり

追記:
id:Nagiseさんからエントリいただきました。

 というわけで、ややしつこく感じられるかもしれないけど誤りだと思うところはツッコミを入れさせてもらいます。人に恨みがあるとかそういうわけじゃなくて、説に用事があるってところをご理解いただければ幸いです。

こちらも建設的な議論をしたいと思っているので、もちろん、そのつもりです。

 中間のクラスが〜という話題は、開放閉鎖原則を破って境界面に変更を加えた場合に話であって、検査例外が開放閉鎖原則を破るわけじゃない。

なるほど。よくわかりました。
目的と手段で分離してみた場合、「開放閉鎖原則」を「検査例外」を使って破っているだけであって「検査例外」自体の存在が「開放閉鎖原則」を破っているわけでない。「開放閉鎖原則」を破るのは「非検査例外」でもできるわけで、直接の因果関係は成立しないということですね。これは、私の論じ方に問題あったようです。ここに訂正します。ご指摘ありがとうございます。

    • -

また例外ネタですかといわれそうですが、いろいろ考えを整理した。これが自分の結論だ。

最近、また検査例外は使いづらいというエントリが目につく。みんな好きだな〜w

# 検査例外という概念そのものが良くない
# Javaの検査例外の仕様、つまり検査例外の特定の実装がマズい


自分も昨年後半に検査例外についていろいろ考えを巡らせた。
チェック例外がJavaにあってC#にない理由 - じゅんいち☆かとうの技術日誌
例外の再考 - じゅんいち☆かとうの技術日誌
例外の扱いについて その2 - じゅんいち☆かとうの技術日誌
チェックされる例外とチェックされない例外について - じゅんいち☆かとうの技術日誌

検査例外の問題はいろいろあるのだが、一番クリティカルなことはこれだ。

もし、DClass#dMethodでスローする例外にFugaExceptionが追加されたら変更箇所はどうなるだろうか。AとDのクラスは当事者同士なのでよいとして、その経路上にいるBとCにまで変更が影響されてしまうのはつらいねー。
(snip)
コンパイルエラーで明確にわかるからいいよねという問題ではなく、変更箇所が多くなるのが面倒ですよね。IDEで支援されても確認が面倒。

public class AClass{
    public void aMethod(){
        BClass b = new BClass();
        try{
            b.bMethod();
        }catch(HogeException ex){
            ex.printStacktrace();
        }catch(FugaException ex){
            ex.printStacktrace();
        }
    }
}
public class BClass{
    // 自分の責務が変化していないのに変更の影響を受けた!
    public void bMethod() throws HogeException,FugaException {
        CClass c = new CClass();
        c.cMethod();
    }
}
public class CClass{
    // 自分の責務が変化していないのに変更の影響を受けた!
    public void cMethod() throws HogeException,FugaException {
        DClass d = new DClass();
        d.dMethod();
    }
}
public class DClass{
    public void dMethod() throws HogeException,FugaException {
        if ( isFailA() ){
            throw new HogeException();
        }else if ( isFailB() ){
            throw new FugaException();
        }
    }
}

上記のソースコードをみると、メソッドシグニチャのthrowsが変更されることによって、クライアントのコード(具体的には呼び出し経路のコード)を確実に破壊されることがわかるだろう。これは避けようがない。
システムの開発中でもリリース後でも、要件は変化する。アジャイルでも常に変化を包容していくのが、ベストプラクティスだ。だが、要件が変化して例外仕様を変更するときに、クライアントのコードが無用にも破壊されるのだ。例外の抽象化を考えずに、検査例外を利用してしまうと非常に変更にもろいコードになってしまう。
さらに、破壊されるメソッドはクラスに依存している。それはクラスのカプセル化を破壊していることにつながる。オブジェクト指向の原則で、オープンクローズド原則は非常に重要な原則だ。検査例外によってこの原則を破っている。


つまり、変更に強くしたいなら、検査例外は使うべきではないと思っている。publicなAPIにも向かないだろう。APIクライアントを巻き込んでしまうからだ。とりわけ、特に大規模システムになるとメソッドの呼び出し階層が深くなるため、なおさら使うべきではない。この手の大規模案件では無用なクラス変更はまず一切認められないと考えたほうがいいからだ。throwsを追加や削除するだけという簡単な問題ではない。
検査例外を使うなら、変更に弱くなる犠牲を払っても「人命」や「金銭」に関わるような一部の極めて重要な例外処理に対応する場合のみで、呼び出し階層も深くしないで限定的に利用するべきだ。privateな内部ロジックなどで使うほうがいい。何をもって「変更に弱くてもよい、極めて重要な例外処理への要求」とするかは判断基準が難しい。自分としては、前述した通り、デメリットが大きいので、検査例外はほとんど使う機会はないだろうと思っている。

実際に非検査例外を使うなら、
検査例外をキャッチしたところで、別途用意した別の非検査例外にラップしなおしてスローすればよい。

public class Hoge{
    public void doFuga(){ 
       try{
            // IOExceptionがスローされる
	} catch (IOException e) {
		throw new IORuntimeException(e);
	}
    }
}


ともあれ、検査例外は、そのまま使ってしまうと、アジャイルや、オブジェクト指向の考え方に反してしまうのだ。これは気をつけなければならない。


一方で、検査例外ではなく非検査例外を用いるとなると、そのメソッドでどんな例外がスローされるか、例外仕様の把握が難しくなるのだ。Javadocの@throwsで例外仕様を記述できるが、非検査例外をすべて列挙することは非現実的だ。非現実ということで記述されないとなると例外仕様の把握が難しくなるのだ。

Javadocとしては、自メソッドでスローされている例外だけを記述するこの方法が一番よい解決策だと考える。
呼び出し先の例外仕様は、その先のメソッドのJavadocを参照すればよい。

チェック例外 + 自分自身のクラス内でthrowするRTEは @throws に書く(案)
意外とバランスが取れてるかもなー。

JUnitのテストコードでも自メソッドでスローする例外だけをテストすればいいのではないかと考える。

public class AClass{
    public void aMethod(){
        BClass b = new BClass();
        b.bMethod();
    }
}

public class BClass{
    public void bMethod() {
        CClass c = new CClass();
        c.cMethod();
    }
}

public class CClass{
    public void cMethod() {
        DClass d = new DClass();
        d.dMethod();
    }
}

public class DClass{
    public void dMethod() {
        if ( isFailA() ){
            throw HogeException();
        }else if ( isFailB() ){
            throw FugaException();
        }
    }
}

public class AClassTest {
    @Test
    public void test_aMethod(){
        AClass a = new AClass();
        a.aMethod();
    }
}

public class BClassTest {
    @Test
    public void test_bMethod(){
        BClass b = new BClass();
        b.bMethod();
    }
}

public class CClassTest {
    @Test
    public void test_cMethod(){
        CClass c = new CClass();
        c.cMethod();
    }
}

public class DClassTest {
    @Test
    public void test_dMethod(){
        DClass d = new DClass();
        d.dMethod();
    }

    @Test
    public void test_dMethod_HogeException(){
        DClass d = new DClass();
        try{
            // HogeExceptionを発生させるための前準備
            d.dMethod();
        }catch(HogeException ex){
            return;
        }
        fail();
    }

    @Test
    public void test_dMethod_FugaException(){
        DClass d = new DClass();
        try{
            // FugaExceptionを発生させるための前準備
            d.dMethod();
        }catch(FugaException ex){
            return;
        }
        fail();
    }

}


追記:

この話も同様のことをいってるが、、、実際の稼働しているデプロイ環境で想像してみるといいよ。検査例外によって縛られた依存関係ができてしまうといってよい。怖すぎるよねー。throwsを変更すると互換性に問題がでるので既存の検査例外を使って隠蔽してしまう可能性だって出てくる。まぁ、メソッドの型付けと違ってあまりに実用的でないという話だ。

使いにくいので検査例外を使わずに、非検査例外だけを使うことが、「とても善良なJavaプログラマがやることでない」といわれそうですが、他言語からみた場合なんの罪もないことだ。非検査例外のほうがデファクトなのだ。検査例外のないC#が悪なのか、そんなことはないでしょう。

さらにいうならば、Javaの検査例外という概念が、Java 10年の歴史をもってしても、今なお 他言語や他のプラットフォームに普及していない。それは、評価されていないという事実だと思うのだ。よいものは他の言語、プラットフォームにも普及するのですよ。キャズムを超えるというのはそういうことだ。

あわせて読みたい
非検査例外に萌えるわけ - じゅんいち☆かとうの技術日誌
検査例外はアジャイルやオブジェクト指向の考えに反するという事実 - じゅんいち☆かとうの技術日誌 - 独断のコメント置場