ようこそここは俺のチラシの裏だ。

専門学校卒のぽんこつえんじにあが個人事業主になって書いているただの日記。

Javaに於ける例外実装のベストプラクティス

(*'▽') を、教えてください。

f:id:sugaryo1224:20171004174500j:plain

軽く自己紹介

元々はC#屋さんです

ぼくはもともとC#出身(実務経験では2.x~4.5まで)です。

割と長い事C#/Windowsと言う環境で開発やってて、転職を機にJava/Webと言う環境に転向しました。

Javaは嫌いです

開発言語としてのJavaはハッキリ言って嫌いです。

C#やKotolinが素晴らしいと思います。

ただ、Java自体が開発言語的にイケてなかったが故に、周りに素晴らしいライブラリが多数生まれており、捨て難い資産とそれによる開発生産性があると言うのがJavaを選択する大きな理由になってるのかな、と思っています。

何と言うか「ダメな親を見て育った優秀な子供が沢山いる」というイメージ。

ロジックを書くならC#、製品開発するならJava(のライブラリ)

一言で言うとこんなイメージ。

或いは、こんな比喩ですかね。

C#

C#はスマートで、頭の中のロジックをほぼそのままストンとコードに落とせる。 芸術と言ってもいい表現力の高さが魅力な、文字通りシャープな言語。

Java

Javaは完全後方互換性が良くも悪くも特徴的で、Beansル-ルと言うローカルルールを前提に育ったので、贅肉だらけのダルダルなコードになりがち。 ただ、それを前提とした便利なライブラリが多いので、コードの贅肉を必要経費として支払えば利用出来るものが増える。

C#モデル体型Javaおでぶちゃん

そんなイメージ。

言語の好き嫌いの話はこれくらいにして

好き嫌いの話は宗教論争に発展するし、何より生産性が無いのでこれくらいにしておきましょう。

と言う事で、ここからが本題。

Javaに於ける例外について

Java言語の例外の種類

Javaに於ける例外は「検査例外」と「ランタイム例外」の大きく2種類に分類されます。

※例外以外にも、throw出来るものと言う意味ならThrowableなクラス(Errorとかね)がありますが、普通使用しないのでここでは例外だけを話題にします。

検査例外はException派生、ランタイム例外はRuntimeException派生の例外クラスを使用します。

で、ぼくはこの「検査例外」と言う言語仕様が大嫌いです。

検査例外ってうざったいだけで利点が無いと思う

  • 検査例外を発生させるメソッドはthrowsキーワードで宣言が必要になる。
  • 検査例外を宣言しているメソッド呼び出しには必ずtry-catchでエラートラップが必要である。
  • もしエラートラップしない場合はthrows宣言を引き継ぐ必要がある。
  • throws宣言シグネチャの関係が曖昧で良く解らん。

ぼくはこの辺の「検査例外特有の仕様」が大嫌いです。

以下に、ぼくが検査例外に関して気に食わないと思っている点を幾つか挙げてみます。

Javaの検査例外が気に食わない幾つかの理由

throws宣言の伝搬が気に食わない。

百歩譲って、自分自身の実装内容に基づくthrows宣言に関しては、まぁまぁ、我慢するとしよう。

でも、自分自身がスロー*1する訳でもないのにthrows宣言を要求されるのが気に食わない。

自分自身はその例外を直接扱う訳でもなく、下位で発生する例外をスルーして上位に例外処理を委譲したいだけなのに、いちいちthrows節発生する可能性のある検査例外を全列挙するのはバカバカしい

呼び出している下位メソッドに依存し過ぎなのが気に食わない。

つまりこれが全て。

呼び出し先のメソッドのthrows宣言が変わった(仕様変更があったり、何かしらリファクタリングして変更された場合)事でthrows宣言が変わると、そのメソッドを呼び出している全ての処理ルートが影響を受ける事になる。

この一事がとにかくバカバカしい。

しかも、影響を受けるつってもthrows宣言部分若しくはcatchブロックであり、本処理は全く手付かずと言うオチが99%である。 なお、残り1%になるケースを実際に目にした事はないが、そのケースはそもそも設計から考え直した方が良いと思われる。

つまり、本来の処理の主目的に対して関係性の低い部分で依存が無駄に強過ぎる

シグネチャの一部なのかそうじゃないのか、扱いが曖昧なのが気に食わない。

Javaに於けるメソッドシグネチャ*2として認められるのは以下の要素。

  • メソッドの識別子(メソッド名)
  • 引数の型と、その並び順(仮引数名はシグネチャに含まれない)

戻り値の型が含まれないのがミソであり、戻り値違いのオーバーロードは作れないと言う事。

これはまぁ、常識ですね。

例外宣言はシグネチャに含まれない:

そして、同様にthrows宣言シグネチャには含まれない。

つまり、throws宣言違いのオーバーロードは作れないと言う事。

まぁこれは当然と言うか、throws宣言違いのオーバーロードを作りたいと言う要望自体が理解不能なので、これは良い、何の問題もない。

※個人的にはそれよりもJavaのGeneric実装、もっと正確に言えばErasureと言う仕様が気に食わないが、今回は別の話なので割愛。

シグネチャではないがオーバーライドの際は問題になる:

しかし、厳密にはシグネチャに含まれないのに、オーバーライドする際に基底クラスのthrows宣言との「整合性」を求められる*3と言う仕様があります。

この制約のせいで、共通で使用しているメソッドのthrows宣言を変えた事で、これを呼び出しているいろんな箇所でコンパイルエラーが出たので機械的に修正していくハメになったと言うトラブルがありました。 (処理内容は全く何も変えず、ひたすらthrows宣言を合わせていくだけと言うカンタンナオシゴトでした)

それ以来、この制約はマジで気に食わないです。

実際に、修正作業としてはthrows宣言をひたすら合わせて行くだけと言う、処理の実装内容に一切関係無いんだよなぁ、と思いながら脳死でやってました。

基底クラスの例外宣言により派生クラスでの実装内容が制限される:

オーバーライド時に、基底クラスが宣言していない検査例外をスローしようとしたら怒られる(まぁ、継承に於けるA is B 原則 を考えればそれ自体は妥当な話なんですが・・・)、これだとプロキシクラスを上手く作れない事もあったりして、不便な場面があるのも事実なんすよね。

オーバーライド時の例外宣言の制約はちょっとどうかと思う:

本来抽象的であり、実装内容に依存すべきでない基底クラスの宣言が、派生クラスでの実装内容に制約を与えているのはちょっとどうかな、と思う場合があります。

※例えばDBアクセス処理するヤツをプロキシで差し替えてファイルIOにするとか、そう言うケースでSQL例外を宣言している所からFileIO例外をスロー出来ない、みたいなケース。

つまり例外の宣言というのは「どちらかと言うと具体的」な内容であり、基底クラスがこれを宣言する事で、基底クラスに求められる抽象性が損なわれるのではないかと言うのが気に食わないと思う所です。

そもそも「例外の型」で処理を振り分けると言うのが中途半端で気に食わない。

第二に気に食わないのは、そもそもなんで個別にcatchブロックを書かされるのか、と言う事。

普通、例外の型に応じて何か例外処理が変わる事ありますかね?

仮に例外処理の分岐が必要だったとして(多くの場合はユーザ通知するメッセージの切り替えとかそういうのだと思いますが)、それって例外の型だけで情報として十分なんですか?

そんな事無いですよね、もっと詳細な、例えばSQL例外ならOracleの吐いてくる具体的なエラーコードが欲しかったり、例外の型じゃなくその例外オブジェクトの持っている詳細なプロパティ情報が必要な訳ですよね。

だったらthrows宣言に型情報を列挙させるって中途半端過ぎません?

ならもう集約してExceptionで検査例外全般の宣言だけで良いやってなりません?

ってかそうなったらもう「何かしらの例外が発生するかもよ」ってレベルの情報でしかなく、それって別にthrows宣言してない所だって同じ(ランタイム例外はどこでも発生する可能性があるので、例外が発生する可能性が無い場所など皆無に等しい)なんだから、結局その宣言って意味ないよね??

そもそも、個別に例外処理を実装させると言うのが気に食わない。

仮に、例外の型だけで例外処理の分岐に必要十分だったと仮定したとして。

だとしても、根本的に例外処理を(業務実装で)個別に実装させると言う事が有り得ないので、やはり気に食わない。

例外設計は個別にやる事ではない:

例外処理は、集約例外ハンドラを1箇所に定めて、そこで集中的・統合的に管理するべきだと考えます。 つまり、業務チームの実装範囲ではなく、共通チームやアーキテクトがキッチリ抑えるべき部分です。

例外処理・例外管理はフレームワークや共通部品群が管理すべき範囲であり、業務機能実装に於いては不要な贅肉でしかないです。

例外設計を共通化していないのはレガシー開発である:

たまに、開発現場でも「例外処理とかちゃんとやってますか?」と機能実装の担当者に聞いて来るボンクラがいたりしますが「は?例外処理って共通化されてないんですか、マジで言ってます?」と聞き返したくなります。 それどころか「例外処理ですか、ちゃんとトライキャッチ書いてますよ」とか答えるボンクラも居れば、その回答に満足して帰っていくキングボンクラもいたりして、ハッキリ言って正気を疑います。

そう言う所はアーキテクトがまともな仕事をしていないか、或いは下手をするとそもそもアーキテクトがいないという、とんでもないプロジェクトだったりしますね。

当然、コーディング規約何それ美味しいの状態で、下手をすると規約系文書が存在しない、なんて事はザラですね。

個別に例外処理を実装させると、中には例外を不正に握り潰す馬鹿が出て来るので気に食わない。

これはもう言語仕様とは関係なく、低レベルなエンジニアの、つまり「人の問題」であるので、言い掛かりに近い難癖だというのは重々理解していますが。

検査例外のせいでエラートラップないしは例外宣言が必須化されているせいで、中にはコンパイルエラーを回避する為だけに例外を握り潰すと言う危険な行為に走る大バカ者が出て来る事もあります。

結論:検査例外の使用は大反対。

デメリットは多く挙げられるが、メリットが見当たらない。

なんか、後半はJavaが悪い訳じゃなく、悪い開発現場とエンジニアがいる、って話になってしまいましたが。

何れにせよ、検査例外ってデメリットしかなく、これと言ってメリットが存在しないよなぁ、と言うのがぼくの考えです。

少なくとも、「あー、検査例外があったおかげで助かったぁ!!」と言う事例は全くございません。

もしなんかそういう事例があったら是非教えてください。

検査例外がうざったすぎるので

Eclipseの移譲メソッド生成機能でメソッドを作り、throws宣言を消して発生例外を全てRuntimeExceptionでラップしてスローし直すだけ、と言うQuietクラスを作る事すら良くあります。

無関係なコードでロジックが汚されまくるあたり、贅肉ダルダルなイメージのいかにもJavaらしいコードになって本当に嫌いです。

あと、たまにライブラリの実装メソッドで並列関係にあるメソッドのthrowsレベルが合ってなくてコード対象性が低いのを見たりすると、もうイライラが有頂天ホテルになりますよね。

ぼくがとある開発PJで例外設計した際のルール変更

ぼくが過去、後発参加で共通チームに入り、開発規約まわりを整理した時の事。

開発のルールでは「システム固有の例外クラスを定義し、業務が例外をスローする場合は必ずこれを使う事」となっていました。

共通例外クラスの変更:

目的としては、ユーザ通知する例外メッセージ及び例外処理の一元化・共通化という事でしたが、こいつがextends Exceptionつまり検査例外になっていました。

ので、これをextends RuntimeExceptionのランタイム例外に変更し、throws宣言を整理(と言うか削除)していきました。

コーディング規約の変更:

そして、原則としてthrows宣言を禁止(と言う程強くはないですが、非推奨扱い)にし、また可能な限りtry-catchの個別実装は控えるように徹底しました。

と言うか、基本的にtryを使用するのはtry-with-resources構文のみとし、業務実装でcatchブロックが出て来た場合は基本的にNGとしました。

例外に対する例外規定:

どうしても例外を制御手段として使いたい場合があれば、まぁまずはその妥当性を検討して実装方法の見直しを第一に行いますが。

その上でなお例外を使用するしかないとなった場合、その処理構造を共通仕様として昇格して部品提供するので、業務実装の中には個別で残すな、と言う事で徹底しました。
(例えば実際に発生した要件としては、処理の規定回数のリトライ構造とかがありました)

例外処理を個別実装するなど論外、理由なくやったら説教部屋行きだ!!

くらい強く言いました。

まぁ実際には説教部屋なんて部屋は無かったですけど。

一応、それなりに強く言う必要のある立場(気付いたら共通チームのリーダー、実質的なアーキテクト的な立場。なんちゃってレベルですけどね。)だったので、内心では「本当にこれがベストプラクティスなのかなぁ?」と迷いがありましたが、 方向性を決めてそれを周知徹底する立場の人間がブレていては話になりませんので、ここはもう自分の考えは間違ってはいないと信じてやりました。

他にも、SVNでのソース管理に関しても色々とルール決め何かをやりましたが、その多くは似たように「ベストプラクティスと言えるか本当は自信がない」ものもありました。

ちなみに、それに対する解としてGitが登場したので、その辺の話もまた今度したいなぁと思います。

と言う事で、冒頭に戻りますが。

Javaに於ける例外実装のベストプラクティス、を、教えて下さい。

に繋がります。

まぁ、「Javaに於ける」って言う枕詞もぶっちゃけ不要ですね。

ただまぁ検査例外と言う独特な言語仕様が話に大きく関わっていたので「Java」と題しましたが、実際もっと広い話題になりました。

Qiitaとかに載せるほど明確な指針や結論が持てている訳では無いので、ブログに思う所を書き殴ってみた、と言うのが実態です。

今でも強い自信や、明確な見解がある訳では無いですが、それでもやっぱり検査例外と言う仕様は今でも「Javaの気に食わない言語仕様」のトップ5くらいにはランクインしています。

いやいや、そう思うのはお前がJavaの例外をきちんと理解してないからだ、とか。

俺も検査例外は嫌いだし全部Exceptionに纏めちゃうかRuntimeExceptionでラップしてブン投げてるわ、とか。

他の人の思想を聞いてみたい所。

*1:いわゆる再スローの事ではなく、自分が新規例外を発生させると言う意味の「スロー」ね。

*2:メソッドシグネチャは簡単に言うとメソッドを一意識別するためのもの。シグネチャが衝突する場合、コンパイラ的に見て同一のメソッドとして扱われると言う事。シグネチャが一致するメソッドを同一のクラスに含める事は出来ない。

*3:厳密に言うと、基底クラスの例外宣言よりも多くの例外宣言が認められていない、と言う事。より少なくする分には構わない。