システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

BigDecimalを扱う共通クラス

はじめに

今までBigDecimalを使うようなコードを書くことが無かったのですが、とうとう(?)書く機会を得ました。
開発現場で共通部品として書いたものを振り返ることを目的にしつつ、現場では必要十分として*1 諦めたところなどを充足したものを整理しようと思ってOSS*2にしたものです。

どこかに ありもののサンプルがあれば良かったのですが、それらしいものが見つからなかったので自分で作ってみました。

何が嬉しいの?

  • BigDecimalを扱う汎用クラスで数値を扱うケース(Integer、Long、BigDecimal)を透過的に扱えます
  • 四則演算の組み合わせを持たせられ(加算だけのクラスが作れる)

実装と説明

基底の型

Integer、Long、BigDecimal いずれの数値型からもinstanceを生成してアクセスできるようにしています。

public interface CalculatorBase {

    /**
     * 計算に使用するプロパティ値を返却します.
     *
     * @return 計算に使用するプロパティ値
     */
    default BigDecimal getCalcValue() {
        return this.toBigDecimal();
    }

    /**
     * プロパティ数値を返却します.
     *
     * @return プロパティで保持している値
     */
    BigDecimal toBigDecimal();

    /**
     * プロパティ数値を返却します.
     *
     * @return プロパティで保持している値(桁落ちした場合は、桁落ちした値)
     */
    default Long toLong() {
        return this.toBigDecimal().longValue();
    }

    /**
     * プロパティ数値を返却します.
     *
     * @return プロパティで保持している値(桁落ちした場合は、桁落ちした値)
     */
    default Integer toInt() {
        return this.toBigDecimal().intValue();
    }

加算

加算は異なる型による加算はNGとして引数と戻り値に対して型縛りをしています。
加算に限ってはリストを一括で加算したいケースがありそうなので、インターフェースにもそれを入れました。

public interface Plus<T> extends CalculatorBase {

    /**
     * 加算したインスタンスを返却します.
     *
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    @SuppressWarnings("unchecked")
    public T plus(T... other);

    /**
     * 複数要素を加算します.
     * <br>
     * 加算は複数情報を一括で処理したいケースがあるため、デフォルトでも準備しています.
     *
     * @param others 計算するインスタンスリスト
     * @throws RuntimeException 引数インスタンスからクラス情報が取得出来ない場合
     * @return 計算後のインスタンス
     */
    public T plusAll(List<T> others);

}

減算

減算も加算と同じく、異なる型による加算はNGとして引数と戻り値に対して型縛りをしています。
可変長引数により複数要素を一括で減算するようにはしていますが、リストで一括は あまり無さそうなので外しました。

public interface Minus<T> extends CalculatorBase {

    /**
     * 減算したインスタンスを返却します.
     *
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    @SuppressWarnings("unchecked")
    public T minus(T... other);

}

乗算

乗算は自身とは異なる型を引数に指定したいことは多々あります(単位(ダース) × 数量 のような感じで)。
それに加えて、戻り値の型も自身の型、引数の型とも異なるようにしたい場合もあります(金額 =単価 × 数量)。

public interface Multiply<T extends Multiply<T>> extends CalculatorBase {

    /**
     * 乗算したインスタンスを返却します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    public <U extends Multiply<U>> T multiply(U other);

    /**
     * 乗算したインスタンスを返却します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param <R> 戻り値の型
     * @param other 計算するインスタンス
     * @param newInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
     * @return 計算後のインスタンス
     */
    public <U extends Multiply<U>, R extends CalculatorBase> R multiply(U other, Function<BigDecimal, R> newInstance);
}

除算

除算は、scale や 丸め を直接計算では使うので そういう感じに。
型については乗算と同じく。

public interface Divide<T extends Divide<T>> extends CalculatorBase {

    /**
     * インスタンスが保持しているscaleを返却します.
     *
     * @return プロパティで保持している値
     */
    Integer getScale();

    /**
     * インスタンスが保持しているRoundingModeを返却します.
     *
     * @return プロパティで保持している値
     */
    RoundingMode getRoundingMode();

    /**
     * 除算したインスタンスを返却します.
     * 小数点以下の桁数(scale)と丸め({@code RoundingMode})はインスタンスのデフォルトを使用します.<br>
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    default <U extends Divide<U>> T divide(U other) {
        return this.divide(other, this.getScale(), this.getRoundingMode());
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * 小数点以下の桁数(scale)はインスタンスのデフォルトを使用します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param other 計算するインスタンス
     * @param roundingMode 丸めモード
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    default <U extends Divide<U>> T divide(U other, RoundingMode roundingMode) {
        return this.divide(other, this.getScale(), roundingMode);
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param other 計算するインスタンス
     * @param scale 小数点以下の有効桁数
     * @param roundingMode 丸めモード
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public <U extends Divide<U>> T divide(U other, int scale, RoundingMode roundingMode);

    /**
     * 除算したインスタンスを返却します.
     * 小数点以下の桁数(scale)と丸め({@code RoundingMode})はインスタンスのデフォルトを使用します.<br>
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param <R> 戻り値の型
     * @param other 計算するインスタンス
     * @param newInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    default <U extends Divide<U>, R extends CalculatorBase> R divide(
            U other, Function<BigDecimal, R> newInstance) {
        return this.divide(other, this.getScale(), this.getRoundingMode(), newInstance);
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * 小数点以下の桁数(scale)はインスタンスのデフォルトを使用します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param <R> 戻り値の型
     * @param other 計算するインスタンス
     * @param roundingMode 丸めモード
     * @param newInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    default <U extends Divide<U>, R extends CalculatorBase> R divide(
            U other, RoundingMode roundingMode, Function<BigDecimal, R> newInstance) {
        return this.divide(other, this.getScale(), roundingMode, newInstance);
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param <R> 戻り値の型
     * @param other 計算するインスタンス
     * @param scale 小数点以下の有効桁数
     * @param roundingMode 丸めモード
     * @param newInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public <U extends Divide<U>, R extends CalculatorBase> R divide(
            U other, int scale, RoundingMode roundingMode, Function<BigDecimal, R> newInstance);
}

汎用計算機

全部の四則演算をもった汎用計算機。
これを各ドメインモデルで使用します。
もちろん、ただのクラスなので直接使っても良いです。

public class BigDecimalCalculator implements CalculatorBase {

    private final Integer DEFAULT_SCALE = 0;
    private final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.DOWN;

    private final BigDecimal value;
    private final Integer scale;
    private final RoundingMode roundingMode;

    BigDecimalCalculator(BigDecimal value, Integer scale, RoundingMode roundingMode) {
        this.value = value;
        this.scale = Objects.isNull(scale)
                ? DEFAULT_SCALE
                : scale;

        this.roundingMode = Objects.isNull(roundingMode)
                ? DEFAULT_ROUND_MODE
                : roundingMode;
    }

    public static BigDecimalCalculatorBuilder builder(BigDecimal value) {
        return new BigDecimalCalculatorBuilder(value);
    }

    public static BigDecimalCalculatorBuilder builder(Integer value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return new BigDecimalCalculatorBuilder(bigDecimal);
    }

    public static BigDecimalCalculatorBuilder builder(Long value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return new BigDecimalCalculatorBuilder(bigDecimal);
    }

    public static BigDecimalCalculatorBuilder builder(String value) {
        var bigDecimal = new BigDecimal(value);
        return new BigDecimalCalculatorBuilder(bigDecimal);
    }

    /**
     * 加算したインスタンスを返却します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    @SafeVarargs
    public final <T extends CalculatorBase> BigDecimalCalculator plus(T... other) {
        if (other.length == 1) {
            var result = this.getCalcValue().add(other[0].getCalcValue());
            return BigDecimalCalculator.builder(result).build();
        }
        BigDecimal result = Stream.of(other)
                .map(T::getCalcValue)
                .filter(Objects::nonNull)
                .reduce(this.getCalcValue(), BigDecimal::add);

        return BigDecimalCalculator.builder(result).build();
    }

    /**
     * 複数要素を加算したインスタンスを返却します.
     *
     * 加算は複数情報を一括で処理したいケースがあるため、デフォルトでも準備しています.
     *
     * @param <T> 計算対象の型
     * @param others 計算するインスタンスリスト
     * @throws RuntimeException 引数インスタンスからクラス情報が取得出来ない場合
     * @return 計算後のインスタンス
     */
    public final <T extends CalculatorBase> BigDecimalCalculator plusAll(List<T> others) {
        BigDecimal result = others.stream()
                .map(T::getCalcValue)
                .filter(Objects::nonNull)
                .reduce(this.getCalcValue(), BigDecimal::add);

        return BigDecimalCalculator.builder(result).build();
    }

    /**
     * 加算したインスタンスを返却します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    @SafeVarargs
    public final <T extends CalculatorBase> BigDecimalCalculator minus(T... other) {
        if (other.length == 1) {
            var result = this.getCalcValue().subtract(other[0].getCalcValue());
            return BigDecimalCalculator.builder(result).build();
        }
        BigDecimal result = Stream.of(other)
                .map(T::getCalcValue)
                .filter(Objects::nonNull)
                .reduce(this.getCalcValue(), BigDecimal::subtract);

        return BigDecimalCalculator.builder(result).build();
    }

    /**
     * 乗算したインスタンスを返却します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    public final <T extends CalculatorBase> BigDecimalCalculator multiply(T other) {
        var result = this.getCalcValue().multiply(other.getCalcValue());
        return BigDecimalCalculator.builder(result).build();
    }

    /**
     * 除算したインスタンスを返却します.
     * 小数点以下の桁数(scale)と丸め({@code RoundingMode})はインスタンスのデフォルトを使用します.<br>
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public final <T extends CalculatorBase> BigDecimalCalculator divide(T other) {
        return this.divide(other, this.getScale(), this.getRoundingMode());
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * 小数点以下の桁数(scale)はインスタンスのデフォルトを使用します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @param roundingMode 丸めモード
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public final <T extends CalculatorBase> BigDecimalCalculator divide(T other, RoundingMode roundingMode) {
        return this.divide(other, this.getScale(), roundingMode);
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @param scale 小数点以下の有効桁数
     * @param roundingMode 丸めモード
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public final <T extends CalculatorBase> BigDecimalCalculator divide(T other, int scale, RoundingMode roundingMode) {
        if (other.getCalcValue().compareTo(BigDecimal.ZERO) == 0) {
            return BigDecimalCalculator.builder(BigDecimal.ZERO)
                    .scale(scale)
                    .roundingMode(roundingMode)
                    .build();
        }

        var result = this.getCalcValue().divide(other.getCalcValue(), scale, roundingMode);
        return BigDecimalCalculator.builder(result)
                .scale(scale)
                .roundingMode(roundingMode)
                .build();
    }

    /**
     * インスタンスが保持しているscaleを返却します.
     *
     * @return プロパティで保持している値
     */
    public Integer getScale() {
        return this.scale;
    }

    /**
     * インスタンスが保持しているRoundingModeを返却します.
     *
     * @return プロパティで保持している値
     */
    public RoundingMode getRoundingMode() {
        return this.roundingMode;
    }

    @Override
    public BigDecimal toBigDecimal() {
        return this.value;
    }
}

scaleと 丸めの指定を任意で出来るようにしたかったので Builderパターンを使いました。 (Builderクラスは NetBeansで自動作成したものを少しアレンジ)

public class BigDecimalCalculatorBuilder {

    private BigDecimal value;
    private Integer scale;
    private RoundingMode roundingMode;

    public BigDecimalCalculatorBuilder(BigDecimal value) {
        this.value = value;
    }

    public BigDecimalCalculatorBuilder scale(Integer scale) {
        this.scale = scale;
        return this;
    }

    public BigDecimalCalculatorBuilder roundingMode(RoundingMode roundingMode) {
        this.roundingMode = roundingMode;
        return this;
    }

    public BigDecimalCalculator build() {
        return new BigDecimalCalculator(value, scale, roundingMode);
    }

}

各種ドメインオブジェクト

計算機としての役割は、インターフェース(実質 抽象クラス)に渡した上で、ドメインオブジェクトで補完するのは「表現力」。
これはView要件という考え方も勿論あるのですが、基底となる丸めや 小数点以下の有効桁は 情報として管理したいところです。
その辺りも広く含めた「表現力」です。

Price

例えば金額クラスとして Price という基本クラスを作ってみました。
仮に金額においては除算は行わないとした場合、インターフェースの implements には Divide<T>は不要ということになります

/**
 * 単価.
 *
 * @author Yamashita.Takahiro
 */
public class Price implements Plus<Price>, Minus<Price>, Multiply<Price> {

    private static final DecimalFormat decimalFormat = new DecimalFormat("#,##0");
    private final BigDecimalCalculator calculator;

    private Price(BigDecimalCalculator calculator) {
        this.calculator = calculator;
    }

    private Price(BigDecimal value) {
        this.calculator = BigDecimalCalculator.builder(value).build();
    }

    public static Price of(BigDecimal value) {
        return new Price(value);
    }

    public static Price of(Integer value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Price.of(bigDecimal);
    }

    public static Price of(Long value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Price.of(bigDecimal);
    }

    static Price from(BigDecimalCalculator calculator) {
        return new Price(calculator);
    }

    public static Price ofNonFormat(String value) {
        var bigDecimal = new BigDecimal(value);
        return Price.of(bigDecimal);
    }

    public static Price ofFormatted(String value) {
        return ofFormatted(value, decimalFormat);
    }

    public static Price ofFormatted(String value, DecimalFormat decimalFormat) {
        try {
            var number = decimalFormat.parse(value);
            var bigDecimal = new BigDecimal(number.toString());
            return Price.of(bigDecimal);
        } catch (ParseException ex) {
            throw new NumberFormatException("Price could not parse value = " + value);
        }
    }

    @Override
    @SafeVarargs
    public final Price plus(Price... other) {
        return Price.from(this.calculator.plus(other));
    }

    @Override
    public Price plusAll(List<Price> others) {
        return Price.from(this.calculator.plusAll(others));
    }

    @Override
    public Price minus(Price... other) {
        return Price.from(this.calculator.minus(other));
    }

    @Override
    public <U extends Multiply<U>> Price multiply(U other) {
        return Price.from(this.calculator.multiply(other));
    }

    @Override
    public <U extends Multiply<U>, R extends CalculatorBase> R multiply(U other, Function<BigDecimal, R> newInstance) {
        var calc = this.calculator.multiply(other);
        return newInstance.apply(calc.getCalcValue());
    }

    @Override
    public BigDecimal toBigDecimal() {
        return this.calculator.toBigDecimal();
    }

    public String toNonFormat() {
        return this.calculator.toBigDecimal().toPlainString();
    }

    public String toFormatted() {
        return this.toFormatted(decimalFormat);
    }

    public String toFormatted(DecimalFormat decimalFormat) {
        return decimalFormat.format(this.toBigDecimal());
    }

//(hashCodeとequalsは省略)

}

Quantity

量は、量×量もありますし、量÷量による割合も欲しいところなので 全部載せ。

public class Quantity implements Plus<Quantity>, Minus<Quantity>, Multiply<Quantity>, Divide<Quantity> {

    private static final DecimalFormat decimalFormat = new DecimalFormat("#,##0.00");
    private static final Integer DEFAULT_SCALE = 0;
    private static final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.HALF_UP;

    private final BigDecimalCalculator calculator;

    private Quantity(BigDecimalCalculator calculator) {
        this.calculator = calculator;
    }

    private Quantity(BigDecimal value) {
        this.calculator = BigDecimalCalculator.builder(value)
                .scale(DEFAULT_SCALE).roundingMode(DEFAULT_ROUND_MODE).build();
    }

    public static Quantity of(BigDecimal value) {
        return new Quantity(value);
    }

    public static Quantity of(Integer value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Quantity.of(bigDecimal);
    }

    public static Quantity of(Long value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Quantity.of(bigDecimal);
    }

    static Quantity from(BigDecimalCalculator calculator) {
        return new Quantity(calculator);
    }

    public static Quantity ofNonFormat(String value) {
        var bigDecimal = new BigDecimal(value);
        return Quantity.of(bigDecimal);
    }

    public static Quantity ofFormatted(String value) {
        return ofFormatted(value, decimalFormat);
    }

    public static Quantity ofFormatted(String value, DecimalFormat decimalFormat) {
        try {
            var number = decimalFormat.parse(value);
            var bigDecimal = new BigDecimal(number.toString());
            return Quantity.of(bigDecimal);
        } catch (ParseException ex) {
            throw new NumberFormatException("Quantity could not parse value = " + value);
        }
    }

    @Override
    public Quantity plus(Quantity... other) {
        return Quantity.from(this.calculator.plus(other));
    }

    @Override
    public Quantity plusAll(List<Quantity> others) {
        return Quantity.from(this.calculator.plusAll(others));
    }

    @Override
    public Quantity minus(Quantity... other) {
        return Quantity.from(this.calculator.minus(other));
    }

    @Override
    public <U extends Multiply<U>> Quantity multiply(U other) {
        return Quantity.from(this.calculator.multiply(other));
    }

    @Override
    public <U extends Multiply<U>, R extends CalculatorBase> R multiply(U other, Function<BigDecimal, R> newInstance) {
        var calc = this.calculator.multiply(other);
        return newInstance.apply(calc.getCalcValue());
    }

    @Override
    public <U extends Divide<U>> Quantity divide(U other, int scale, RoundingMode roundingMode) {
        return Quantity.from(this.calculator.divide(other, scale, roundingMode));
    }

    @Override
    public <U extends Divide<U>, R extends CalculatorBase> R divide(
            U other, int scale, RoundingMode roundingMode, Function<BigDecimal, R> newInstance) {
        var calc = Quantity.from(this.calculator.divide(other, scale, roundingMode));
        return newInstance.apply(calc.getCalcValue());
    }

    public <U extends Divide<U>> Percentage divideToPercentage(U other) {
        return this.divideToPercentage(other, this.getScale(), this.getRoundingMode());
    }

    public <U extends Divide<U>> Percentage divideToPercentage(U other, RoundingMode roundingMode) {
        return this.divideToPercentage(other, this.getScale(), roundingMode);
    }

    public <U extends Divide<U>> Percentage divideToPercentage(U other, int scale, RoundingMode roundingMode) {
        var value = this.calculator.divide(other, scale, roundingMode).getCalcValue();
        return Percentage.of(value.multiply(BigDecimal.valueOf(100)));
    }

    @Override
    public Integer getScale() {
        return this.calculator.getScale();
    }

    @Override
    public RoundingMode getRoundingMode() {
        return this.calculator.getRoundingMode();
    }

    @Override
    public BigDecimal toBigDecimal() {
        return this.calculator.toBigDecimal();
    }

    public String toNonFormat() {
        return this.calculator.toBigDecimal().toPlainString();
    }

    public String toFormatted() {
        return this.toFormatted(decimalFormat);
    }

    public String toFormatted(DecimalFormat decimalFormat) {
        return decimalFormat.format(this.toBigDecimal());
    }

//(hashCodeとequalsは省略)

}

Percentage

パーセンテージ(率)用の基本クラス。
除算は確実にありそうなので 全部載せ。
ちょっと手こずったのは率をどうやって扱うか(百分率にするか、Decimalにするか)というところ。
今回は、あえて(?)面倒くさそうな「百分率」にしてみました*3

public class Percentage implements Plus<Percentage>, Minus<Percentage>, Multiply<Percentage>, Divide<Percentage> {

    private static final DecimalFormat decimalFormat = new DecimalFormat("#0.000");

    // 小数表記で保持しているので、百分率の有効桁が3桁としたい場合、Scaleとしては2桁加算
    private static final Integer DEFAULT_SCALE = 5;
    private static final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.HALF_UP;

    // 計算しやすいように少数値で保持
    private final BigDecimalCalculator calculator;

    private Percentage(BigDecimalCalculator calculator) {
        this.calculator = calculator;
    }

    public Percentage(BigDecimal value) {
        var scale = value.scale();
        var decimal = value.divide(BigDecimal.valueOf(100), scale + 2, RoundingMode.DOWN);
        this.calculator = BigDecimalCalculator.builder(decimal)
                .scale(DEFAULT_SCALE).roundingMode(DEFAULT_ROUND_MODE).build();
    }

    /**
     * 百分率の値からインスタンスを生成します.
     *
     * @param value 値(百分率)
     * @return 生成したインスタンス
     */
    public static Percentage of(BigDecimal value) {
        return new Percentage(value);
    }

    /**
     * 百分率の値からインスタンスを生成します.
     *
     * @param value 値(百分率)
     * @return 生成したインスタンス
     */
    public static Percentage of(Integer value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Percentage.of(bigDecimal);
    }

    /**
     * 百分率の値からインスタンスを生成します.
     *
     * @param value 値(百分率)
     * @return 生成したインスタンス
     */
    public static Percentage of(Long value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Percentage.of(bigDecimal);
    }

    static Percentage from(BigDecimalCalculator calculator) {
        return new Percentage(calculator);
    }

    /**
     * 百分率表記からインスタンスを生成します.
     * <br>
     * 税率や利率のように、パーセント表記からインスタンスを生成したい場合に使用します.<br>
     * 例えば、4.75%の場合は,
     * <br> {@code Percentage.ofPercentage("4.75") } と記述します.
     *
     * @param value 百分率表記の文字列
     * @return 生成したインスタンス
     */
    public static Percentage ofPercentage(String value) {
        var bigDecimal = new BigDecimal(value);
        return Percentage.of(bigDecimal);
    }

    /**
     * 小数点表記からインスタンスを生成します.
     * <br>
     * 税率や利率を小数点で扱ったというインスタンスを生成したい場合に使用します. <br>
     * 例えば、30%の場合は,
     * <br> {@code Percentage.ofDecimal("0.3") } と記述します.
     *
     * @param value 小数点表記の文字列
     * @return 生成したインスタンス
     */
    public static Percentage ofDecimal(String value) {
        var bigDecimal = new BigDecimal(value);
        var calculator = BigDecimalCalculator.builder(bigDecimal)
                .scale(DEFAULT_SCALE).roundingMode(DEFAULT_ROUND_MODE).build();
        return new Percentage(calculator);
    }

    @Override
    public Percentage plus(Percentage... other) {
        return Percentage.from(this.calculator.plus(other));
    }

    @Override
    public Percentage plusAll(List<Percentage> others) {
        return Percentage.from(this.calculator.plusAll(others));
    }

    @Override
    public Percentage minus(Percentage... other) {
        return Percentage.from(this.calculator.minus(other));
    }

    @Override
    public <U extends Multiply<U>> Percentage multiply(U other) {
        return Percentage.from(this.calculator.multiply(other));
    }

    @Override
    public <U extends Multiply<U>, R extends CalculatorBase> R multiply(U other, Function<BigDecimal, R> newInstance) {
        var calc = this.calculator.multiply(other);
        return newInstance.apply(calc.getCalcValue());
    }

    @Override
    public <U extends Divide<U>> Percentage divide(U other, int scale, RoundingMode roundingMode) {
        var calc = this.calculator.divide(other, scale, roundingMode);
        return Percentage.from(calc);
    }

    @Override
    public <U extends Divide<U>, R extends CalculatorBase> R divide(U other, int scale, RoundingMode roundingMode, Function<BigDecimal, R> newInstance) {
        var calc = this.calculator.divide(other, scale, roundingMode);
        return newInstance.apply(calc.getCalcValue());
    }

    @Override
    public Integer getScale() {
        return this.calculator.getScale();
    }

    @Override
    public RoundingMode getRoundingMode() {
        return this.calculator.getRoundingMode();
    }

    @Override
    public BigDecimal getCalcValue() {
        return this.calculator.getCalcValue();
    }

    @Override
    public BigDecimal toBigDecimal() {
        return this.calculator.getCalcValue()
                .multiply(BigDecimal.valueOf(100));
    }

    public String toDecimal() {
        BigDecimal result = this.getCalcValue();
        return result.toPlainString();
    }

    public String toNonFormat() {
        return this.toBigDecimal().toPlainString();
    }

    public String toFormatted() {
        return this.toFormatted(decimalFormat);
    }

    public String toFormatted(DecimalFormat decimalFormat) {
        return decimalFormat.format(this.toBigDecimal());
    }

//(hashCodeとequalsは省略)
}

計算後の戻り値の型

戻り値の型は、計算元(左)の具象クラスの型による限定ができます。

つまり

量 = 量 × 率

こんな感じで戻り値の型を限定することをしたかったんです。
算数と数学の違いみたいな感じで焼け野原が出来そうな気もしますが、ここで「数値」を返却しないことで型への詰め替えを是正できます。

また、乗算と除算は戻り値の型を任意に変えたいことがあるので、そういったことにも対応できるようにしています。

それでもカバーできない場合は、実装クラス側に専用のメソッドを準備するという感じで実装していけば良いと思います。

以下は、実装の参考までにユニットテスト抜粋

@Test
public void testPlus1() {
    var quantity1 = Quantity.of(100);
    var quantity2 = Quantity.of(200);
    var actual = quantity1.plus(quantity2);
    assertEquals(300, actual.toInt());

    // 型の異なるモデルは加算不可(コンパイルエラーになる)
    // quantity1.plus(Price.of(2));
}
@Test
public void testDivideToPercentage() {
    var quantity1 = Quantity.ofNonFormat("10");
    var quantity2 = Quantity.ofNonFormat("2");

    // 内部的な数値をラップしただけなので、こうなってしまう。
    // 尺度の違うモデルを扱う場合は、個別にメソッドを準備して対応する方が良い
    var actual1 = quantity1.divide(quantity2, Percentage::of);
    assertEquals(Percentage.of(5), actual1);

    // 戻り値の型 Percentage のためのメソッドを準備
    var actual2 = quantity1.divideToPercentage(quantity2);
    assertEquals(Percentage.of(500), actual2);
}

GitHub(コード全量)

GitHub - vermeerlab/domains at bigdecimal

さいごに

初めはインターフェースのdefaultに実装をして継承をベースにしたのですが、乗算と除算のところで躓きました。
戻り値の型を状況によって変えたいことを鑑みて、継承より委譲の方が見通しが良いだろうという判断に至りました*4

*1:という形の時間切れ

*2:MITライセンス

*3:案の定、面倒でしたが良い素振りにはなったように思います

*4:加算と減算は良かったので、そこだけ残しても良かったのですが 継承と委譲の混在は避けることにしました