2種類のデータを1つの表で表現したいときにポリモーフィズムを使うとスッキリ実装できた話

# 2種類のデータを1つの表で表現したいときにポリモーフィズムを使うとスッキリ実装できた話
## 概要
1つの表の中に2種類のデータを一緒くたにして表示をする画面を作ることがありました。そのときに条件分岐ではなく、インターフェースを使用したポリモーフィズムを使うとスッキリ実装することができました。実際のコードと合わせてどんなメリットがあるのかを紹介したいと思います。

## 前提
### 開発環境
+ IntelliJ
+ Java11
+ Gradle
+ SpringBoot 
+ Thymeleaf

### 仕様

1. Amazonにおける商品の購入履歴とサブスクリプションサービスの契約履歴を表示する
2. 商品の購入履歴かサブスクリプションサービスの契約履歴かにかかわらず時系列で表示したい
3. 購入日、商品名、単価、購入個数、小計を表示する
4. サブスクリプション契約については、契約開始日、サービス名、単価、契約月数、小計を表示する
5. サブスクリプション契約は、年契約すると割引が発生する

### 画面イメージ

<img width="1170" alt="スクリーンショット 2019-06-24 12.38.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241675/1bf116b7-e4b1-fc68-3ed6-fb0119c8c38e.png">

## サンプルコード2種
コードの全体像は[GitHub](https://github.com/dkurata38/amazon-payment-sample)にアップしてあります。
かいつまんで説明するので不足があるかもしれませんがご容赦ください。

### 条件分岐を使った例
極端に汚いプログラムを挙げさせていただきますが、条件分岐を使った例は以下のように実装しました。

1. 購入履歴と契約履歴からデータを取得して連想配列に格納する
2. 連想配列のキーと購入日/契約日を抽出し、IDを日付の昇順に並び替えたリストを生成する
3. IDのリスト順に、購入履歴と契約履歴を画面に表示する

```monomorphism/PurchaseController.java
@Controller
@RequestMapping("monomorphism/purchase")
public class PurchaseController {
    // ServiceのDIに関する記述は省略

    @GetMapping("list/{memberId}")
    public ModelAndView listByBadDto(@PathVariable Integer memberId) {
        List<Contract> memberContracts = purchaseHistoryQueryService.getMemberContracts(memberId);
        List<Purchase> memberPurchases = purchaseHistoryQueryService.getMemberPurchases(memberId);

        ModelAndView modelAndView = new ModelAndView("monomorphism/complex");
        // c-{id}形式のキーとContractのMap
        Map<String, Contract> contractMap = memberContracts.stream().collect(Collectors.toMap(c -> "c-" + c.id, c -> c));
        modelAndView.addObject("memberContracts", contractMap);

        // p-{id}形式のキーとPurchaseのMap
        Map<String, Purchase> purchaseMap = memberPurchases.stream().collect(Collectors.toMap(p -> "p-" + p.id, p -> p));
        modelAndView.addObject("memberPurchases", purchaseMap);

        // idと購入日付のMapを生成し、購入日順にソート
        Map<String, LocalDate> contractDateMap = contractMap.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().beginDate));
        Map<String, LocalDate> purchaseDateMap = purchaseMap.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().purchasedDate));

        Map<String, LocalDate> idMap = new HashMap<>();
        idMap.putAll(contractDateMap);
        idMap.putAll(purchaseDateMap);
        List<String> ids = idMap.entrySet().stream()
                .sorted(Comparator.comparing(Map.Entry::getValue))
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());

        modelAndView.addObject("ids", ids);
        return modelAndView;
    }
}
```

```monomorohism/complex.html
<table class="uk-table">
    <thead>
        <tr>
            <th>購入日</th>
            <th>購入商品名</th>
            <th>単価</th>
            <th>購入個数</th>
            <th>小計</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="id: ${ids}">
            <th:block th:if="${id.startsWith('c-')}" th:with="contract = ${memberContracts.get(id)}">
                <td th:text="${contract.beginDate}"></td>
                <td th:text="${contract.service.name}"></td>
                <td th:text="|${contract.service.priceByUnitMonth}円/${contract.service.unitMonths}ヶ月|"></td>
                <td th:text="|${contract.months}ヶ月|"></td>
                <td th:text="|${contract.service.priceByUnitMonth / contract.service.unitMonths * contract.months - contract.service.yearlyDiscount}円|"></td>
            </th:block>
            <th:block th:if="${id.startsWith('p-')}" th:with="purchase = ${memberPurchases.get(id)}">
                <td th:text="${purchase.purchasedDate}"></td>
                <td th:text="${purchase.commodity.name}"></td>
                <td th:text="|${purchase.commodity.unitPrice}円|"></td>
                <td th:text="|${purchase.amount}個|"></td>
                <td th:text="|${purchase.commodity.unitPrice * purchase.amount}円|"></td>
            </th:block>
        </tr>
    </tbody>
</table>
```

契約履歴と購入履歴を一覧で表示するだけなのに、ControllerもViewも複雑です。

### ポリモーフィズムを使った例
ポリモーフィズムを使った例では以下のように実装しました。

1. データベースから取得した購入データと契約データを購入履歴型のクラスに変換する
2. 購入履歴型のデータを購入日順に並び替えて画面に表示する

```polymorphism.PurchaseService.java
@Service
public class PurchaseService {
    // RepositoryのDIに関する記述は省略

    public List<PurchaseHistory> getMemberPurchaseHistory(Integer memberId) {
        // 商品購入データを取得し、購入履歴型を実装した商品購入履歴クラスに変換
        List<Purchase> purchases = purchaseRepository.findByMemberId(memberId);
        Stream<CommodityPurchaseHistory> commodityPurchaseHistoryStream = purchases.stream().map(CommodityPurchaseHistory::new);

        // サービス契約データを取得し、購入履歴型を実装したサービス購入履歴クラスに変換
        List<Contract> contracts = contractRepository.findByMemberId(memberId);
        Stream<ServicePurchaseHistory> contractHistoryStream = contracts.stream().map(ServicePurchaseHistory::new);

        // 購入履歴型のリストを結合し、購入日順に並び替え
        return Stream.concat(commodityPurchaseHistoryStream, contractHistoryStream)
                .sorted(Comparator.comparing(PurchaseHistory::purchasedDate))
                .collect(Collectors.toList());
    }
}
```

```polymorphism.PolyPurchaseController.java
@Controller
@RequestMapping("polymorphism/purchase")
public class PolyPurchaseController {
    // ServiceのDIに関する記述は省略
    @GetMapping("list/{memberId}")
    public ModelAndView getInterface(@PathVariable("memberId") Integer memberId) {
        List<PurchaseHistory> memberPurchaseHistories = purchaseService.getMemberPurchaseHistory(memberId);
        ModelAndView modelAndView = new ModelAndView("polymorphism/list");
        modelAndView.addObject("memberPurchaseHistories", memberPurchaseHistories);
        return modelAndView;
    }
}
```

viewの記述は購入履歴型のメソッドを呼び出しているだけなのでとてもスッキリしています。

```polymorphism/list.html
<table class="uk-table">
    <thead>
        <th>購入日</th>
        <th>購入商品名</th>
        <th>単価</th>
        <th>購入個数</th>
        <th>小計</th>
    </thead>
    <tbody>
        <tr th:each="purchaseHistory : ${memberPurchaseHistories}">
            <td th:text="${purchaseHistory.purchasedDate()}"></td>
            <td th:text="${purchaseHistory.purchasedCommodityName()}"></td>
            <td th:text="${purchaseHistory.unitPrice()}"></td>
            <td th:text="${purchaseHistory.amount()}"></td>
            <td th:text="${purchaseHistory.subtotal()}"></td>
        </tr>
    </tbody>
</table>
```

## さいごに
ポリモーフィズムを使うか使わないかはさておき、計算や条件分岐はサーバーサイドのプログラムに閉じ込めたほうがよいです。

+ 同じロジック（今回の例では小計を求める計算）がいろんな画面に散らばるのを防げる
+ 画面からロジック(今回の例だと小計を計算するロジック)を意識しなくて済むようになる
+ if文やfor文が無いので、協業するデザイナーさんにもわかりやすいHtmlファイルになる

特に協業するデザイナーさんはJavaのテンプレートエンジンに関する知見がないことがほとんどだと思いますから、Htmlファイルはシンプルであることに越したことがないと思うんですよね。
仲間がどんな技術を持っているのか、だからどんな役割分担をするのがベストなのかというのを意識して設計・プログラミングをすると、開発チーム全体の成功に近づくかもしれませんね。

その上でポリモーフィズムを使うと、サーバーサイドのプログラムからもif文が減って見通しの良いプログラムを書くことができますというお話でした。