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

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

Interfaceのdefaultで多重継承(数値編)

はじめに

コードの全量のリンクをこちらの記事に書いているので、先読みで全量を見たい方はこちらを参照してください。

vermeer.hatenablog.jp

今回はInterfaceのdefaultを使った具体的な実装編の「数値編」です。
もともとの出発点がこちらの数値編(計算編?)だったりします。
BigDecimalを使ったユーティリティをInterfaceのdefaultで実現できないかな?というのがキッカケです。

何が嬉しいの?

精度を必要とするBigDecimalによる計算は、なんやかんやで冗長なロジックが必要です。

int x = x + 1;

くらいのシンプルな実装で扱えると嬉しい。
オブジェクトとメソッドを使うとして、Javaの言語仕様として このくらいを落としどころになると嬉しいかな、という感じです。

Numeric y = Numeric.of(1).plus(Numeric.of(2));

Interfaceのdefaultを使ってちょっと楽をする

数値を扱う場合、ほとんどは int で事足ります。
割合のような小数を扱ったり、割り算をするときになると急に手間が増えます。
「計算をする」という操作が、計算の種類によってやり方が変わってしまうのを統一した使用感で扱うためにInterfaceを使って 操作の属性として分類をすると そのあたりがスッキリします。
プロパティとして常にBigDecimalにすることで精度が必要になった時に急に操作手順が変えることもない、それくらいのちょっとした楽ができます。

また、無味乾燥な数字を計算することと違って型をもった数値を使った計算をするというのは 「1 + 1 = 2」に加えて、計算対象とその結果の属性を考える必要があります。
具体的には「1個のリンゴ + 3m紐 = ?」みたいな計算は出来ないという感じです。
型を使うことで制約を設けられるので考えることをちょっと減らせて楽ができます。

Nullの許容と数値の基本型

実際にロジックとして単純な計算をするときには精度を必要とすることはあまりありません。
なので、精度に関係する情報はdefaultで定義してしまいます。
精度を設けたい場合は、具象クラスの基本知識として個別にオーバーライドをして解決します。

public interface NullableNumberType<T> extends SinglePropertyObjectType<BigDecimal> {

  /**
   * プロパティの値を返却します.
   *
   * @return <code>null</code>の場合はZERO
   */
  default BigDecimal getOrZero() {
    return this.getNullableValue().orElse(BigDecimal.ZERO);
  }

  /**
   * プロパティがNullであるか判定をします.
   *
   * @return nullの場合はtrue
   */
  default boolean isEmpty() {
    return this.getNullableValue().isEmpty();
  }

  /**
   * 保持する値に適用するScaleを返却します.
   *
   * @return 除算に使用する丸め
   */
  default int getScale() {
    return 0;
  }

  /**
   * 計算に使用する丸めを返却します.
   *
   * @return 除算に使用する丸め
   */
  default RoundingMode getRoundingMode() {
    return RoundingMode.UNNECESSARY;
  }

}

編集汎用

文字列編と同様で汎用的な関数による編集用の型です。

public interface NumericUnaryOperator<T> extends NullableNumberType<T>, SingleArgumentNewInstance<BigDecimal, T> {

  /**
   * callbackを用いて保持している値を編集して新しいインスタンスを生成します.
   *
   * @param callback コールバック関数
   * @return 編集後の新しいインスタンス.
   */
  default T apply(UnaryOperator<BigDecimal> callback) {
    BigDecimal updated = callback.apply(this.getNullableValue().orElse(null));
    return newInstance(updated);
  }
}

加算

加算で考えられる制約は

  • 左辺と右辺は同じ型である
  • 結果も同じ型である
public interface Plus<T extends NullableNumberType<T>> extends NullableNumberType<T>, SingleArgumentNewInstance<BigDecimal, T> {

  /**
   * 加算したインスタンスを返却します.
   *
   * @param other 計算するインスタンス
   * @return 計算後のインスタンス
   */
  @SuppressWarnings("unchecked")
  default T plus(T... other) {
    if (other.length == 1) {
      var result = this.getOrZero().add(other[0].getOrZero());
      return this.newInstance(result);
    }
    var result = Stream.of(other)
            .map(T::getOrZero)
            .reduce(this.getOrZero(), BigDecimal::add);

    return this.newInstance(result);
  }

  /**
   * 複数要素を加算します.
   * <p>
   * 加算は複数情報を一括で処理したいケースがあるため、デフォルトでも準備しています.
   * </p>
   *
   * @param others 計算するインスタンスリスト
   * @return 計算後のインスタンス
   * @throws RuntimeException 引数インスタンスからクラス情報が取得出来ない場合
   */
  default T plusAll(Collection<T> others) {
    var result = others.stream()
            .map(T::getOrZero)
            .reduce(this.getOrZero(), BigDecimal::add);
    return this.newInstance(result);
  }

}

減算

減算で考えられる制約は

  • 左辺と右辺は同じ型である
  • 結果も同じ型である

加算と違って、連続した減算は、加算した結果をまとめて減算をするものだろうということで plusAll相当は作成せず。
一応、申し訳ない程度に(?)、可変長引数にはしました。

public interface Minus<T extends NullableNumberType<T>> extends NullableNumberType<T>, SingleArgumentNewInstance<BigDecimal, T> {

  /**
   * 減算したインスタンスを返却します.
   *
   * @param other 計算するインスタンス
   * @return 計算後のインスタンス
   */
  @SuppressWarnings("unchecked")
  default T minus(T... other) {
    if (other.length == 1) {
      var result = this.getOrZero().subtract(other[0].getOrZero());
      return this.newInstance(result);
    }
    BigDecimal result = Stream.of(other)
            .map(T::getOrZero)
            .reduce(this.getOrZero(), BigDecimal::subtract);

    return this.newInstance(result);
  }

}

乗算

乗算は、加算/減算と制約が異なります。

  • 左辺と右辺は同じ型でなくても良い(もしくはいずれかの型)
  • 結果も同じ型でなくても良い(もしくはいずれかの型)
  • 単位無しもありうる

リンゴ3個×3人分=リンゴ9個
3人分×リンゴ3個=リンゴ9個
こんな感じで、右辺・左辺のどちらかに単位を計算する操作だけで決められないという感じです。

また、乗算では単位の無い、ただの数値を用いたいこともあります

100円の80%として
100円×0.8 = 80円
という感じです。

もちろん、百分率型を準備して
100円×80% = 80円 というやり方もあります。
(こちらは次の記事で書く予定です)

public interface Multiply<T extends NullableNumberType<T>> extends NullableNumberType<T>, SingleArgumentNewInstance<BigDecimal, T> {

  /**
   * 乗算したインスタンスを返却します.
   *
   * @param <U> 引数の型, 乗算は自身と異なるクラスを指定することもできます.
   * @param other 計算に使用するインスタンス
   * @return 計算後のインスタンス(自クラスを単位とします)
   */
  default <U extends NullableNumberType<U>> T multiply(U other) {
    return this.multiply(other.getOrZero());
  }

  /**
   * 乗算したインスタンスを返却します.
   * <p>
   * インスタンスの値を単純な数値により乗数計算します.
   * </p>
   *
   * @param <U> 引数の数値の型
   * @param otherValue 計算に使用する数値. 直接数値を指定することを想定しているため、Nullを許容しません.
   * @return 計算後のインスタンス(自クラスを単位とします)
   * @throws NullPointerException otherがNullの場合
   */
  default <U extends Number> T multiply(U otherValue) {
    if (Objects.isNull(otherValue)) {
      throw new NullPointerException();
    }
    var other = BigDecimal.valueOf(otherValue.doubleValue());
    var result = this.getOrZero().multiply(other);
    return this.newInstance(result);
  }

  /**
   * 乗算したインスタンスを返却します.
   *
   * @param <U> 引数の型, 乗算は自身と異なる型のクラスを指定することもできます.
   * @param <R> 新たなインスタンスを生成する関数の戻り値の型(本メソッドのインスタンスの型)
   * @param other 計算に使用するインスタンス
   * @param funcNewInstance 任意のクラスを生成する関数
   * @return 計算後のインスタンス(関数で指定した任意の型)
   */
  default <U extends NullableNumberType<U>, R> R multiply(U other, Function<BigDecimal, R> funcNewInstance) {
    return this.multiply(other.getOrZero(), funcNewInstance);
  }

  /**
   * 乗算したインスタンスを返却します.
   *
   * @param <U> 引数の型, 乗算は自身と異なる型のクラスを指定することもできます.
   * @param <R> 新たなインスタンスを生成する関数の戻り値の型(本メソッドのインスタンスの型)
   * @param otherValue 計算に使用する数値. 直接数値を指定することを想定しているため、Nullを許容しません.
   * @param funcNewInstance 任意のクラスを生成する関数
   * @return 計算後のインスタンス(関数で指定した任意の型)
   */
  default <U extends Number, R> R multiply(U otherValue, Function<BigDecimal, R> funcNewInstance) {
    BigDecimal result = this.multiply(otherValue).getOrZero();
    return funcNewInstance.apply(result);
  }
}

除算

除算も乗算と同じような型のルールが良さそうです。

  • 左辺と右辺は同じ型でなくても良い(もしくはいずれかの型)
  • 結果も同じ型でなくても良い(もしくはいずれかの型)
  • 単位無しもありうる

それに加えて計算時に精度の指定も欲しいところです。
精度はいくつかの組み合わせがあると思ったのでルールを設けすぎず、ある程度 任意の組み合わせが出来るようにしました*1

public interface Divide<T extends NullableNumberType<T>> extends NullableNumberType<T>,
        NoArgumentNewInstance<T>, SingleArgumentNewInstance<BigDecimal, T> {

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは自インスタンス(this)で定義されたものを適用します.
   * </p>
   * 計算に使用するScaleと丸めは自インスタンス(this)で定義されたものを使用します.
   *
   * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
   * @param other 除算の分母となるインスタンス
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   * @throws ArithmeticException 割り切れない場合
   */
  default <U extends NullableNumberType<U>> T divide(U other) {
    return this.divide(other, this.getScale(), this.getRoundingMode());
  }

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは自インスタンス(this)で定義されたものを適用します.
   * </p>
   * 計算に使用する丸めは自インスタンス(this)で定義されたものを使用します.
   *
   * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
   * @param other 除算の分母となるインスタンス
   * @param scale 小数点以下の有効桁数
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   * @throws ArithmeticException 割り切れない場合
   */
  default <U extends NullableNumberType<U>> T divide(U other, int scale) {
    return this.divide(other, scale, this.getRoundingMode());
  }

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは自インスタンス(this)で定義されたものを適用します.
   * </p>
   * 計算に使用する丸めは自インスタンス(this)で定義されたものを使用します.
   *
   * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
   * @param other 除算の分母となるインスタンス
   * @param roundingMode 丸めモード
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   * @throws ArithmeticException 割り切れない場合
   */
  default <U extends NullableNumberType<U>> T divide(U other, RoundingMode roundingMode) {
    return this.divide(other, this.getScale(), roundingMode);
  }

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは自インスタンス(this)で定義されたものを適用します.
   * </p>
   * 計算に使用する丸めは自インスタンス(this)で定義されたものを使用します.
   *
   * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
   * @param other 除算の分母となるインスタンス
   * @param roundingMode 丸めモード
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   * @throws ArithmeticException 割り切れない場合
   */
  default <U extends Number> T divide(U other, RoundingMode roundingMode) {
    return this.divide(other, this.getScale(), roundingMode);
  }

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

    return this.divide(other.getOrZero(), scale, roundingMode);
  }

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは自インスタンス(this)で定義されたものを適用します.
   * </p>
   *
   * @param <U> 引数の数値の型
   * @param otherValue 計算に使用する数値. 直接数値を指定することを想定しているため、Nullを許容しません.
   * @param scale 小数点以下の有効桁数
   * @param roundingMode 丸めモード
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   */
  default <U extends Number> T divide(U otherValue, int scale, RoundingMode roundingMode) {
    if (Objects.isNull(otherValue)) {
      throw new NullPointerException();
    }

    var other = BigDecimal.valueOf(otherValue.doubleValue());

    if (other.compareTo(BigDecimal.ZERO) == 0) {
      return this.newInstance(BigDecimal.ZERO);
    }

    BigDecimal result = this.getOrZero().divide(other, scale, roundingMode);
    return this.newInstance(result);
  }

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは関数で生成するクラスの定義を適用します.
   * </p>
   * 計算に使用するScaleと丸めは計算に使用するインスタンス(other)で定義されたものを使用します.
   *
   * @param <U> 引数の型, 除算は自身と異なる型のクラスを指定することもできます.
   * @param <R> 新たなインスタンスを生成する関数の戻り値の型
   * @param other 計算するインスタンス
   * @param funcNewInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   * @throws ArithmeticException 割り切れない場合
   */
  default <U extends NullableNumberType<U>, R extends NullableNumberType<R>> R divide(
          U other, Function<BigDecimal, R> funcNewInstance) {
    return this.divide(other, other.getScale(), other.getRoundingMode(), funcNewInstance);
  }

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは関数で生成するクラスの定義を適用します.
   * </p>
   * 計算に使用する丸めは計算に使用するインスタンス(other)で定義されたものを使用します.
   *
   * @param <U> 引数の型, 除算は自身と異なる型のクラスを指定することもできます.
   * @param <R> 新たなインスタンスを生成する関数の戻り値の型
   * @param other 計算するインスタンス
   * @param scale 小数点以下の有効桁数
   * @param funcNewInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   * @throws ArithmeticException 割り切れない場合
   */
  default <U extends NullableNumberType<U>, R extends NullableNumberType<R>> R divide(
          U other, int scale, Function<BigDecimal, R> funcNewInstance) {
    return this.divide(other, scale, other.getRoundingMode(), funcNewInstance);
  }

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは関数で生成するクラスの定義を適用します.
   * </p>
   * 計算に使用する丸めは計算に使用するインスタンス(other)で定義されたものを使用します.
   *
   * @param <U> 引数の型, 除算は自身と異なる型のクラスを指定することもできます.
   * @param <R> 新たなインスタンスを生成する関数の戻り値の型
   * @param other 計算するインスタンス
   * @param roundingMode 丸めモード
   * @param funcNewInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   */
  default <U extends NullableNumberType<U>, R extends NullableNumberType<R>> R divide(
          U other, RoundingMode roundingMode, Function<BigDecimal, R> funcNewInstance) {
    int scale = other.getOrZero().scale();
    return this.divide(other, scale, roundingMode, funcNewInstance);
  }

  /**
   * 除算したインスタンスを返却します.
   * <p>
   * 計算後のインスタンスのScaleと丸めは関数で生成するクラスの定義を適用します.
   * </p>
   *
   * @param <U> 引数の型, 除算は自身と異なる型のクラスを指定することもできます.
   * @param <R> 新たなインスタンスを生成する関数の戻り値の型
   * @param other 計算に使用するインスタンス
   * @param scale 小数点以下の有効桁数
   * @param roundingMode 丸めモード
   * @param funcNewInstance 任意のクラスを生成する関数
   * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   */
  default <U extends NullableNumberType<U>, R extends NullableNumberType<R>> R divide(
          U other, int scale, RoundingMode roundingMode, Function<BigDecimal, R> funcNewInstance) {

    if (other.getOrZero().compareTo(BigDecimal.ZERO) == 0) {
      return funcNewInstance.apply(BigDecimal.ZERO);
    }

    var result = this.getOrZero().divide(other.getOrZero(), scale, roundingMode);
    return funcNewInstance.apply(result);
  }
}

比較

計算(編集)に加えて、比較も欲しいところです。
BigDecimalの比較は、compareToを使えばできますが表現として少し直感的とは言いにくいので、そこをカバーすると ちょっとだけですが嬉しいです。

eqequalsは同じケースが多いとは思いますが、後者はインスタンスとしての等価で、こちらはあくまで数値比較に限定するので、厳密にいえば違うこともあり得るように思います。

public interface NumericComparator<T extends NullableNumberType<T>> extends NullableNumberType<T> {

  /**
   * equal to.
   *
   * @param other 比較対象
   * @return {@code this = other} を満たす場合 true(nullはZEROとして比較します)
   */
  default boolean eq(T other) {
    return this.getOrZero().compareTo(other.getOrZero()) == 0;
  }

  /**
   * not equal to.
   *
   * @param other 比較対象
   * @return {@code this != other} を満たす場合 true(nullはZEROとして比較します)
   */
  default boolean ne(T other) {
    return this.getOrZero().compareTo(other.getOrZero()) != 0;
  }

  /**
   * less than.
   *
   * @param other 比較対象
   * @return {@code this < other} を満たす場合 true(nullはZEROとして比較します)
   */
  default boolean lt(T other) {
    return this.getOrZero().compareTo(other.getOrZero()) == -1;
  }

  /**
   * less than or equal to.
   *
   * @param other 比較対象
   * @return {@code this <= other} を満たす場合 true(nullはZEROとして比較します)
   */
  default boolean le(T other) {
    return this.eq(other) || this.lt(other);
  }

  /**
   * greater than.
   *
   * @param other 比較対象
   * @return {@code this > other} を満たす場合 true(nullはZEROとして比較します)
   */
  default boolean gt(T other) {
    return this.getOrZero().compareTo(other.getOrZero()) == 1;
  }

  /**
   * greater than or equal to.
   *
   * @param other 比較対象
   * @return {@code this >= other} を満たす場合 true(nullはZEROとして比較します)
   */
  default boolean ge(T other) {
    return this.eq(other) || this.gt(other);
  }

}

テスト

Interfaceのdefaultのテストについても少し補足

Interfaceのロジックをテストするために代表となる具象クラスを作ってテストをするという方法もあると思いますがテストユニット内だけで完結すれば余計なクラスを作成する必要もなくなります。

加算を例に示します。

テスト用の具象クラスを準備します。

  • テストユニット内のインナークラスを作る
  • インナークラスはstaticにする

インナークラスは、まぁそうですねという感じですが、staticにする理由は InstanceCreatorでインスタンス生成をさせるためです。
staticにしないとエラーになります。

注意点は そのくらいでしょうか?

public class PlusTest {

  @Test
  public void testPlus() {
    var value1 = new PlusImpl(new BigDecimal(10));
    var value2 = new PlusImpl(new BigDecimal(20));
    var actual1 = value1.plus(value2);
    assertEquals(30, actual1.getOrZero().intValue());

    var actual2 = value2.plus(value1, value1);
    assertEquals(40, actual2.getOrZero().intValue());
  }

  @Test
  public void testPlusAll() {
    var value1 = new PlusImpl(new BigDecimal(10));                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
    var value2 = new PlusImpl(new BigDecimal(20));
    var list = List.of(value1, value2);
    var actual1 = value1.plusAll(list);
    assertEquals(40, actual1.getOrZero().intValue());
  }

  static class PlusImpl implements Plus<PlusImpl> {

    private final BigDecimal value;

    public PlusImpl() {
      this.value = null;
    }

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

    @Override
    public Optional<BigDecimal> getNullableValue() {
      return Optional.ofNullable(value);
    }
  }

}

さいごに

常にBigDecimalを使うという事は、intを使うよりも計算コストが高いんじゃないの?という批判(?)は正しいです。
そもそもですが計算量や計算速度を突き詰めるようなケースではドメインモデル的な実装をせず、C言語的(?)な実装をすることをお勧めします*2

ユーティリティを作るよりも正直手間がかかりました。
「計算をすることとは?」という感じで、これまで深く考えていなかったことを改めて考え直す機会として有意義だったように思います。
OOPLの話ではないといいつつですが、構造化プログラミングと違って、継承をベースにする実装を考えるのは難しいなぁ半分、一度部品を作ったらあとは楽も出来るなぁ半分という感想を持ちました。

次回は数値をベースとした「単位編」を予定。

*1:もうちょっと絞り込めるような気もしますが…

*2:要はバランス