Javaのオーバーロード設計に潜む認知負荷

Javaではメソッドのオーバーロード(多重定義)が簡単にできます。
同じ名前で引数が違うだけでAPIを柔軟に設計できるのは一見便利に思えますが、設計を誤るとコードの読み手に過剰な判断負荷を強いる結果になります。

本記事では、オーバーロードの構造的な設計ミスがなぜ認知負荷の増加をもたらすのかを明らかにし、
レビュー時に見逃さないための具体的な観点を整理します。

そもそもオーバーロードとは何か?

public class Logger {
    public void log(String message) { ... }
    public void log(String message, int level) { ... }
    public void log(String message, Throwable e) { ... }
}

オーバーロードは名前は同じで、シグネチャ(引数の型・数)が異なるメソッドを定義できるJavaの機能。
1つの概念的な動作(例: log)に複数の入力方法を用意する。

一見、統一されたインターフェースのようでいて、受け入れる意味や責務が異なると設計上の罠になります。

認知負荷の本質:選択肢の曖昧さ

呼び出す側から見ると、同じ名前のメソッドに対して「どれが使われるか」「何を意図しているのか」を読み解く必要があります。

logger.log("error", 3);          // level = 3 を明示
logger.log("error", new Exception());  // 例外付きログ

logger.log("error");             // これは何のログ?

呼び出しは簡単に見えて、内部で何が行われるかの判断は困難です。

オーバーロードが招く設計上の混乱
  • nullを引数に渡したとき、どのオーバーロードが選ばれるか不明瞭
  • IDEの補完で複数候補が出て、どれを使えばいいか迷う
  • 呼び出し側にとって「挙動の違いがコードから読み取れない」

よくあるオーバーロードの設計ミス

1. 機能的に異なる動作を「名前だけ同じ」にしている

public void save(User user) { ... }
public void save(List<User> users) { ... }
public void save(User user, boolean overwrite) { ... }

saveという言葉に複数の責務が混在
→ 「どのsaveが何を意味するか?」がコードから直感的に読み取れない

2. 意図が曖昧な引数の差

send(String address, String message)
send(String message)

→ 「send(String)」は誰に送る?ログ?標準出力?通知?
意味的な暗黙を含むオーバーロードは認知負荷を増やす

3. Optionalな引数をオーバーロードで表現しすぎている

print()
print(boolean newLine)
print(String content)
print(String content, boolean newLine)

→ 組み合わせ爆発
利用者にとって選択の指針が不明

正しく設計するには:メソッド名を責務で切る

オーバーロードが必要なのは形式の差異(型・個数)だけで振る舞いが等価な場合だけに限定すべきです。

設計判断の指針
  • 振る舞いに意味的な差異があるなら別メソッド名に分離
  • 引数の組み合わせが複雑化する場合はビルダーパターンなどの導入を検討
  • ユースケースベースで一貫性のあるAPI命名を意識

改善例:saveのオーバーロードを排除して明示化

public void saveUser(User user) { ... }
public void saveUsers(List<User> users) { ... }
public void saveUserWithOverwrite(User user) { ... }
  • 呼び出し側にとって明確
  • IDEの補完でも意味が見える
  • テストコードでも意図がわかりやすい

オーバーロードの意味不明瞭 vs 明示設計

UML Diagram

レビュー観点:オーバーロードの責務混在に注意

レビュー観点

オーバーロードを設計・レビューする際は次の観点を確認しましょう。

複数の選択肢があるというだけで、人は“余計な判断”を強いられます。
それが1日に何十・何百と繰り返されると、そのコードのAPIは疲れる設計になります。

結論:オーバーロードは少ない方が“親切”

  • オーバーロードは“振る舞いが等価”なときだけ使う
  • ユースケースごとに意図の異なる処理は名前を分ける
  • 呼び出し側の判断コストを意識した設計が、コードの可読性と品質を守る