Entityの集合に対するビジネスロジックをJavaのクラスで扱う
はじめに
単一のEntityに対するロジックはEntityに対して振る舞いを定義すればよいのですが、Entityのリストや集合に対する処理を書く場所に迷いますよね。そんな迷いを解決すべく調べた内容をまとめました。
よくあるプログラム
わたしはEntityのリストや集合に対する処理をServiceやControllerに書いてしまうことが多かったです。
@Controller
public class HogeController {
private final HogeService service;
HogeController(HogeService service) {
this.service = service;
}
@RequestMapping("hoge")
public String xxx(Model model) {
List<ShoppingCartItem> cartItems = new ArrayList<>();
Integer total = cartItems.stream().mapToInt(ShoppingCartItem::subtotal()).sum();
model.addAttribute("total", total);
return "hoge/hoge";
}
}
これでもいいとは思います。ただ、Controllerにその処理を書いてしまうと、同様の処理を他のControllerでも行いたいとなったときに、おそらくコードがコピペされて、ロジックに変更が加わるときにメンテナンスが大変になってしまうことが目に見えます。
集合を表すクラスを作る
そこで以下のように集合を表すクラスを作ると、ロジックを1か所に閉じ込めることができます。
public class ShoppingCartItems {
private List<ShoppingCartItem> items = new ArrayList<>();
public void addItem(ShoppingCartItem item) {
items.add(item);
}
public Integer total() {
return items.stream().mapToInt(ShoppingCartItem::subtotal()).sum();
}
}
これで合計金額を計算するメソッドを1クラスに閉じ込められました。ただ、これではまだまだ不便です。なぜなら、一つ一つのShoppingCartItemを取り出せないからです。 脳死して Getterを定義してしまってもいいですが、ここでいくつか便利なテクニックを紹介します。
Listじゃなくても拡張for文を使えるようにする
一つ一つのShoppingCartItemを取り出すときに使用するだろう構文として拡張for文があります。以下のように呼び出せたら素敵ですよね!
ShoppingCartItems shoppingCartItems = new ShoppingCartItems();
for (ShoppingCardItem item : shoppingCartItems) {
// doSomething
}
これを実現するにはIterable<T>
インターフェースを実装するのがよいです。
public class ShoppingCartItems implements Iterable<ShoppingCartItem> {
private List<ShoppingCartItem> items = new ArrayList<>();
// 省略
@Override
public Iterator<Asking> iterator() {
return items.iterator();
}
@Override
public void forEach(Consumer<? super Asking> action) {
items.forEach(action);
}
@Override
public Spliterator<Asking> spliterator() {
return items.spliterator();
}
}
Iterableインターフェースで実装すべき3つのメソッドについては、クラス内で保持しているコレクションクラスitems
フィールドのメソッドを呼び出すようにしてあげればよいです。これで拡張for文を書くことができます。
JSTLのforEachタグでも使いたいな
こんな風に書きたいな。
@Controller
public class HogeController {
@GetMapping("hoge")
public String hoge(Model model) {
model.addAttribute("shoppingCartItems", new ShoppingCartItems());
return "hoge";
}
}
<c:forEach items="${shoppingCartItems}" var="shoppingCartItem">
<%-- do something --%>
</c:forEach>
結論としてはJSTLを使わず、。JSTLのforEachタグでitems属性にできるのは、以下に該当する変数だけです。
- Collection型
- Map型
- Iterator型
- Enumeration型
- 配列
この制限がなかなか厳しいです。今回はリストを扱っているので、Collection型を実装することになると思うのですが、Collection型は不要なメソッドが多いので個人的に避けたかったです。なので今回は1回しかループを回さないという前提でIterator型を実装してみました。
※この実装はShoppingCartItemsインスタンスが生成されるときにcursorを初期化しているので、1回しかループを回せません。
まったくもっておすすめできません。
public class ShoppingCartItems implements Iterable<ShoppingCartItem>, Iterator<ShoppingCartItem> {
private List<ShoppingCartItem> items = new ArrayList<>();
// 省略
private int cursor = 0;
@Override
public boolean hasNext() {
if (cursor < items.size()) {
return true;
}
return false;
}
@Override
public ShoppingCartItem next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return items.get(cursor++); // インクリメントしないと無限ループします☆彡
}
}
これでJSTLのforEachタグのitems属性にShoppingCartItemsクラスのインスタンスを指定できるようになります。
StreamAPIを使いたいお
itemsフィールドstreamメソッドをこのShoppingCartItemsに委譲メソッドとして定義すれば使えます。
他にも必要なメソッドがあれば、このクラスに委譲メソッドを定義すればすぐに使えるようになります。
ただビジネスロジックに不要な操作を行うメソッドを定義するのはお勧めしません。
public Stream<ShoppingCartItem> stream() {
return items.stream();
}
そもそもコレクションを扱うためのクラスを作っている目的は、コレクションに対する不正な操作を防いだり、凝集度を高めるたりすることです。あまりに多くのメソッドを定義すると目的にそぐわないものになります。メソッドを定義する前に、本当に必要かどうかを確かめるようにしてください。
さいごに
集合に対する操作、特にビジネスロジックにかかわる操作をServiceクラスに書くのはありがちだと思います。集合に対するビジネスロジックを1か所に閉じ込めるための手段として、集合をフィールドにもつクラスを定義することを今後は検討してみようと思いました。