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

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 0.1.1

さいごに

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

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

*2:MITライセンス

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

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

Swaggerのメモ

SpringFox

SpringFox by springfox

メモ

ぱっと調べると SpringでSwaggerは SpringFox くらいの 感じだったし、既存のプロジェクトでも使っていたから採用はしたものの、最新バージョン(3_0_0)にすると SpringBoot2.6系にしたら 起動が出来なかった。

swagger-starter3.0 not support springBoot2.6.x version · Issue #3934 · springfox/springfox · GitHub (他にも類似のissueがあった気もするけど見つけられなかった)

メンテナンスも止まっているみたい(最終コミットが 2020年10月)

Commits · springfox/springfox · GitHub

positon の順番が効かないとかもあるし、このまま使い続けるのは良くないかも。。

リンク

とはいえ、色々と参考になる(調べるときのヒントになる)かもしれないので気になる情報はメモしておく。

SpringFoxでのSwaggerのリクエスト・レスポンス説明の共通化 | GROUP DEV BLOG | TECHNO DIGITAL

stringdoc-openapi

OpenAPI 3 Library for spring-boot

メモ

SpringFoxよりも こっちを使うようにした方が良いかも、、 ということで まずは情報収集。 少なくとも Swagger用ライブラリで SpringBootのバージョンアップの弊害になっているのは困る。
私のプロジェクトは始まったばかり。今なら間に合う。

リンク

SpringFoxからspringdoc-openapiに移行してみた - Qiita

OpenAPI 文書のカスタマイズ

Documenting a Spring REST API Using OpenAPI 3.0 | Baeldung

@RestControllerAdvice and @ResponseStatus(HttpStatus.OK) overriding default response for @RestController methods · Issue #565 · springdoc/springdoc-openapi · GitHub

Configure default response Message · Issue #381 · springdoc/springdoc-openapi · GitHub

springdoc-openapiでOpenAPI形式のAPIドキュメントを生成する

パッケージ構成の考察(3)

はじめに

過去2回のパッケージ構成について

vermeer.hatenablog.jp

vermeer.hatenablog.jp

約3年ぶりに改めて考察してみようかと。
DDD関連に影響は受けていると思いますが別物です。
3層+ドメイン を基本とした構成です。

なぜ改めて見直すのか?

Reactなどの開発や参考になる情報も増えてきたり、サーバーサイドについてもWebAPIを作成したり、Spring関連*1の勉強もしたりするの中で、以前の整理では ちょっと良くないなと思うところが自分で読み返しても感じたため。
特に WebAPI(RESTful API)については全く考えていませんでした。

パッケージ構成図

https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/package-%20examination/img/package.drawio.svg

ブラウザで見るには小さいのでリンクからsvgファイルを取得してください。
https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/package-%20examination/img/package.drawio.svg

シーケンス

https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/package-%20examination/img/layer-sequence.drawio.svg

https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/package-%20examination/img/layer-sequence.drawio.svg

所感的な補足

baseとapps

分類として横断的機能と業務遂行の中心となるパッケージを分けました。
非機能要件と機能要件を分けるというのも類似です。
技術要素によったパッケージとして coreでも良いのですが中心というイメージではないかな、と。
また意図としてはshareも近い気がしますが、そういう横断的ではなくて箱庭の枠みたいなイメージで同列性を感じない表現が近しいイメージです。

複数のサービスを参照するものの分類は設けない。

複数のサービスの参照じゃなくなったときに命名やパッケージを変更するというのは、なんか違うかな、と。 そういう「複数のサービスを利用している」「別のサービスからも利用されている」というのは、コード分析(JIGみたいな)で取得して把握すれば必要十分だろうということで設けないことにしました。

RepositoryはDomain以外の場所に配置(Application層 or 独立させる)

ドメインオブジェクトから参照はしないから。 何か関数的な操作をRepositoryに「させる」ことはあっても、Repostoryがドメイン内の操作を「する」ことはない。 もし「ある」場合は、ドメインからIO(副作用)があることになりかねない。
逆に非常に簡易的な参照系システムの場合 Presentation層から直接 Repositoryを使用することもアリかなと思ってもいます(トランザクション境界が不要だから)。
となると、RepositoryはPresentation層から使用も出来るし、Application層からも使用できる場所に配置させるのが妥当ではないかな?という結論に至りました。

Service内でのif分岐はあり

構成図には記載していませんが、かつては、if分岐すらも無しに出来ないかな?と思ったことがあります。
ですが、UMLユースケースでも 代替フローがあるので、さすがにやりすぎだと今は思っています。

Serviceの命名は異論あるかも

個人的にはサービスは動詞がアリだと思うのですが ちょっと万人向けじゃないかもなぁと思っています。
とはいえ ユースケース記述に近い実装のやりやすさを考えると、クラス名もユースケース記述に近い「動詞」が良いんじゃないかなぁと。 ということで、ここは以前の考えのままにしました。

Repositoryに重複データ検証用の問い合わせを設ける

1つの理由は上述の「RepositoryはDomain以外に配置」です。

またドメインサービスの例として重複データ検証があると思います。 重複検証がエンティティに実装する振舞いには不適当だからという文脈からいう理解をしています。 つまり「永続情報から何をもって重複判定を行うのか?」という知識を どこまでコード側(Domain)で扱うか?というところだと思います。 僕は「Repositoryのメソッド名による表明で必要十分」という整理に落ち着きました。 たとえば、Repositoryを経由して問い合わせる先がDBではなく外部APIだったらどうでしょうか? そもそもそういった問い合わせ先の具体的な詳細を隠蔽することがRepositoryを用いたDIPの目的であると考えています。

さて、そうするとドメインサービスはいらないの?という問いが出てきます。
僕としては そもそも僕自身がドメインサービスの理解を誤っていた、というのが結論です。 ドメインサービスは

  • 状態を持たない関数として実装する
  • 複数ドメインオブジェクトを組み合わせて表現されるルールを扱う
  • 複数のドメインオブジェクトを扱うにあたって、どちらのドメインオブジェクトにロジックを配置するのか悩ましいときの解決方法(つまり、どちらにもロジックを配置しない)

こう理解をすると、IOは不要でも違和感はありません。

きっかけは 以下の指摘で理解が進んだ気がという感じです。

なお、僕はドメインサービスという名称はアプリケーションサービスもあって分かりにくいので「ルール(Rule)」としています。

Factoryを省略

集約ルートにメソッドを準備する、クラスとして独立させる、ということは目的と規模に応じて適宜判断するものであると考えました。
何かしら必ず作成するようなものではないので、構成図からは外すことにしました。

ライセンス

構成図は MITなので ご自由に使用してください。
Draw.ioがあれば 修正も出来ます。
なお 説明、パッケージオブジェクト、下地の3つのレイヤーにしてロックしているので、編集時には適宜 レイヤーのロックを解除してください。

参考

GitHub - system-sekkei/isolating-the-domain: architecture sample using : Spring Boot gradle, Spring MVC, Thymeleaf, and MyBatis

Atomic Designをやめてディレクトリ構造を見直した話|食べログ フロントエンドエンジニアブログ|note

さいごに

Draw.io を svg形式で保存できるのは本当に便利。

*1:Spring Bootとか Spring Securityとか

Draw.ioをGitHub管理して画像を埋め込む

はじめに

ブログなどのMarkdownドキュメントにフロー図などを書いた画像を埋めたいなぁと思っているときに、Draw.ioで画像拡張子(png/svg)で編集情報を保持したままで管理できるということを知りました。
ということは、ひょっとして埋め込み画像を直接編集して Gitなどでバージョン管理も出来るのでは?と思い至り試してみました。

何が嬉しいの?

  • ドキュメントに埋め込んだ画像の更新のためにエクスポート・アップロードをしなくて良くなる。
  • Draw.ioはローカルで編集できるのでGit管理ができる。

環境

書かないこと

  • Draw.ioの具体的な使い方

やってみた

Draw.ioファイルを作成

新規ファイル作成するときに拡張子を.dioで作成したら Draw.ioファイルとして認識してくれます。
何か描きます。

f:id:vermeer-1977:20220109122033p:plain

画像形式に変換

[ファイル] > [Convert ...] > [.drawio.svg]
変換したら、svgファイルとしてVS Code上表示されますが 改めてVS Code内で ファイルを開きなおすと ちゃんとDraw.ioファイルとして表示されます。

変わったのは拡張子だけ、という感じ(実際は画像ファイルに Draw.io用の情報が埋め込まれている)

f:id:vermeer-1977:20220109122314p:plain

GitHubに上げて確認

ちゃんと画像として読み込めていることも確認。

f:id:vermeer-1977:20220109130015p:plain

ブログなどに埋め込む

埋め込むURLとして[Raw] からリンクを取得

https://github.com/vermeer-1977-blog/blog-parts/raw/main/drawio-in-blog/test.drawio.svg

以下は、更新編集前のブログにリンクを貼った状態。
ちょっとごちゃついているけど、こんな感じで画像としてブログでも確認できる。

f:id:vermeer-1977:20220109131053p:plain

Draw.ioファイルを更新してGitHubに上げる

以下の画像は上述の手順で更新した GitHub管理下のDraw.io(svg)

https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/drawio-in-blog/test.drawio.svg

上記の通り、自動で更新される。

参考

https://tech.fusic.co.jp/posts/2021-06-15-vscode-draw-io-integration/

https://zenn.dev/kaakaa/articles/qiita-20200728-ff9ab5933cc0fb6fd8d8

さいごに

既存のDraw.ioファイルを変換しても同じことが出来ると思います。

これまでは GoogleDriveで お絵描きをして公開設定をしてリンクを貼っていましたが、これでブログ(に限らず)Git管理下に置けそうです。
Draw.ioはオフライン編集が出来るので、色々とやれることは多そうです。

シーケンス図やクラス図を書くときに、PluntUMLを使ってテキストベースでGit管理をして、それをそのまま画像変換するWebサービスみたいなものがあるのは見たことがある気がします。
もちろんそういうアプローチも良いと思いますが、お絵描き系は やっぱりGUIで描画できるのが直観的で私はDraw.ioの方が好きです*1。 あと、Draw.ioはXML形式で構造的にデータを保持しているのでプラグインとかアドオンとか何か情報取得をするときにやりやすそうな印象も受けました。

*1:PluntUML 毎度毎度 入門と中退を繰り返して一向に履修も卒業も出来る気がしないヨワヨワです

LocalStorageを扱うHooks

何が嬉しいの?

Windowをまたがった情報共有ができる。
ただし多用すると状態管理が散乱するので避けた方良い。

実装

import { useEffect, useState, useRef } from 'react';

const useCrossWindowState = <T>(
  stateKey: string,
  defaultValue: T,
): [state: T, setState: (state: T) => void] => {
  const [state, setState] = useState<T>(defaultValue);
  const isNewSession = useRef(true);

  useEffect(() => {
    if (isNewSession.current) {
      const currentState = localStorage.getItem(stateKey);
      if (currentState) {
        setState(JSON.parse(currentState));
      } else {
        setState(defaultValue);
      }
      isNewSession.current = false;
      return;
    }
    try {
      localStorage.setItem(stateKey, JSON.stringify(state));
    } catch (error) {}
  }, [state, stateKey, defaultValue]);

  useEffect(() => {
    const onReceieveMessage = (e: StorageEvent) => {
      const { key, newValue } = e;
      if (key === stateKey) {
        setState(JSON.parse(newValue ?? ''));
      }
    };
    window.addEventListener('storage', onReceieveMessage);
    return () => window.removeEventListener('storage', onReceieveMessage);
  }, [stateKey, setState]);

  return [state, setState];
};

export default useCrossWindowState;

使い方
useStateと同じ感じ。

const [volume, setVolume] = useCrossWindowState<number>('audioVolume', 15);

参考

react-cross-windows-state - CodeSandbox

さいごに

TypeScript対応をするときの型を探すのが地味に手間。
でもanyに逃げない*1

*1:今回だとStorageEvent

イメージファイルをPDFにまとめるシェルスクリプト

やりたいこと

複数のpngファイルを1つのpdfにまとめたい

環境

Lubuntu20.04(VirtualBox

下準備

ImageMagick をインストール

sudo apt install imagemagick-6.q16

ImageMagickの環境設定

/etc/ImageMagick-6/policy.xml 

の設定値を変更

resource

画像ファイルが中途半端に結合されていて「きっと、作業領域が狭いかったんだろう。」ということで、数値を適当に増やしていきました。

結構 増やしたつもりでも足りないケースがあり、最終的に「えーい、もういいや ビックリするくらい多くとってしまおう」という結果が以下。*1

coder

noneread|writeに変更

これもPDFだけで良いはずですが、とりあえず適当にザクザク設定変更

<policymap>
  <!-- <policy domain="system" name="shred" value="2"/> -->
  <!-- <policy domain="system" name="precision" value="6"/> -->
  <!-- <policy domain="system" name="memory-map" value="anonymous"/> -->
  <!-- <policy domain="system" name="max-memory-request" value="256MiB"/> -->
  <!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
  <policy domain="resource" name="memory" value="3GiB"/>
  <policy domain="resource" name="map" value="3GiB"/>
  <policy domain="resource" name="width" value="16KP"/>
  <policy domain="resource" name="height" value="16KP"/>
  <!-- <policy domain="resource" name="list-length" value="128"/> -->
  <policy domain="resource" name="area" value="3GiB"/>
  <policy domain="resource" name="disk" value="3GiB"/>
  <!-- <policy domain="resource" name="file" value="768"/> -->
  <!-- <policy domain="resource" name="thread" value="4"/> -->
  <!-- <policy domain="resource" name="throttle" value="0"/> -->
  <!-- <policy domain="resource" name="time" value="3600"/> -->
  <!-- <policy domain="coder" rights="none" pattern="MVG" /> -->
  <!-- <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> -->
  <!-- <policy domain="delegate" rights="none" pattern="HTTPS" /> -->
  <!-- <policy domain="path" rights="none" pattern="@*" /> -->
  <!-- <policy domain="cache" name="memory-map" value="anonymous"/> -->
  <!-- <policy domain="cache" name="synchronize" value="True"/> -->
  <!-- <policy domain="cache" name="shared-secret" value="passphrase" stealth="true"/> -->
  <!-- <policy domain="system" name="pixel-cache-memory" value="anonymous"/> -->
  <!-- <policy domain="system" name="shred" value="2"/> -->
  <!-- <policy domain="system" name="precision" value="6"/> -->
  <!-- not needed due to the need to use explicitly by mvg: -->
  <!-- <policy domain="delegate" rights="none" pattern="MVG" /> -->
  <!-- use curl -->
  <policy domain="delegate" rights="none" pattern="URL" />
  <policy domain="delegate" rights="none" pattern="HTTPS" />
  <policy domain="delegate" rights="none" pattern="HTTP" />
  <!-- in order to avoid to get image with password text -->
  <policy domain="path" rights="none" pattern="@*"/>
  <!-- disable ghostscript format types -->
  <policy domain="coder" rights="read|write" pattern="PS" />
  <policy domain="coder" rights="none" pattern="PS2" />
  <policy domain="coder" rights="none" pattern="PS3" />
  <policy domain="coder" rights="read|write" pattern="EPS" />
  <policy domain="coder" rights="read|write" pattern="PDF" />
  <policy domain="coder" rights="read|write" pattern="XPS" />
</policymap>

実行シェルスクリプト

#!/bin/sh
# パラメータ指定しない場合は、実行shと同列のフォルダ配下全てのpngファイルをpdfにまとめる
# pdfファイル名はフォルダ名
# temp.pdfを一気にファイル名指定ができたら良いんだけど、ちょっとわからない。
# 結果としては、親フォルダ配下にマージしたPDFを集めるということで良しとする

echo "Start ..............."

CURRENT_DIR=`dirname $0`

if [ $# = 1 ]; then
    CURRENT_DIR=$1
fi

items=$CURRENT_DIR/*

for item in $items; do
    if [ -d ${item} ]; then
        cd ${item}
        DIR_NAME=`basename ${item}`
        echo $DIR_NAME
        ls *.png | sort -n | tr '\n' ' ' | sed 's/$/\ temp.pdf/' | xargs convert
        mv -f temp.pdf ../${DIR_NAME}.pdf
    fi
done

echo "............. All Done"

参考

convertで画像をPDFへ変換する - Akionux-wiki

Ubuntu18.04 ImageMagick convertで、not authorized xxx.pdf エラー | Ninton

「さようなら ImageMagick」の考察 - Qiita

さいごに

ImageMagick脆弱性で色々と問題はあるみたいなので ご利用は自己責任で*2

*1:数値に根拠は無いので、各環境にあわせてチューニングしてください

*2:私は作業後に気が付いて、このブログをメモとして残して作業が終わったら削除しました

Karateのメモ

GitHub - karatelabs/karate: Test Automation Made Simple

www.slideshare.net

APIのテスト自動化ツール「Karate」を使ってみる - Reasonable Code

Karateツールを使用してテストケースを並行して実行し、レポートを生成する - その他

Karateに性能試験とUI試験を任せてみる - Taste of Tech Topics

APIテスト自動化ツールKarateをBDDツールとして使う - まっつんの日記