くちたと計算記

プログラミングのことを書きます

モジュールごとに分割したソースコードが読みづらいのは、プログラミング言語などの仕様に依存した問題だと感じた話

概要

インスタンスメソッドと関数のどちらが分かりやすいのか考えるうちに、いずれにせよ Java で普通に書いたら分かりづらいという結論に至りました。

Java でメソッドなどを呼び出すときコードを書くと、呼び出すメソッドで定義した仮引数の名前がコード上に表れないからです。だから、渡しているデータが呼び出したメソッドの中でどんな役割を果たすのか読み取ることが難しいです。

その状況下では、メソッドの引数を減らして引数の役割を推測しやすくすることが重要そうです。具体的には、強い関連がある引数同士を一つのオブジェクトにしたり、ビルダーパターンを使ったりする方法があると思います。

メソッド呼び出しのわかりづらさ

例 1

isAfter(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1));

例 2

LocalDate.of(2024, 1, 1).isAfter(LocalDate.of(2024, 2, 1)); 

例 1 のようなコードは、第一引数より第二引数の方が後だったら true なのか、第二引数より第一引数の方が後だったら true なのか、はっきりしないコードだと思います。例 2 は、その点を克服できているコードだと思います。

この分かりづらさを生んでいるのは、以下の 3 点だと思います。

  1. 仮引数の名前が分からない
  2. 関数の引数が複数ある
  3. 語順が自然言語(英語)と異なる

この 3 点が揃うと、呼び出し元から引数の役割を推測するのが難しくなります。

プログラミング言語の仕様由来のわかりづらさ

メソッドを呼び出すコードが分かりづらくなってしまうのは言語仕様由来の問題でもあると思います。

以下のように書くことで、 例 1 のメソッドの仕様をほとんど維持したまま、わかりやすく表現できます。(※)

isAfter(test=LocalDate.of(2024, 1, 1),  compare=LocalDate.of(2024, 2, 1)); // 名前付き引数

isAfter({ test: LocalDate.of(2024, 1, 1),  compare: LocalDate.of(2024, 2, 1) }); // TypeScript でよくありますね。

LocalDate.of(2024, 1, 1) isAfter LocalDate.of(2024, 2, 1)); // 二つの変数をもつ関数を中置記法で書いてみました。

このように、メソッドの仕様を変えずに他のプログラミング言語で採用されている記法を取り入れるだけで、分かりやすさが変わったと思います。

Java で呼び出し元でも理解できるメソッドを宣言するには

二つの方向で考えました。

  1. 引数の数を減らして、仮引数の名前がなくても意味を推測できるようにする。
  2. 仮引数の名前を呼び出し元に書かせる。

すべての場合で問題を解決できるわけではありませんが、私は以下のようなコードを書くことで改善できるものもありそうだなと思いました。

インスタンスメソッドを使う

設計の観点で無理がないのであれば、関数ではなくインスタンスメソッドを選択しても良さそうです。関数の引数だったものをインスタンス変数に移動すれば、引数の数が減ります。また、語順の点でも分かりやすくなる可能性があります。

ただし、不変性などの制約を課さなかった場合に、テストのしやすさやバグの生みやすさの観点で、デメリットが大きくなるかもしれません。

強く関連するメソッドの引数同士は一つの引数としてまとめる

これを

public search(LocalDateTime registeredAtStart, LocalDateTime registeredAtEnd) {}

こうする。

record DatetimeSpan(LocalDate start, LocalDate end) {
  public DatetimeSpan {
  // validation
  }
}
public search(DatetimeSpan registeredAt) {}

このように関連する引数を一つの値としてみなすことで、メソッドの引数を減らせます。 また、引数間のバリデーションを DatetimeSpan 型に実装することで、あちこちに同じロジックを重複して実装せずに済みます。

ビルダーパターンを使用する

これは オブジェクトを生成するときに利用できる方法です。

Effective Java で、オプショナルな引数を多数含むコンストラクタを扱いやすくするための方法として、 Builder パターンが有効だとされています。 Builder のコンストラクタに必須の引数だけを指定することで、引数の数が少なくすっきりします。また、オプショナルな引数を設定するためのメソッドには仮引数の名前に相当するものが表れるので、その点でも分かりやすいと思います。

以下の記事では、必須の引数も含めた Builder パターンが紹介されているので、機会があれば試してみたいです。

Java で Scala の Type Safe Builder パターンをエミュレートする

最後に

表面上の記法によってソースコードの読みやすさが大きく変わることが分かるので、プログラミング言語の選び方の基準の一つになりそうだと思います。それはそれとして、 Java を利用すると言う制約下でもできることはあるので、もっとどんな工夫をできるか考えたいですね。

参考資料