Java | Java 詳細・モダン文法:設計・実務視点 – defensive programming

Java Java
スポンサーリンク

defensive programming を一言でいうと

defensive programming(防御的プログラミング)は、
「相手(呼び出し側や外部システム)が“ちゃんとしている”と期待しすぎないで、
最悪のケースを想定しながらコードを書く姿勢」です。

「こんな使い方はしてこないだろう」「ここに null は来ないはず」は、
現実のシステムでは普通に裏切られます。
防御的プログラミングは、「裏切られても壊れない」「壊れてもすぐに気づける」ようにしておくための考え方です。


何から守るのか:バグ・不正な入力・外部要因

「味方のコード」もいつかは敵になる

同じチームのコードでも、将来の自分のコードでも、
仕様変更や勘違いで「想定外の値」を渡してくることがあります。

例えば、「ユーザー名は必ず 1 文字以上」と決めていたのに、
いつの間にか空文字や null が飛んでくるようになる。
あるいは、「金額は必ず 0 以上」と思っていたのに、マイナスが入ってくる。

防御的プログラミングでは、
「味方だから大丈夫」とは考えず、
「将来の誰かが間違えるかもしれない」と思ってコードを書きます。

外部システムやユーザー入力は基本「信用しない」

Web API のレスポンス、DB のデータ、ユーザー入力などは、
「きれいな値が来る」と期待しない方が安全です。

null、空文字、桁あふれ、フォーマット違反、想定外の enum 値…。
防御的プログラミングは、こうした「汚れた入力」を前提に、
「受け取った瞬間にチェックする」「おかしければすぐにエラーにする」
というスタイルを取ります。


Java での defensive programming の基本パターン

1. 入口でチェックする(早めに失敗させる)

メソッドの入口で、引数の妥当性をチェックするのは、防御的プログラミングの基本です。

public User findUser(String id) {
    if (id == null || id.isBlank()) {
        throw new IllegalArgumentException("id must not be null or blank");
    }
    // ここから先は「id は必ず非 null・非空」という前提で書ける
    ...
}
Java

こうしておくと、
「おかしな値が入ってきた瞬間に、原因が分かりやすい例外で止まる」ようになります。
逆に、チェックをサボると、
後ろの処理で NullPointerException や謎のバグとして現れ、
原因の特定が難しくなります。

「おかしいものは早く弾く」
これが防御的プログラミングのとても大事な考え方です。

2. null をそのまま信じない

Java では null がよく出てきますが、
「ここには null は来ないはず」と思っている場所ほど、
いつか null が来て落ちます。

例えば、外部 API のレスポンスをそのまま信じてこう書くと危険です。

String name = response.getName();
System.out.println(name.length()); // ここで NPE の可能性
Java

防御的に書くなら、少なくともこうします。

String name = response.getName();
if (name == null) {
    throw new IllegalStateException("name is null in response");
}
System.out.println(name.length());
Java

あるいは、「null のときは空文字にする」など、
仕様として「どう扱うか」をはっきり決めておきます。

大事なのは、「null が来たらどうするか」をコードに明示することです。
「来ないはず」は防御的ではありません。

3. 防御的コピーで「外から壊されない」ようにする

オブジェクトの内部状態を守るのも、防御的プログラミングの一部です。

例えば、次のようなクラスを考えます。

public class Team {
    private final List<String> members;

    public Team(List<String> members) {
        this.members = members;
    }

    public List<String> getMembers() {
        return members;
    }
}
Java

呼び出し側が getMembers() で取得したリストに対して addremove を呼ぶと、
Team の内部状態が外から勝手に変えられてしまいます。

防御的に書くなら、コンストラクタとゲッターでコピーや不変ビューを使います。

public class Team {
    private final List<String> members;

    public Team(List<String> members) {
        this.members = List.copyOf(members); // コピーして保持
    }

    public List<String> getMembers() {
        return List.copyOf(members); // あるいは Collections.unmodifiableList(...)
    }
}
Java

こうしておけば、
「外部から渡されたリストが後で書き換えられて Team が壊れる」
「getMembers で返したリストをいじられて内部状態が壊れる」
といった事故を防げます。


defensive programming と「うるさすぎるコード」の境界

何でもかんでも疑うと読みづらくなる

防御的プログラミングを意識し始めると、
「全部チェックしなきゃ」となって、
コードが if 文だらけになることがあります。

例えば、内部でしか呼ばれない private メソッドに対して、
呼び出し元がすでにチェックしているのに、
さらに同じチェックを重ねる、などです。

これはこれで、読みづらさや重複の原因になります。

「契約」と「防御」のバランスを取る

現実的には、レイヤーごとに「ここまでは防御する」「ここから先は契約として信頼する」という線引きをします。

例えば、
外部からの入力を受ける層(コントローラや API 入口)では、
防御的にガッツリチェックする。

その下のサービス層やドメイン層では、
「入口でチェック済み」という前提で、
前提が破られたときだけ assert や例外で即座に落とす。

こうすると、
「外からの攻撃や不正な入力には強く、
内部ではシンプルな前提で考えられる」構造になります。

防御的プログラミングは「全部疑う」ことではなく、
「疑うべき場所をちゃんと疑う」ことだと捉えてください。


実務で defensive programming が効いてくる場面

バグの「早期発見」と「原因特定のしやすさ」

防御的に書かれたコードは、
おかしな状態になった瞬間に、
意味のあるメッセージで例外を投げてくれます。

例えば、

「id must not be null」
「amount must be >= 0」

といったメッセージがログに出ていれば、
「どこで何を間違えたか」がすぐに分かります。

防御的でないコードだと、
ずっと後ろの処理で NullPointerException が出て、
スタックトレースを延々と追いかける羽目になります。

「壊れるなら、できるだけ早く、できるだけ分かりやすく壊れてほしい」
これも防御的プログラミングの大事な目的です。

仕様変更や他チームとの連携に強くなる

実務では、「仕様が変わる」「別チームが別の言語でクライアントを書く」など、
自分の想定外の使われ方をされることがよくあります。

防御的に書かれたコードは、
そうした「想定外」に対しても、
「静かにおかしな状態で動き続ける」のではなく、
「ちゃんとエラーとして表に出る」ようになっています。

これは、システム全体の信頼性を上げるうえで、とても大きな意味があります。


まとめ:defensive programming を自分の言葉で説明するなら

あなたの言葉で整理すると、こうなります。

「defensive programming は、『相手が必ず正しい値をくれる』と信じないで、
おかしな入力や使われ方を前提にコードを書く姿勢。
メソッドの入口で引数をチェックし、null や不正値を早めに弾き、
オブジェクトの内部状態が外から勝手に壊されないように防御的コピーを行う。

その結果、バグは早く・分かりやすく表に出て、
マルチスレッドや仕様変更にも強いコードになる。
ただし、何でもかんでも疑って if だらけにするのではなく、
『外部との境界では強く防御し、内部では契約を信頼する』というバランスが大事になる。」

タイトルとURLをコピーしました