この記事は、保守性の高いコード設計シリーズの一部です。前回の記事「【第1部】 forループのネストから脱却しよう。Stream APIと早期リターンで実現する、読みやすいコレクション処理」では、ループ処理をクリーンにする方法を学びました。
こんにちは。多くのアプリケーションでは、List<User>
やArrayList<Product>
のように、コレクションをそのまま変数として宣言し、様々なメソッドに引き渡して使います。しかし、この一見当たり前の慣習が、実はコードの凝集度を下げ、バグを生み出す原因となっていることをご存知でしょうか?
今回は、**コレクションそのものを専用のクラスでラップする「ファーストクラスコレクション」**という設計パターンを紹介します。これは、コレクションにまつわるビジネスルールをカプセル化し、コードを安全で堅牢にするための非常に強力なテクニックです。
「裸のコレクション」がもたらす問題
List<Product>
のような「裸のコレクション」を扱う場合、そのコレクションに対する操作ロジ ック(商品の追加、合計金額の計算など)は、どこに書かれるでしょうか?多くの場合、CartManager
やOrderService
といった、様々な外部のクラスに分散してしまいます。
【Before】ショッピングカートのロジックが、外部クラスに分散している(低凝集)
// カートに商品を追加するロジック
class CartManager {
void addProduct(List<Product> products, Product newProduct) {
// カートの上限チェック
if (products.size() >= 100) {
throw new RuntimeException("カートがいっぱいです。");
}
products.add(newProduct);
}
}
// カート内の全商品が購入可能かチェックするロジック
class CheckoutService {
boolean canCheckout(List<Product> products) {
// 1つでも在庫切れの商品があれば購入不可
return products.stream().allMatch(product -> product.hasStock());
}
}
この設計では、「カートの上限は100個」「在庫切れ商品は購入不可」といったショッピングカートに関する重要なビジネスルールが、CartManager
やCheckoutService
といった全く別のクラスに散らばっています。もしルールの変更が必要になった場合、関連するすべてのクラスを探し出して修正しなければならず、修正漏れのリスクが非常に高くなります。
解決策:ファーストクラスコレクションでカプセル化する
ファーストクラスコレクションとは、コレクションをフィールドとして持つクラスを作成し、そのコレクションに関連するすべてのロジックをそのクラス内に実装するパターンのことです。List<Product>
の代わりに、ShoppingCart
という新しいクラスを作ります。
【After】ShoppingCart
クラスに、データとロジックを集約する(高凝集)
class ShoppingCart {
private static final int MAX_PRODUCTS = 100;
private final List<Product> products;
// コンストラクタ
ShoppingCart() {
this.products = new ArrayList<>();
}
// 不変性を保つために、新しいインスタンスを返す設計
private ShoppingCart(List<Product> products) {
this.products = new ArrayList<>(products);
}
// 商品を追加するロジックを、クラス自身が持つ
ShoppingCart add(final Product newProduct) {
if (isFull()) {
throw new RuntimeException("カートがいっぱいです。");
}
final List<Product> added = new ArrayList<>(this.products);
added.add(newProduct);
return new ShoppingCart(added);
}
// 購入可能かチェックするロジックも、クラス自身が持つ
boolean canCheckout() {
return products.stream().allMatch(product -> product.hasStock());
}
// カートがいっぱいかどうかの判断ロジック
boolean isFull() {
return products.size() >= MAX_PRODUCTS;
}
}
ShoppingCart
クラスは、商品リスト(データ)と、それに関連するすべてのルール(ロジック)をカプセル化しています。これにより、以下のような絶大なメリットが生まれます。
- 高凝集: ショッピングカートに関する関心事が、すべて一つの場所に集まっている。
- 再利用性:
ShoppingCart
は、信頼できる一つの部品として、システムのどこからでも安全に利用できる。 - 保守性: ルールの変更は、
ShoppingCart
クラスを修正するだけで完了する。
コレクションを外部に渡す際の注意点
クラス内部のコレクションを、表示などの目的で外部に渡したい場合があります。その際、private
なリストをそのまま返してしまうと、外部から勝手に中身を変更されてしまう危険性があります。
【Bad】内部のリストをそのまま返してしまい、カプセル化が壊れる
class ShoppingCart {
private final List<Product> products;
// ...
List<Product> getProducts() {
return this.products; // 可変なリストを返している!
}
}
// 外部のコードが、カートのルールを無視して中身を操作できてしまう
ShoppingCart cart = new ShoppingCart();
List<Product> products = cart.getProducts();
products.clear(); // カートが空になってしまった!
これを防ぐには、**変更不可能なコレクション(読み取り専用のビュー)**として返すのが定石です。
【Good】変更不可能なビューを返し、カプセル化を維持する
import java.util.Collections;
class ShoppingCart {
private final List<Product> products;
// ...
List<Product> getProducts() {
// 変更しようとすると例外が発生する、読み取り専用のリストを返す
return Collections.unmodifiableList(this.products);
}
}
これにより、ShoppingCart
の内部状態は安全に保たれ、カプセル化が維持されます。
まとめ
List<T>
のような裸のコレクションをそのまま使うのは、多くの場合、低凝集の始まりです。 **コレクションとその振る舞いをカプセル化する「ファーストクラスコレクション」**の考え方を取り入れることで、あなたのコードはより安全で、意図が明確で、変更に強い構造へと進化するでしょう。