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

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

Interfaceのdefaultで多重継承(単位編)

はじめに

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

vermeer.hatenablog.jp

今回はInterfaceのdefaultを使った「数値編」を拡張した「単位編」です。
Interfaceのdefaultではなくて、Interfaceを使った実装編になります。

何が嬉しいの?

基本操作(加算などの計算)はInterfaceをimplementsするだけ。
抽象クラスや実装の継承と違って「配置したくない操作」を除外することができます。

具象クラスとしては

が差分となります。

百分率(パーセント)

小数計算のサンプルとして。

単位というと、ちょっと違いますがパーセント計算は色々と使うケースがあります。
精度の基準もシステムによってあれこれと変わります。
そういったドメインによって変わるところを具象クラスに指定をして、それ以外の基本操作はInterfaceのdefaultに任せます。
今回は、パーセントをパーセントで除算するというのは無いということをimplementsから外すことで宣言しています。

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

  /**
   * プロパティは少数値で保持するため、百分率を左に2シフトを含むSCALEの指定をします.
   */
  private static final int DEFAULT_SCALE = 5;
  private static final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.HALF_UP;

  /**
   * プロパティはパーセント表記で保持します. 30.01%の場合は, 30.01
   */
  private final BigDecimal value;

  private Percentage() {
    this.value = null;
  }

  private Percentage(BigDecimal value) {
    this.value = value.setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
  }

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

    var bigDecimal = BigDecimal.valueOf(value.doubleValue()).movePointLeft(2);
    return new Percentage(bigDecimal);
  }

  /**
   * 百分率表記からインスタンスを生成します.
   *
   * @param value 百分率表記の文字列
   * @return 生成したインスタンス
   */
  public static Percentage of(String value) {
    if (Objects.isNull(value) || Objects.equals(value, "")) {
      return new Percentage();
    }
    var bigDecimal = new BigDecimal(value).movePointLeft(2);
    return new Percentage(bigDecimal);
  }

  /**
   * 小数点表記からインスタンスを生成します.
   * <p>
   * 30%の場合は、{@code Percentage.ofDecimal("0.3") } と記述します.
   * </p>
   *
   * @param value 小数点表記の文字列
   * @return 生成したインスタンス
   */
  public static Percentage ofDecimal(Number value) {
    if (Objects.isNull(value)) {
      return new Percentage();
    }
    var bigDecimal = BigDecimal.valueOf(value.doubleValue());
    return new Percentage(bigDecimal);
  }

  /**
   * 小数点表記からインスタンスを生成します.
   * <p>
   * 30%の場合は、{@code Percentage.ofDecimal("0.3") } と記述します.
   * </p>
   *
   * @param value 小数点表記の文字列
   * @return 生成したインスタンス
   */
  public static Percentage ofDecimal(String value) {
    if (Objects.isNull(value) || Objects.equals(value, "")) {
      return new Percentage();
    }
    var bigDecimal = new BigDecimal(value);
    return new Percentage(bigDecimal);
  }

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

  @Override
  public Percentage newInstance(BigDecimal value) {
    return new Percentage(value);
  }

  /**
   * プロパティ値を百分率表記にして返却します.
   *
   * @return 百分率表記にした値(保持している値が "0.30123"の場合は, "30.123"%
   */
  public BigDecimal toPercent() {
    return this.getOrZero().movePointRight(2);
  }

  @Override
  public int hashCode() {
    int hash = 5;
    hash = 59 * hash + Objects.hashCode(this.getOrZero());
    return hash;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    final Percentage other = (Percentage) obj;
    return Objects.equals(this.getOrZero(), other.getOrZero());
  }

  @Override
  public String toString() {
    return Objects.toString(value);
  }

}

単価

良くある商品単価を扱うクラスです。

特徴としては少数の無いところです*1

表記表現から、そのままインスタンスを生成できるようなフォーマッターを使ったファクトリとか、ちょっとした工夫はありますが このあたりはリクエストボディのクラスで対応してクラス側には実装しないという方法もあると思います。

public final class Price implements Plus<Price>, Minus<Price>, Multiply<Price> {

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

  private final BigDecimal value;

  private Price() {
    this.value = null;
  }

  private Price(BigDecimal value) {
    this.value = value.setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
  }

  /**
   * インスタンスを生成します.
   *
   * @param value
   * @return 生成したインスタンス
   */
  public static Price of(Number value) {
    if (Objects.isNull(value)) {
      return new Price();
    }
    var bigDecimal = BigDecimal.valueOf(value.doubleValue());
    return new Price(bigDecimal);
  }

  /**
   * 文字列からモデルを生成します.
   *
   * @param value
   * @return 生成したインスタンス
   */
  public static Price of(String value) {
    if (Objects.isNull(value)) {
      return new Price();
    }
    var bigDecimal = new BigDecimal(value);
    return new Price(bigDecimal);
  }

  /**
   * 文字列からモデルを生成します.
   *
   * @param value 変換元の文字列
   * @return 生成したインスタンス
   */
  public static Price ofFormatted(String value) {
    if (Objects.isNull(value)) {
      return new Price();
    }
    try {
      var number = decimalFormat.parse(value);
      var bigDecimal = new BigDecimal(number.toString());
      return new Price(bigDecimal);
    } catch (ParseException ex) {
      throw new NumberFormatException("Price could not parse value = " + value);
    }
  }

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

  @Override
  public Price newInstance(BigDecimal value) {
    return new Price(value);
  }

  /**
   * 書式変換をした文字列を返却します.
   *
   * @return 変換した文字列
   */
  public String toFormatted() {
    return decimalFormat.format(this.getOrZero());
  }

  /**
   * 金額の数値を返却します.
   *
   * @param quantity
   * @return 金額
   */
  public NullableNumber multiply(Quantity quantity) {
    return this.multiply(quantity.getOrZero(), NullableNumber::of);
  }

  /**
   * 割引後単価を返却します.
   *
   * @param percentage 割引率
   * @return 割引後単価
   */
  public Price discount(Percentage percentage) {
    return this.multiply(Percentage.of(100).minus(percentage).getOrZero());
  }

  @Override
  public int hashCode() {
    int hash = 5;
    hash = 41 * hash + Objects.hashCode(this.value);
    return hash;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    final Price other = (Price) obj;
    return Objects.equals(this.getOrZero(), other.getOrZero());
  }

  @Override
  public String toString() {
    return Objects.toString(value);
  }

}

拡張として、単価×数量 による金額計算を追加しました。

こんな感じでコードで宣言的な感じで表現できるのが嬉しいかな?と思います。
操作名として「乗算(multiply)」を使っていますが、「金額計算(calcAmount)」とかでも良いかもしれないですね。

var price = Price.of("20");
var quantity = Quantity.of("10.01");
NullableNumber result = price.multiply(quantity);

あと、単価に対する割引とかも追加しました。

数量

単価とペアで数量。
ただの数字ではなくて数量型クラスを作る意味ですが、数量は分量(小数点あり)を扱うこともあるためです。
今回は、小数点第2位までを有効桁数としてみました。
数量と広く作っていますが、例えば敷物の数量のような「面積」という独自の型にしても良いかもしれませんね。

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

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

  private final BigDecimal value;

  private Quantity() {
    this.value = null;
  }

  private Quantity(BigDecimal value) {
    this.value = value.setScale(DEFAULT_SCALE, DEFAULT_ROUND_MODE);
  }

  @Override
  public Quantity newInstance() {
    return new Quantity();
  }

  @Override
  public Quantity newInstance(BigDecimal value) {
    return new Quantity(value);
  }

  /**
   * インスタンスを生成します.
   *
   * @param value
   * @return 生成したインスタンス
   */
  public static Quantity of(Number value) {
    if (Objects.isNull(value)) {
      return new Quantity();
    }

    var bigDecimal = BigDecimal.valueOf(value.doubleValue());
    return new Quantity(bigDecimal);
  }

  /**
   * 文字列からモデルを生成します.
   *
   * @param value
   * @return 生成したインスタンス
   */
  public static Quantity of(String value) {
    if (Objects.isNull(value)) {
      return new Quantity();
    }
    var bigDecimal = new BigDecimal(value);
    return new Quantity(bigDecimal);
  }

  /**
   * 文字列からモデルを生成します.
   *
   * @param value 変換元の文字列
   * @return 生成したインスタンス
   */
  public static Quantity ofFormatted(String value) {
    if (Objects.isNull(value)) {
      return new Quantity();
    }
    try {
      var number = decimalFormat.parse(value);
      var bigDecimal = new BigDecimal(number.toString());
      return new Quantity(bigDecimal);
    } catch (ParseException ex) {
      throw new NumberFormatException("Quantity could not parse value = " + value);
    }
  }

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

  /**
   * 数量による除算で割合を導出します.
   * <p>
   * 計算時のScaleと丸めは数量クラスに従います.
   * </p>
   *
   * @param quantity 数量
   * @return 割合.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   */
  public Percentage percent(Quantity quantity) {
    return this.divide(quantity, Percentage::ofDecimal);
  }

  /**
   * 数量による除算で割合を導出します.
   * <p>
   * 計算時のscaleは数量クラスに従います.
   * </p>
   *
   * @param quantity 数量
   * @param scale 小数点以下の有効桁数
   * @return 割合.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   */
  public Percentage percent(Quantity quantity, int scale) {
    return this.divide(quantity, scale, Percentage::ofDecimal);
  }

  /**
   * 数量による除算で割合を導出します.
   * <p>
   * 計算時の丸めは数量クラスに従います.
   * </p>
   *
   * @param quantity 数量
   * @param roundingMode 丸めモード
   * @return 割合.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   */
  public Percentage percent(Quantity quantity, RoundingMode roundingMode) {
    return this.divide(quantity, roundingMode, Percentage::ofDecimal);
  }

  /**
   * 数量による除算で割合を導出します.
   *
   * @param quantity 数量
   * @param scale 小数点以下の有効桁数
   * @param roundingMode 丸めモード
   * @return 割合.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
   */
  public Percentage percent(Quantity quantity, int scale, RoundingMode roundingMode) {
    return this.divide(quantity, scale, roundingMode, Percentage::ofDecimal);
  }

  @Override
  public RoundingMode getRoundingMode() {
    return DEFAULT_ROUND_MODE;
  }

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

  @Override
  public int hashCode() {
    int hash = 3;
    hash = 58 * hash + Objects.hashCode(this.value);
    return hash;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    final Quantity other = (Quantity) obj;
    return Objects.equals(this.getOrZero(), other.getOrZero());
  }

  @Override
  public String toString() {
    return Objects.toString(value);
  }

}

あと、数量は割合を求めることが多いと思うので、その操作(percent)を具象クラス独自実装として拡張しました。

こんな感じで型で宣言的な実装ができます。

var quantity1 = Quantity.of("2");
var quantity2 = Quantity.of("10");
Percentage actual2 = quantity1.percent(quantity2);

さいごに

今回のinterfaceのdefaultで一番やってみたかったのが、この「単位編」です。
計算式に型を使って宣言的な感じに実装したいなーというのがスタートです。
そこで基本的な計算を委譲で準備したものの、ちょっと冗長というか作業的に感じたので、どうにか楽は出来ないものか?というのがキッカケです。
個人的には面白い実装が出来たなぁと思っています。

次回は時間を扱う「日時編」を予定。

*1:もちろん、業務内容によっては小数ありもあると思います