【Java】【DDD】会員ランクと購入額に応じた獲得ポイント率計算

やりたいこと

下表のように会員ランクと購入額に応じて、購入額に対する獲得ポイント率が変化するというビジネスルールをif文を使わずにJavaで実装する方法を考えます。

購入額獲得ポイント率
1,000円未満1%
1,000以上、10,000円未満5%
10,000円以上10%
Bronzeランク会員の獲得ポイント率
購入額獲得ポイント率
1,000円未満2%
1,000以上、10,000円未満6%
10,000円以上11%
Silverランク会員の獲得ポイント率

MapとNavigableMapを使った実装

【Java】【DDD】NavigableMapを使った支払い料金に応じた獲得ポイントの計算で紹介したように、NavigableMapを使用することで値の範囲に対する値を管理することができます。

今回は会員ランクと購入額により獲得ポイント率が決まるので、会員ランクごと獲得ポイント率のテーブルをNavigableMapで作成します。そして獲得ポイントテーブルと会員ランクの紐付けをMapで管理します。

package com.example.demo.domain.model;

import lombok.Getter;

@Getter
public enum CustomerRank {

    BRONZE("bronze"),
    SILVER("silver");

    private CustomerRank(String rank) {
        this.rank = rank;
    }

    private final String rank;
}
package com.example.demo.domain.model;

import java.math.BigDecimal;
import java.util.*;


public class PointRateTable {

    private final Map<CustomerRank, NavigableMap<BigDecimal, BigDecimal>> table;

    public PointRateTable() {
        NavigableMap<BigDecimal, BigDecimal> bronzePointTable = new TreeMap<>();
        bronzePointTable.put(new BigDecimal(1), new BigDecimal("0.01"));
        bronzePointTable.put(new BigDecimal(1000), new BigDecimal("0.05"));
        bronzePointTable.put(new BigDecimal(10000), new BigDecimal("0.1"));

        NavigableMap<BigDecimal, BigDecimal> silverPointTable = new TreeMap<>();
        silverPointTable.put(new BigDecimal(1), new BigDecimal("0.02"));
        silverPointTable.put(new BigDecimal(1000), new BigDecimal("0.06"));
        silverPointTable.put(new BigDecimal(10000), new BigDecimal("0.11"));

        table = new HashMap<>();
        table.put(CustomerRank.BRONZE, bronzePointTable);
        table.put(CustomerRank.SILVER, silverPointTable);
    }

    public PointRateTable(Map<CustomerRank, NavigableMap<BigDecimal, BigDecimal>> table) {
        this.table = table;
    }

    public BigDecimal getPointRateByCustomerRankAndPurchaseAmount(CustomerRank rank, BigDecimal purchaseAmount) {
        var pointTable = table.get(rank);
        return Optional.ofNullable(pointTable.floorEntry(purchaseAmount))
                .map(Map.Entry::getValue)
                .orElse(BigDecimal.ZERO);
    }
}

テスト

単体テストを以下のように作成し境界値テストを行いました。

package com.example.demo.domain.model;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.math.BigDecimal;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;

class PointRateTableTest {

    @ParameterizedTest
    @MethodSource("dataProvider")
    public void bronze_rank_test(CustomerRank customerRank, BigDecimal purchaseAmount, BigDecimal expected) {
        var pointTable = new PointRateTable();
        var rate1 = pointTable.getPointRateByCustomerRankAndPurchaseAmount(customerRank, purchaseAmount);
        assertThat(rate1).isEqualTo(expected);
    }

    static Stream<Arguments> dataProvider() {
        return Stream.of(
                arguments(CustomerRank.BRONZE, 1, new BigDecimal("0.01")),
                arguments(CustomerRank.BRONZE, 999, new BigDecimal("0.01")),
                arguments(CustomerRank.BRONZE, 1000, new BigDecimal("0.05")),
                arguments(CustomerRank.BRONZE, 9999, new BigDecimal("0.05")),
                arguments(CustomerRank.BRONZE, 10000, new BigDecimal("0.1")),
                arguments(CustomerRank.SILVER, 1, new BigDecimal("0.02")),
                arguments(CustomerRank.SILVER, 999, new BigDecimal("0.02")),
                arguments(CustomerRank.SILVER, 1000, new BigDecimal("0.06")),
                arguments(CustomerRank.SILVER, 9999, new BigDecimal("0.06")),
                arguments(CustomerRank.SILVER, 10000, new BigDecimal("0.11"))
        );
    }
}