この記事は、保守性の高いコード設計シリーズの一部です。これまでの記事では、条件分岐を整理するための様々なデザインパターンを解説しました。今回は、多くの条件分岐が潜む「コレクションのループ処理」に焦点を当て、ネストを解消し、可読性を高めるための具体的なテクニックを紹介します。
こんにちは。プログラミングにおいて、配列やリストといったコレクションの要素を一つずつ処理するfor
ループは、基本中の基本です。しかし、ループ処理の中にif
文による条件分岐が加わると、コードはあっという間にネストが深くなり、読み解くのが困難な「スパゲッティコード」になりがちです。
今回は、このようなfor
ループのネストを解消し、コードの意図を明確にするための2つの強力なアプローチを学びましょう。
1. そもそも、ループを書かない。Stream APIに任せる
まず考えたいのは、「本当にこのfor
ループは手で書く必要があるのか?」ということです。多くのプログラミング言語には、コレクションを効率的に扱うための豊富な機能が備わっています。Javaでは、その代表がStream APIです。
【Before】for
ループで、特定の商品の有無を自前でチェックする
// カートに「魔法のポーション」が含まれているかチェック
boolean hasMagicPotion = false;
for (Product each : shoppingCart.getProducts()) {
if (each.name().equals("魔法のポーション")) {
hasMagicPotion = true;
break;
}
}
このコードは、「どのように(How)」探すかを逐一記述しています。しかし、私たちが本当に表現したいのは、「何がしたいか(What)」、つまり「特定の条件に一致する要素があるかどうかを知りたい」ということだけのはずです。
【After】Stream APIを使い、やりたいこと(What)を宣言的に記述する
boolean hasMagicPotion = shoppingCart.getProducts().stream()
.anyMatch(product -> product.name().equals("魔法のポーション"));
Stream APIのanyMatch
メソッドを使うことで、コードは一行になりました。for
もif
もbreak
もありません。**「商品のストリームの中に、名前が『魔法のポーション』であるものと一つでも一致(anyMatch)するか?」**という、やりたいことそのものを表現するコードになり、意図が非常に明確になりました。
2. ループ内のネストをcontinue
とbreak
で解消する
とは言え、複雑な処理では自前でループを書く必要がある場合もあります。そんな時でも、ネストを浅く保つテクニックがあります。
continue
で不要な処理をスキップする
continue
は、現在のループ処理を中断し、次の要素の処理に移るための命令です。これは、ループ内におけるガード節のように使うことができます。
【Before】if
のネストで、割引対象の商品にのみ処理を行う
for (Product product : products) {
// 在庫がある商品についてのみ処理
if (product.hasStock()) {
// さらにセール対象の商品についてのみ処理
if (product.isSaleTarget()) {
// メインの処理:割引を適用する
product.applyDiscount(0.1);
// ...
}
}
}
【After】continue
を使い、対象外のものを早期に除外する
for (Product product : products) {
// 在庫がなければ、この商品の処理はスキップ
if (!product.hasStock()) continue;
// セール対象でなければ、この商品の処理はスキップ
if (!product.isSaleTarget()) continue;
// メインの処理
product.applyDiscount(0.1);
// ...
}
if
のネストがなくなり、平坦な構造になりました。「割引対象外」という異常系(前提条件)をループの先頭で除外することで、メインの処理が明確になります。
break
でループ自体を中断する
break
は、条件が満たされた場合にループ全体を終了するための命令です。
【Before】フラグ変数とif-else
でループの終了を管理する
int totalPrice = 0;
boolean process Succeeded = true;
for (Product product : products) {
if (product.isValid()) {
// 有効な商品なら価格を加算
totalPrice += product.price();
} else {
// 一つでも無効な商品があれば、処理を中断してフラグを立てる
processSucceeded = false;
break;
}
}
【After】早期break
でシンプルに記述する
int totalPrice = 0;
for (Product product : products) {
// 無効な商品が見つかった時点で、即座にループを抜ける
if (!product.isValid()) {
totalPrice = 0; // or throw exception
break;
}
totalPrice += product.price();
}
break
を使うことで、ループを抜ける条件が明確になり、余計なフラグ変数も不要になりました。
まとめ
コレクションのループ処理は、少しの工夫で劇的に読みやすくなります。
- まずAPIの利用を検討する:
for
ループを手で書く前に、Stream APIのような高レベルな機能で実現できないか考えましょう。 - ループ内のガード節として
continue
を使う: 前提条件を満たさない要素は、ループの先頭でスキップさせましょう。 - ループの中断には
break
を使う: ループ全体を終了させる条件を明確にしましょう。
▼次の記事 次回は、
List<Product>
のようなコレクションをそのまま扱うことの危険性に焦点を当てます。コレクションとその操作ロジックを一つのクラスにカプセル化する「ファーストクラスコレクション」という強力な設計パターンを紹介します。