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

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

JJUGナイトセミナー Jakarta EE特集

はじめに

こちらに参加してきました

speakerdeck.com

メモ

さっととったメモというか感想みたいなものを放流

豆知識

Spring Bootの成果物をOpen Libertyの上で実行すると高いスループットを出すこともあるよ。

Jakarta EE 11

Java SE 17が仕様の前提。

Jakarta Data

Repositoryの戻り値がJPAのEntityになるみたい?
となると3層+ドメインだと、Repositoryからインフラ層で、その中で更にRepositoryという名前のインターフェースでデータ取得をするっていう、名前付け的な意味で結構わかりにくくなりそう。
というか、サービスから直接 Jakarta DataのRepositoryを呼ぶイメージなんだろうなって思った。
理由は、以前、JJUG CCCだったと思うんだけど、MSのJavaの方が「EEでDDD」的なセッションでは、JPA(のEntity)をドメインオブジェクトの中核として扱っていたので。
この辺りは流派が違うというか、EEを軸にしている人は「EEパッケージの依存が入ることは問題ない」という流派なんだと思う。
僕はドメインオブジェクトは可能な限りPOJOでEEパッケージの依存も入れたくない流派 。 抽象化をどこにどういう感じで仕込むのか、みたいなのは好みが分かれるよなぁなどと思う。

メジャーとマイナーのバージョンアップの違い

互換性が無くなったらメジャー、、なんだけどPersistanceは非推奨削除だけどマイナーバージョンアップだったり、必ずしも統一的ではないみたい。

TCKの修正もバージョンアップの対象

仕様の策定も含めてEEだから、TCKの修正もその対象になるというのは確かにだけど気が付かなかった発見。

EE11で変更が一番多かったのはJPA

日付の型はLocalDate系以外は非推奨。

javaxを単純にjakartaに変換するのはNG

Java SE の javaxは置換するとNG。
Eclipse Transformerが有力。
バイトコードも変換対象にする。
ソースコードが入手できない場合でも対応ができるということ。

クラスタリングのベースの遷移

昔はミドルウェアによる実現であった。
今はK8sのようにインフラ自体がその主体になっているというのが違い。 EEの思想はJVM自体がクラスタリングの主体だったところが、もっと広いところで実現するようになったかんじかな?
このあたりは、むかーし Glassfish勉強会で 寺田さんがK8sだったかな?を扱った話をしていた時にも思ったところと同じかな?

Glassfishを扱わなくなったのでOracleにはEEの参照実装がない

そうか、、そういうことになるのか、、

WebSphereLibertyは全部カバー

JavaEEもJakartaEEとMicroprofile全部乗せ。

Spring Boot3からjakartaパッケージ

同梱のTomcatに引っ張られる感じ。

EEの良くないところ

アプリケーションサーバーとwarのデプロイがいるということ。
Spring Bootとの違いともいえる。
確かになぁ…
実際、改めてやってみると管理コンソールでDB接続情報の設定とか、Tomcatで同梱みたいなのと比べると敷居は高い。
情報量も年々少なくなってきているし…

OpenLibertyはBootに近いのかな?

導入可能zipに加えて、実行可能jarも作成できる。
ただ、実行可能jarはログとか異常終了したときの振る舞いとか設定が色々必要なので実際はちょっと面倒とのこと。
なのでおすすめは導入可能zipをunzipして実行みたい。

OpenLibertyを使ってみようかな?

と、2年前も思って、そのまま今(Netbeans + Payara)に至る…
あくまで素振りな実装をしているだけだから、それで事足りてしまっているんだよなぁ…
クラウド利用を真剣に(?)考えるんだったら、OpenLibertyは真剣に導入を考えたいと思いつつ(そして月日が過ぎていく)。

EJBが非推奨はどうなりそう?

今のところはCDIを推奨で、EJBは消えはしないけど放置したままにするという扱いというのが現時点。
なので「非推奨」にもなっていない、ただし放置。

セキュリティマネージャー

脆弱性につながっているケースが少なからずあるみたい?
(このあたりは良く分からない)

VSCodeJavaの開発はどのくらいできるもの?

GUIによる設定とかを期待すると弱いとは思うけど、開発するためのIDEとしては随分と使えるようになっているみたい。
3年くらい前に VSCode + Payara をやろうとして、ちょっと躓いて、即 Netbeansに戻った情弱だけれど、OpenLibertyの仕組みとか聞いているといけそうな気もしてくる。
理由:VSCode + Payara で躓いた理由は、IDEとEEサーバの連携するところがイマイチ イメージできなくて、何をしたら良いのか調べるのも億劫になって考えるのを止めたというのが経緯だったと思うから(目の前に動かせるNetbeansがあるから…)

OpenLibertyは逆にアプケーションサーバーへのデプロイをする、というのとは違うアプローチなので、そういう意味だと Bootに近くて VSCodeでやりやすい印象をもったというのが感想。

さいごに

見知らぬ人との会話の訓練を、、と考えて懇親会にも参加しました。
人数が少ないこともあって、探り探りではありましたが、色々と話を伺う事ができて楽しい時間でした。
前々回は参加したけど懇親会は顔出しだけして気分すぐれず即撤退、前回のJJUG CCCは不参加、という感じで人と交わることに強いストレスを感じることが多くなってしまっていたけれど、今回の懇親会は良いリハビリ(?)になったように思います。

Interfaceのdefaultで多重継承(ファーストクラスコレクション編)

はじめに

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

vermeer.hatenablog.jp

こちらでファーストクラスコレクションを扱う実装例を考えてみました。

ファーストクラスコレクション - システム開発で思うところ

少し工夫をすればInterfaceのdefaultで実装をするパターンが作れそうだと思ってやってみました。

何が嬉しいの?

ファーストクラスコレクションを統一して扱う操作の定義をしておくと類似実装をする時のブレがなくなります。
副作用の伴う操作は独立したInterfaceにすることで、対象のファーストクラスコレクションが副作用を扱うか、扱わないかということを宣言を調べることで判別できるようになります。

基底の型

リストの基本情報の取得と、変換用の操作を持たせました。
getValues()で内部リストを公開しているので、それを使ってしまえば何でも出来てしまうのですが…。
アクセッサを非公開にすることも考えたのですが、それを実現するためにはファーストクラスコレクションを抽象クラスにすれば実現できそうではあったのですが、それをすると「副作用を持たないファーストクラスコレクション」をつくることができないため断念しました。
なので、アクセッサの利用抑止をさせたい場合は ArchUnitを使えば…と思ったりします*1

public interface FirstClassCollectionType<T> {

  /**
   * 保持しているリストを返却します.
   * <p>
   * ファーストクラスコレクション内で使用するようにしてください.
   *
   *
   * @return
   */
  List<T> getValues();

  /**
   * リストが空であることを判定します.
   *
   * @return 空リストの場合はtrue
   */
  default boolean isEmpty() {
    return this.getValues().isEmpty();
  }

  /**
   * 保持しているリストの件数を返却します.
   *
   * @return 保持しているリストの件数
   */
  default int size() {
    return this.getValues().size();
  }

  /**
   * 要素の変換をしたリストを返却します.
   * <p>
   * ドメインオブジェクトからDTOへの変換に使用することを想定したもののため、更にファーストクラスコレクションにしたい場合は 変換後のリストをコンストラクタ(またはFactoryメソッド)の引数として指定してください.
   * </p>
   *
   * @param <R> 変換後のリスト要素の型
   * @param function リストの要素を変換する関数
   * @return 要素を変換したリスト
   */
  default <R> List<R> apply(Function<T, R> function) {
    return this.getValues().stream().map(item -> {
      return function.apply(item);
    }).collect(Collectors.toUnmodifiableList());
  }
}
  • テスト
public class FirstClassCollectionTypeTest {

  @Test
  public void testGetValues() {

    var fstClassCollection = FirstClassCollectionTypeImpl.of(List.of(new Item("1"), new Item("2")));
    List<Item> items = fstClassCollection.getValues();

    assertEquals(new Item("1"), items.get(0));
    assertEquals(new Item("2"), items.get(1));
    assertEquals(2, items.size());

  }

  @Test
  public void testIsEmpty() {
    var fstClassCollection = FirstClassCollectionTypeImpl.of(null);
    assertTrue(fstClassCollection.isEmpty());
  }

  @Test
  public void testSize() {
    var fstClassCollection = FirstClassCollectionTypeImpl.of(List.of(new Item("1"), new Item("2")));
    assertEquals(2, fstClassCollection.size());
  }

  @Test
  public void testApply() {
    var fstClass = FirstClassCollectionTypeImpl.of(List.of(new Item("1"), new Item("2")));
    List<Item2> convertedItemList = fstClass.apply(item -> new Item2(item.getValue()));
    assertEquals(2, convertedItemList.size());
    assertEquals("1", convertedItemList.get(0).getValue());
    assertEquals("2", convertedItemList.get(1).getValue());
  }

  static class FirstClassCollectionTypeImpl implements FirstClassCollectionType<Item> {

    private final List<Item> values;

    private FirstClassCollectionTypeImpl(List<Item> values) {
      this.values = List.copyOf(values);
    }

    static FirstClassCollectionTypeImpl of(List<Item> values) {
      List<Item> items = Objects.nonNull(values) ? List.copyOf(values) : Collections.emptyList();
      return new FirstClassCollectionTypeImpl(items);
    }

    @Override
    public List<Item> getValues() {
      return this.values;
    }
  }

副作用

リストの要素に対して副作用を伴う操作をしたい場合はこちらを使用します。
こちらもgetValues()使えば良いじゃないというのは、ごもっともなんですが、こちらを使えば「副作用に使用されている」ということが宣言的に調査がしやすくなるので、こちらを使って実装をしてもらいたいところです。

public interface FirstClassCollectionConsumer<T> extends FirstClassCollectionType<T> {

  /**
   * リスト要素毎に副作用の伴う操作を行います.
   *
   * @param consumer 副作用を行う関数
   */
  default void accept(Consumer<T> consumer) {
    this.getValues().forEach(item -> {
      consumer.accept(item);
    });
  }

}
  • テスト
public class FirstClassCollectionConsumerTest {

  @Test
  public void testAccept() {
    var fstClass = FirstClassCollectionConsumerImpl.of(List.of(new Item("1"), new Item("2")));

    List<Item> items = new ArrayList<>();

    // 本来は標準出力などの副作用のあるロジックの実行を想定
    fstClass.accept(item -> items.add(item));
    assertEquals(2, items.size());
    assertEquals("1", items.get(0).getValue());
    assertEquals("2", items.get(1).getValue());

  }

  public static class FirstClassCollectionConsumerImpl implements FirstClassCollectionConsumer<Item> {

    private List<Item> values;

    private FirstClassCollectionConsumerImpl(List<Item> values) {
      this.values = values;
    }

    static FirstClassCollectionConsumerImpl of(List<Item> values) {
      List<Item> items = Objects.nonNull(values) ? List.copyOf(values) : Collections.emptyList();
      return new FirstClassCollectionConsumerImpl(items);
    }

    @Override
    public List<Item> getValues() {
      return this.values;
    }
  }

さいごに

filterなど「新たなファーストクラスコレクションを生成する」というパターンもやろうと思って

Interfaceのdefaultで多重継承(下準備編) - システム開発で思うところ

で作った「InstanceCreator」を使う事を考えたのですが、リフレクションをベースにせず関数をベースにした生成をした方が黒魔術感の無い実装になるのでは?と思い直して一旦この段階で止めました。
気が向いたら、過去のものも含めて、ちょっと考え直してみようかな?と思っています。

*1:あとはInterfaceのメソッドがprotectedが使えれば良いのですが無いものは仕方ないので…

ファーストクラスコレクション

はじめに

僕がはじめて「ファーストクラスコレクション」という名前を知ったのは
@masuda220 さんの
「現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法」から。

何が嬉しいの?

ファーストクラスコレクションっていうだけだと Listをラップするだけなんですが、もうちょっと こうしたらイイよねというのをコード例を添えて。

コード

実装

  • リストの要素
public class Item {

  // 不変となる実装をしておくこと
  private final String value;

  public Item(String value) {
    this.value = value;
  }

  public String getValue() {
    return value;
  }
}
  • ファーストクラスコレクション
public class FirstClassCollection {

  private final List<Item> items;

  private FirstClassCollection(List<Item> items) {
    this.items = items;
  }

  public static FirstClassCollection ofWithUnmodifiableList(List<Item> items) {
    // nullを許容しません
    List<Item> list = Objects.nonNull(items) ? Collections.unmodifiableList(items) : Collections.emptyList();
    return new FirstClassCollection(list);
  }

  public static FirstClassCollection ofWithCopyOf(List<Item> items) {
    // nullを許容しません
    List<Item> list = Objects.nonNull(items) ? List.copyOf(items) : Collections.emptyList();
    return new FirstClassCollection(list);
  }

  // 加工をした後もimmutable
  public FirstClassCollection filterBy(String value) {
    List<Item> filterd = this.items.stream().filter(e -> e.getValue().equals(value)).collect(Collectors.toList());
    return FirstClassCollection.ofWithCopyOf(filterd);
  }

  // 要素の変換はcallbackで
  public <R> List<R> apply(Function<Item, R> function) {
    return this.items.stream().map(item -> {
      return function.apply(item);
    }).collect(Collectors.toUnmodifiableList());
  }

  // 副作用の伴う操作もcallbackで
  public void accept(Consumer<Item> consumer) {
    this.items.forEach(item -> {
      consumer.accept(item);
    });
  }

  // テスト用にアクセッサを作っているけれど無くなっても大丈夫なのでプロパティを不変にできます
  public List<Item> getItems() {
    // インスタンス生成時に不変を保証しているので、そのまま返却しても問題ありません.
    return this.items;
  }

}

説明

Nullを許容しない

インスタンス変数でリストを扱うメリットはNullをゼロサイズのリストに出来るところです。
どうしても「Nullだったことを残したい」場合は、、Optionalを使うかそれ用のインスタンス変数を追加して判定できるようにすると良いと思います。*1

インスタンス生成時にimmutable

内部ロジックで加工・編集をする際でも、うっかりインスタンス変数を上書きしてしまわないようにガードをかけます。

やってみて分かったことなのですが、Collections#unmodifiableListだと元とのなるリストに要素を追加すると、インスタンス変数のリストの状態も更新されてしまいました。
Java10以降のList#copyOfを使うのが良さそうです。

加工をした後もimmutable

要素の絞り込みをした場合や加工をした場合も戻り値をファーストクラスコレクションにしておけば「インスタンス生成時にimmutable」になります。
List<?>をそのまま返却してしまいがちですが、そこは ぐっとこらえてファーストクラスコレクションにします。

要素の変換はcallbackで

ドメインオブジェクトからDTOへの変換など、getterにしたいところが必ずあります。
ただ、そこでインスタンス変数を開放してしまうと、直接操作(ループを回す)というロジックがどうしても外に出てしまいます。
そういうのを発生し辛くするという意味で、getterよりもcallbackを使って「リストへの操作を利用側が決める」という口を準備すると良いかな?と思います。

副作用の伴う操作もcallbackで

「リストの全要素を標準出力に書き出す」「リストの全要素をRepositoryを使ってDBへインサートする」というような副作用をループで回したい場合はこちら(Consumer)を使います。

アクセッサは無くても大丈夫

ただしくは「要素の変換は」とか「副作用の伴う操作」のcallbackを使ったら内部情報を外に出すことが出来る裏口はあるので直接インスタンス変数を開放する必要は無いです。
むしろアクセッサがあるとついついリスト構造をつかって、ファーストクラスコレクションの外でリストの操作をしてしまいやすくなるので無いくらいでも良いかもしれません。

テスト関連

リストの変換後用のオブジェクト

public class Item2 {

  // 不変となる実装をしておくこと
  private final String value;

  public Item2(String value) {
    this.value = value;
  }

  public String getValue() {
    return value;
  }
}
public class FirstClassCollectionTest {

  @Test
  void test不変の確認() {

    ArrayList<Item> items1 = new ArrayList<>();
    items1.add(new Item("1"));
    items1.add(new Item("2"));

    var fstClasse1 = FirstClassCollection.ofWithCopyOf(items1);
    var fstClasse2 = FirstClassCollection.ofWithUnmodifiableList(items1);

    // インスタンス変数では不変になっているので更新、追加、削除は不可
    assertThrows(UnsupportedOperationException.class, () -> fstClasse1.getItems().set(0, new Item("3")));
    assertThrows(UnsupportedOperationException.class, () -> fstClasse1.getItems().add(new Item("3")));

    assertThrows(UnsupportedOperationException.class, () -> fstClasse2.getItems().set(0, new Item("3")));
    assertThrows(UnsupportedOperationException.class, () -> fstClasse2.getItems().add(new Item("3")));

    items1.add(new Item("3"));

    // OK:List.copyOf だとitemsの元インスタンスへの要素の追加による影響を受けない
    assertEquals(2, fstClasse1.getItems().size());
    assertFalse(items1 == fstClasse1.getItems());

    // NG:Collections.unmodifiableList だとitemsの元インスタンスへの要素の追加による影響を受ける
    assertEquals(3, fstClasse2.getItems().size());
    // ListとArrayListとして相違ありとなってはいるが参照先としては同じもの参照している様子
    assertFalse(items1 == fstClasse2.getItems());
  }

  @Test
  public void testFilterBy() {
    var fstClass = FirstClassCollection.ofWithCopyOf(List.of(new Item("1"), new Item("2")));
    var filterdClass = fstClass.filterBy("1");
    assertEquals(1, filterdClass.getItems().size());
    assertEquals("1", filterdClass.getItems().get(0).getValue());
  }

  @Test
  public void testApply() {
    var fstClass = FirstClassCollection.ofWithCopyOf(List.of(new Item("1"), new Item("2")));
    List<Item2> convertedItemList = fstClass.apply(item -> new Item2(item.getValue()));
    assertEquals(2, convertedItemList.size());
    assertEquals("1", convertedItemList.get(0).getValue());
    assertEquals("2", convertedItemList.get(1).getValue());
  }

  @Test
  public void testAccept() {
    var fstClass = FirstClassCollection.ofWithCopyOf(List.of(new Item("1"), new Item("2")));

    List<Item> items = new ArrayList<>();

    // 本来は標準出力などの副作用のあるロジックの実行を想定
    fstClass.accept(item -> items.add(item));
    assertEquals(2, items.size());
    assertEquals("1", items.get(0).getValue());
    assertEquals("2", items.get(1).getValue());
  }
}

参考

First Class Collection | BLOG - DeNA Engineering

ファーストクラスコレクションとは【デザインパターン】 - 正三雑記

さいごに

このクラス、ちょっと作り変えてファーストクラスの抽象クラスにして基底にしても良いかもしれない?

*1:ちなみに、僕だったらどうしてもという場合だったら後者を選びます。

Interfaceのdefaultで多重継承(日時編)

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

vermeer.hatenablog.jp

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

何が嬉しいの?

期間とか比較とか時間に関する操作をインスタンスメソッドで扱えるとUtilsの内容を知らなくても利用側は使えるのでちょっと嬉しいかな?というくらいの嬉しさです。

とくに期間についてはChronoUnitを使った方が良いケースとPeriodを使った方が良いケースと、やりたいことのイメージは似ているのに使用するクラスが違うというようなものを統一して扱いたいということを満たしてくれます。

基底となるInterface

日付(‘LocalDate‘)、日時(LocalDateTime)はあえて分けました。

数値でBigDecimalに寄せたように最も制度の細かい LocalDateTimeに寄せることも考えたのですが、後述の DateUnaryOperatorの操作を行うことを鑑みて分けることにしました。

プリミティブ的な型への変換を基底Interfaceの操作として定義します。

日本のアプリケーションを前提としているのでdefaultで指定しています。
具象クラスで

UnixTimeへの変換は、以前 フロントへ日時関連の情報を返却するときに「フロントで任意で表記変換をしたいのでUnixTimeの方が嬉しい」という話を受けたことがあったので*1

LocalDateを拡張

Dateへの変換はミドルウェアによってはLocalDateではなくDateというケースがあったので。

public interface NullableDateType<T> extends SinglePropertyObjectType<LocalDate> {

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

  /**
   * 保持する日時に適用するタイムゾーンIDを返却します.
   *
   * @return タイムゾーンID
   */
  default ZoneId getZoneId() {
    return TimeZone.getTimeZone("Asia/Tokyo").toZoneId();
  }

  /**
   * プロパティ値をDateへ変換します.
   *
   * @return Data型インスタンス
   */
  default Optional<Date> toDate() {
    if (this.isEmpty()) {
      return Optional.empty();
    }
    return Optional.of(Date.from(
            this.getNullableValue().get().atStartOfDay(this.getZoneId()).toInstant()));
  }

  /**
   * プロパティ値をLocalDateTimeへ変換します.
   *
   * @return LocalDateTime型インスタンス
   */
  default Optional<LocalDateTime> toLocalDateTime() {
    if (this.isEmpty()) {
      return Optional.empty();
    }
    return Optional.of(this.getNullableValue().get().atStartOfDay(this.getZoneId()).toLocalDateTime());
  }

  /**
   * プロパティ値をUnixTimeへ変換します.
   *
   * @return UnixTime
   */
  default Optional<Long> toUnixTime() {
    if (this.isEmpty()) {
      return Optional.empty();
    }
    var zonedDateTime = ZonedDateTime.of(this.getNullableValue().get().atStartOfDay(), this.getZoneId());
    return Optional.of(zonedDateTime.toEpochSecond());
  }
}

LocalDateTimeを拡張

public interface NullableDateTimeType<T> extends SinglePropertyObjectType<LocalDateTime> {

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

  /**
   * 保持する日時に適用するタイムゾーンIDを返却します.
   *
   * @return タイムゾーンID
   */
  default ZoneId getZoneId() {
    return TimeZone.getTimeZone("Asia/Tokyo").toZoneId();
  }

  /**
   * プロパティ値をLocalDateへ変換して返却します.
   *
   * @return LocalDate
   */
  default Optional<LocalDate> toLocalDate() {
    if (this.getNullableValue().isEmpty()) {
      return Optional.empty();
    }

    var dateTime = this.getNullableValue().get();
    return Optional.of(LocalDate.of(dateTime.getYear(), dateTime.getMonth(), dateTime.getDayOfMonth()));
  }

  /**
   * プロパティ値をUnixTimeへ変換します.
   *
   * @return UnixTime
   */
  default Optional<Long> toUnixTime() {
    if (this.isEmpty()) {
      return Optional.empty();
    }
    var zonedDateTime = ZonedDateTime.of(this.getNullableValue().get(), this.getZoneId());
    return Optional.of(zonedDateTime.toEpochSecond());
  }

}

編集汎用

LocalDateLocalDateTimeの日付操作は使い勝手が良いので、そのまま使うのが良いと思います。

LocalDateを拡張

public interface DateUnaryOperator<T> extends NullableDateType<T>, SingleArgumentNewInstance<LocalDate, T> {

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

実装例としてテスト
見てもらえれば分かるのですが、plusDaysは既に分かりやすいし使いやすいので単純な委譲相当で良さそうです。
強調の意味もあって ブロックで実装していますが 通常はブロックは使わなくて良いくらいシンプルです。

public class DateUnaryOperatorTest {

  @Test
  public void testApply() {
    var impl = new DateUnaryOperatorImpl(LocalDate.of(2024, 1, 1));
    var actual1 = impl.apply(value -> {
      return value;
    });

    Assertions.assertEquals(impl.getNullableValue().get(), actual1.getNullableValue().get());
    Assertions.assertTrue(impl != actual1);

    var actual2 = impl.apply(value -> {
      return value.plusDays(1);
    });

    Assertions.assertNotEquals(impl.getNullableValue().get(), actual2.getNullableValue().get());
    Assertions.assertEquals(2, actual2.getNullableValue().get().getDayOfMonth());

  }

  public static class DateUnaryOperatorImpl implements DateUnaryOperator<DateUnaryOperatorImpl> {

    private LocalDate value;

    public DateUnaryOperatorImpl(LocalDate value) {
      this.value = value;
    }

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

}

LocalDateTimeを拡張

public interface DateTimeUnaryOperator<T> extends NullableDateTimeType<T>,
        SingleArgumentNewInstance<LocalDateTime, T> {

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

実装例としてテスト

public class DateTimeUnaryOperatorTest {

  @Test
  public void testApply() {
    var impl = new DateTimeUnaryOperatorImpl(LocalDateTime.of(2024, 1, 1, 2, 3));
    var actual1 = impl.apply(value -> {
      return value;
    });

    Assertions.assertEquals(impl.getNullableValue().get(), actual1.getNullableValue().get());
    Assertions.assertTrue(impl != actual1);

    var actual2 = impl.apply(value -> {
      return value.plusDays(1);
    });

    Assertions.assertNotEquals(impl.getNullableValue().get(), actual2.getNullableValue().get());
    Assertions.assertEquals(2, actual2.getNullableValue().get().getDayOfMonth());
  }

  public static class DateTimeUnaryOperatorImpl implements DateTimeUnaryOperator<DateTimeUnaryOperatorImpl> {

    private LocalDateTime value;

    public DateTimeUnaryOperatorImpl(LocalDateTime value) {
      this.value = value;
    }

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

}

期間日数

LocalDateを拡張

public interface DateDaysRange<T extends NullableDateType<T>> extends NullableDateType<T> {

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間日数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default Long rangeDays(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0L;
    }
    Long count = ChronoUnit.DAYS.between(this.getNullableValue().get(), after.getNullableValue().get());
    return count;
  }

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param <R> 日数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 日数を扱うドメインオブジェクトを生成する関数
   * @return 期間日数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeDays(T after, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    Long count = ChronoUnit.DAYS.between(this.getNullableValue().get(), after.getNullableValue().get());
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param <R> 日数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの日数のドメインオブジェクトを生成する関数
   * @param function 日数を扱うドメインオブジェクトを生成する関数
   * @return 日数を扱うドメインオブジェクト
   */
  default <R> R rangeDays(T after, Supplier<R> defaultSupplier, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    Long count = ChronoUnit.DAYS.between(this.getNullableValue().get(), after.getNullableValue().get());
    return function.apply(count);
  }
}

LocalDateTimeを拡張

public interface DateTimeDaysRange<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T> {

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間日数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default Long rangeDays(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0L;
    }
    Long count = ChronoUnit.DAYS.between(
            this.getNullableValue().get().toLocalDate(),
            after.getNullableValue().get().toLocalDate());
    return count;
  }

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param <R> 日数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 日数を扱うドメインオブジェクトを生成する関数
   * @return 期間日数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeDays(T after, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    Long count = ChronoUnit.DAYS.between(
            this.getNullableValue().get().toLocalDate(),
            after.getNullableValue().get().toLocalDate());
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param <R> 日数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの日数のドメインオブジェクトを生成する関数
   * @param function 日数を扱うドメインオブジェクトを生成する関数
   * @return 日数を扱うドメインオブジェクト
   */
  default <R> R rangeDays(T after, Supplier<R> defaultSupplier, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    Long count = ChronoUnit.DAYS.between(
            this.getNullableValue().get().toLocalDate(),
            after.getNullableValue().get().toLocalDate());
    return function.apply(count);
  }
}

期間月数

ちょっとしたものとして、以前の開発で月を3分割にして刻んだ表現が欲しいというものがあったので それを作りました。
かなり特定のドメインに寄ったものなように思うのですが、まぁこういうのも

LocalDateを拡張

public interface DateMonthsRange<T extends NullableDateType<T>> extends NullableDateType<T> {

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間月数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default Long rangeMonths(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0L;
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    Long count = period.toTotalMonths();
    return count;
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 期間月数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeMonths(T after, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    Long count = this.rangeMonths(after);
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの月数のドメインオブジェクトを生成する関数
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 月数を扱うドメインオブジェクト
   */
  default <R> R rangeMonths(T after, Supplier<R> defaultSupplier, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    Long count = this.rangeMonths(after);
    return function.apply(count);
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param after 指定日
   * @return 月数(0.5単位での月数)
   */
  default BigDecimal rangeMonthsHalfUp(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return BigDecimal.ZERO;
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    var monthCount = period.toTotalMonths();
    var dayCount = period.minusMonths(monthCount).getDays();

    var halfUpDay = new BigDecimal(dayCount)
            .divide(new BigDecimal(30), 2, RoundingMode.HALF_UP)
            .multiply(new BigDecimal(2)).setScale(0, RoundingMode.HALF_UP)
            .divide(new BigDecimal(2));

    return new BigDecimal(monthCount).add(halfUpDay);
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 期間月数を扱うドメインオブジェクト(0.5単位での月数). thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeMonthsHalfUp(T after, Function<BigDecimal, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    BigDecimal count = this.rangeMonthsHalfUp(after);
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの月数のドメインオブジェクトを生成する関数
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 月数を扱うドメインオブジェクト(0.5単位での月数)
   */
  default <R> R rangeMonthsHalfUp(T after, Supplier<R> defaultSupplier, Function<BigDecimal, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    BigDecimal count = this.rangeMonthsHalfUp(after);
    return function.apply(count);
  }
}

LocalDateTimeを拡張

public interface DateTimeMonthsRange<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T> {

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間月数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default Long rangeMonths(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0L;
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    Long count = period.toTotalMonths();
    return count;
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 期間月数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeMonths(T after, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    Long count = period.toTotalMonths();
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの月数のドメインオブジェクトを生成する関数
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 月数を扱うドメインオブジェクト
   */
  default <R> R rangeMonths(T after, Supplier<R> defaultSupplier, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    Long count = period.toTotalMonths();
    return function.apply(count);
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param after 指定日
   * @return 月数(0.5単位での月数)
   */
  default BigDecimal rangeMonthsHalfUp(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return BigDecimal.ZERO;
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    var monthCount = period.toTotalMonths();
    var dayCount = period.minusMonths(monthCount).getDays();

    var halfUpDay = new BigDecimal(dayCount)
            .divide(new BigDecimal(30), 2, RoundingMode.HALF_UP)
            .multiply(new BigDecimal(2)).setScale(0, RoundingMode.HALF_UP)
            .divide(new BigDecimal(2));

    return new BigDecimal(monthCount).add(halfUpDay);
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 期間月数を扱うドメインオブジェクト(0.5単位での月数). thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeMonthsHalfUp(T after, Function<BigDecimal, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    BigDecimal count = this.rangeMonthsHalfUp(after);
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの月数のドメインオブジェクトを生成する関数
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 月数を扱うドメインオブジェクト(0.5単位での月数)
   */
  default <R> R rangeMonthsHalfUp(T after, Supplier<R> defaultSupplier, Function<BigDecimal, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    BigDecimal count = this.rangeMonthsHalfUp(after);
    return function.apply(count);
  }

}

期間年数

LocalDateを拡張

public interface DateYearsRange<T extends NullableDateType<T>> extends NullableDateType<T> {

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間年数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default int rangeYears(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0;
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    int count = period.getYears();
    return count;
  }

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param <R> 年数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 年数を扱うドメインオブジェクトを生成する関数
   * @return 期間年数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeYears(T after, Function<Integer, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    int count = period.getYears();
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param <R> 年数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの年数のドメインオブジェクトを生成する関数
   * @param function 年数を扱うドメインオブジェクトを生成する関数
   * @return 年数を扱うドメインオブジェクト
   */
  default <R> R rangeYears(T after, Supplier<R> defaultSupplier, Function<Integer, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    int count = period.getYears();
    return function.apply(count);
  }
}

LocalDateTimeを拡張

public interface DateTimeYearsRange<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T> {

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間年数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default int rangeYears(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0;
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    int count = period.getYears();
    return count;
  }

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param <R> 年数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 年数を扱うドメインオブジェクトを生成する関数
   * @return 期間年数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeYears(T after, Function<Integer, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    int count = period.getYears();
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param <R> 年数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの年数のドメインオブジェクトを生成する関数
   * @param function 年数を扱うドメインオブジェクトを生成する関数
   * @return 年数を扱うドメインオブジェクト
   */
  default <R> R rangeYears(T after, Supplier<R> defaultSupplier, Function<Integer, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    int count = period.getYears();
    return function.apply(count);
  }
}

日付比較

isAfterとかisBeforeよりも、数直線的な比較記述(less than的な記述)の方が境界が分かりやすいので数値比較と同じ操作の属性表現として揃えました。
やったこととしてはそれくらいです。

LocalDateを拡張

public interface DateComparator<T extends NullableDateType<T>> extends NullableDateType<T> {

  /**
   * equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this = other} を満たす場合 true
   */
  default boolean eq(T other) {
    if (this.getNullableValue().isEmpty() && other.getNullableValue().isEmpty()) {
      return true;
    }

    if (this.getNullableValue().isEmpty() || other.getNullableValue().isEmpty()) {
      return false;
    }

    return this.getNullableValue().get().isEqual(other.getNullableValue().get());
  }

  /**
   * not equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this != other} を満たす場合 true
   */
  default boolean ne(T other) {
    return !this.eq(other);
  }

  /**
   * less than.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this < other} を満たす場合 true
   */
  default boolean lt(T other) {
    if (this.eq(other)) {
      return false;
    }

    if (other.isEmpty()) {
      return false;
    }

    if (this.isEmpty()) {
      return true;
    }

    return this.getNullableValue().get().isBefore(other.getNullableValue().get());
  }

  /**
   * less than or equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this <= other} を満たす場合 true
   */
  default boolean le(T other) {
    return this.eq(other) || this.lt(other);
  }

  /**
   * greater than.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this > other} を満たす場合 true
   */
  default boolean gt(T other) {
    if (this.eq(other)) {
      return false;
    }

    if (other.isEmpty()) {
      return true;
    }

    if (this.isEmpty()) {
      return false;
    }

    return this.getNullableValue().get().isAfter(other.getNullableValue().get());
  }

  /**
   * greater than or equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this >= other} を満たす場合 true
   */
  default boolean ge(T other) {
    return this.eq(other) || this.gt(other);
  }

}

LocalDateTimeを拡張

public interface DateTimeComparator<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T> {

  /**
   * equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this = other} を満たす場合 true
   */
  default boolean eq(T other) {
    if (this.getNullableValue().isEmpty() && other.getNullableValue().isEmpty()) {
      return true;
    }

    if (this.getNullableValue().isEmpty() || other.getNullableValue().isEmpty()) {
      return false;
    }

    return this.getNullableValue().get().isEqual(other.getNullableValue().get());
  }

  /**
   * not equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this != other} を満たす場合 true
   */
  default boolean ne(T other) {
    return !this.eq(other);
  }

  /**
   * less than.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this < other} を満たす場合 true
   */
  default boolean lt(T other) {
    if (this.eq(other)) {
      return false;
    }

    if (other.isEmpty()) {
      return false;
    }

    if (this.isEmpty()) {
      return true;
    }

    return this.getNullableValue().get().isBefore(other.getNullableValue().get());
  }

  /**
   * less than or equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this <= other} を満たす場合 true
   */
  default boolean le(T other) {
    return this.eq(other) || this.lt(other);
  }

  /**
   * greater than.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this > other} を満たす場合 true
   */
  default boolean gt(T other) {
    if (this.eq(other)) {
      return false;
    }

    if (other.isEmpty()) {
      return true;
    }

    if (this.isEmpty()) {
      return false;
    }

    return this.getNullableValue().get().isAfter(other.getNullableValue().get());
  }

  /**
   * greater than or equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this >= other} を満たす場合 true
   */
  default boolean ge(T other) {
    return this.eq(other) || this.gt(other);
  }

}

日付補正

LocalDateを拡張

月初日、月末日にシフトしたインスタンスを生成します。

public interface DateMonthsShift<T extends NullableDateType<T>> extends NullableDateType<T>,
        NoArgumentNewInstance<T>, SingleArgumentNewInstance<LocalDate, T> {

  /**
   * 月初日補正をしたインスタンスを返却します.
   *
   * @return 月初日補正をしたインスタンス
   */
  default T beginMonth() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    LocalDate updated = this.getNullableValue().get().withDayOfMonth(1);
    return this.newInstance(updated);
  }

  /**
   * 月末日補正をしたインスタンスを返却します.
   *
   * @return 月末日補正をしたインスタンス
   */
  default T endMonth() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    var date = this.getNullableValue().get();
    var updated = this.getNullableValue().get().withDayOfMonth(date.lengthOfMonth());
    return this.newInstance(updated);
  }

}

LocalDateTimeを拡張

月初日、月末日にシフトしたインスタンスを生成します。

public interface DateTimeMonthsShift<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T>,
        NoArgumentNewInstance<T>, SingleArgumentNewInstance<LocalDateTime, T> {

  /**
   * 月初日補正をしたインスタンスを返却します.
   *
   * @return 月初日補正をしたインスタンス
   */
  default T beginMonth() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    LocalDateTime updated = this.getNullableValue().get().toLocalDate().atStartOfDay().withDayOfMonth(1);
    return this.newInstance(updated);
  }

  /**
   * 月末日補正をしたインスタンスを返却します.
   *
   * @return 月末日補正をしたインスタンス
   */
  default T endMonth() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    var date = this.getNullableValue().get().toLocalDate().plusMonths(1L)
            .atStartOfDay().withDayOfMonth(1).minusNanos(1L);
    return this.newInstance(date);
  }

}

1日の開始時刻および終了時刻にシフトしたインスタンスを生成します。

public interface DateTimeDaysShift<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T>,
         NoArgumentNewInstance<T>, SingleArgumentNewInstance<LocalDateTime, T> {

  /**
   * 1日の開始時刻補正をしたインスタンスを返却します.
   *
   * @return 開始時刻補正をしたインスタンス
   */
  default T beginDay() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    LocalDateTime updated = this.getNullableValue().get().toLocalDate().atStartOfDay();
    return this.newInstance(updated);
  }

  /**
   * 1日の終了時刻補正をしたインスタンスを返却します.
   *
   * @return 終了時刻補正をしたインスタンス
   */
  default T endDay() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    var updated = this.getNullableValue().get().toLocalDate().plusDays(1).atStartOfDay().minusNanos(1);
    return this.newInstance(updated);
  }

}

さいごに

期間取得に微妙な違いがあったりと、やってみて分かったことがありました。
Interfaceのdefaultを使ったとはいえ、実質Utilsの再発明的なことをしたわけですが、こういうもの素振りとして色々と学びがあって良いものですね。

もう少し、こういうのもやってみようかな?というのがあったりしますが それはまた別の機会にしようと思います。

*1:タイムゾーンのところは、正式に(?)使う時は、ちゃんと確認した方が良いかもしれない

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:もちろん、業務内容によっては小数ありもあると思います

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:要はバランス

Interfaceのdefaultで多重継承(文字列編)

はじめに

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

vermeer.hatenablog.jp

今回はInterfaceのdefaultを使った具体的な実装編の1つ目「文字列」です。

何が嬉しいの?

Nullableであることを考慮した実装はやりたいことは単純なのに冗長になりがちです。

// 意図としてはリクエストから受け取った文字列がNullだった場合
String str = null;

// NullPointerException
str.contains("hoge");

Stringをプロパティにするクラスにすると

// nullを保持している
Text text = new Text();

// NullPointerExceptionにならないような実装をしておけばちょっと嬉しい
text.contains("hoge");

だいたい、このくらいの嬉しさです。

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

文字列に関する操作を行うユーティリティは沢山ありますが、インスタンスのメソッドとして使えるとユーティリティクラスから目的に合ったものを探すよりも楽ができます。
実装例はTextという汎用文字列クラスしか準備していないため全部入り状態ですが、用途を特化したドメインオブジェクトにしたい場合(例えば文字列の結合は不要)は、そのInterfaceをimplements から外すだけです。

実装クラスが担うのは

  • インスタンスの生成ルール
  • equalsなどのオブジェクトととしてやるべき必須の実装
  • 基本操作を超えたドメイン独自の文字列編集操作

だけを記述すれば良くなります。
単純な文字列結合のような「無味乾燥な委譲」は、Interfaceをimplements をするだけでオシマイです。

Nullの許容と文字列の基本型

StringがNullableであること、文字列を保持している(もしくはしていない)、Nullだったときにちょっとだけ便利なもの、そのくらいのところを基底のインターフェースとして作ります。

public interface NullableTextType<T> extends SinglePropertyObjectType<String> {

  /**
   * 保持している文字列を返却します.
   *
   * @return <code>null</code>の場合は空文字
   */
  default String getOrDefault() {
    return this.getNullableValue().orElse("");
  }

  /**
   * 文字列が空文字またはNullであることを判定します.
   *
   * @return 空文字またはnullの場合はtrue
   */
  default boolean isEmpty() {
    return Objects.equals("", this.getNullableValue().orElse(""));
  }

  /**
   * 文字列の保持を判定します.
   *
   * @return 空文字または<code>null</code>以外の場合はtrue
   */
  default boolean hasText() {
    return !this.isEmpty();
  }

  /**
   * 対象の文字列が含まれるか判定をします.
   *
   * @param <U> 検査対象のクラスの型
   * @param other 検査文字列
   * @return 検査文字列が含まれる場合はtrue<br>自インスタンスまたは引数で指定したインスタンスが空文字またはnullの場合はfalse
   */
  default <U extends NullableTextType<T>> boolean contains(U other) {
    if (this.isEmpty() || other.isEmpty()) {
      return false;
    }

    var result = this.getNullableValue().get().contains(other.getNullableValue().get());
    return result;
  }

}

編集汎用

文字列を編集して新しいインスタンスを生成するInterfaceです。
Immutableを常に担保してくれます。
一番はじめに掲載していますが、実際は一番最後に作成しました。 Stringクラス自体のメソッドを使った編集をしたい場合に使ったりするケースもあるかな?ということで作りました。
あまり有意義な利用シーンは無かったりするかもしれませんが各基底インターフェースのペアとして準備しておいた方が良いかもしれないということで作りました。

public interface TextUnaryOperator<T> extends NullableTextType<T>, SingleArgumentNewInstance<String, T> {

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

文字列結合

Nullableを前提としているオブジェクトの文字列結合や地味ですが冗長なコードを書く必要があります。
そのあたりをちょっと楽できます。

public interface TextJoin<T extends NullableTextType<T>> extends NullableTextType<T>, SingleArgumentNewInstance<String, T> {

  /**
   * 文字列を結合したインスタンスを返却します.
   *
   * @param delimiter 区切り文字
   * @param appendText 連結文字
   * @return 文字列を結合した文字インスタンス
   */
  @SuppressWarnings("unchecked")
  default T join(T delimiter, T... appendText) {
    List<String> strList = new ArrayList<>();
    strList.add(this.getNullableValue().orElse(""));

    List<String> appendStrList = Stream.of(appendText).map(e -> e.getNullableValue().orElse("")).collect(Collectors.toList());
    strList.addAll(appendStrList);

    var delimiterStr = Objects.isNull(delimiter) ? "" : delimiter.getNullableValue().orElse("");

    var str = String.join(delimiterStr, strList);
    return this.newInstance(str);
  }

  /**
   * 文字列を結合したインスタンスを返却します.
   *
   * @param appendText 連結文字
   * @return 文字列を結合した文字インスタンス
   */
  @SuppressWarnings("unchecked")
  default T concat(T... appendText) {
    return this.join(null, appendText);
  }
}

文字列長

文字列長の取得は「全角2バイト」というわけにはいきません。
以下のロジックも万能ではなくて、絵文字などの文字列は正しく取得できません。
ちゃんとした文字列長を取りたい場合は、YAVIなどのライブラリをうまく活用したり、新しいJavaのバージョンだとより良い方法があったりしますので、そのあたりを踏まえて書き換えると良いでしょう。
少なくとも、各ロジックでは このInterfaceを使う事で文字列長を取得するということについては水平展開が実現することを保証してくれます。

public interface TextLength<T> extends NullableTextType<T> {

  /**
   * 保持している文字数を返却します.
   * <p>
   * サロゲートペアを考慮した文字数を返却します.
   * </p>
   *
   * @return 保持している文字数を返却します.<code>null</code>を保持している場合は 0 を返却します.
   */
  default int length() {
    if (this.getNullableValue().isEmpty()) {
      return 0;
    }

    BreakIterator iterator = BreakIterator.getCharacterInstance();
    iterator.setText(this.getNullableValue().orElse(""));

    int current = iterator.next();
    int count = 0;
    while (current != BreakIterator.DONE) {
      count++;
      current = iterator.next();
    }
    return count;
  }
}

文字列の長さが欲しいというときは、Nullの場合もとりあえず0バイトとして処理をしたいケースが多いのではないでしょうか?
そんなちょっとしたところをカバーしてます。

改行コードの除去

いるかなぁと思って作ってみたんですが、、、
かつてこれが欲しいシチュエーションがあったような記憶があったのですが、あんまり使わないかもしれないです。

public interface TextRemoveReturn<T> extends NullableTextType<T>, InstanceCreator<T> {

  /**
   * キャリッジリターンを除去したインスタンスを返却します.
   *
   * @return キャリッジリターンを除去した文字インスタンス
   */
  default T removeReturn() {
    if (this.getNullableValue().isEmpty()) {
      return this.newInstanceFromThis();
    }

    var replaced = this.getNullableValue().orElse("").replaceAll("\\r\\n|\\n", "");
    return this.newInstanceFromThis(replaced);
  }
}

ゼロ埋め

どちらかというと数値型のInterface側に欲しかったかもしれない。。。

public interface TextZeroPadding<T> extends NullableTextType<T>, SingleArgumentNewInstance<String, T> {

  /**
   * 前ゼロ埋めをした文字列を返却します.
   * <p>
   * 値を保持していない場合もゼロ埋めして桁を保証します.
   * </p>
   *
   * @param length ゼロ埋めを含めた文字列桁数
   * @return 文字列を前ゼロ埋めした文字インスタンス
   */
  default T zeroPadding(Integer length) {
    var format = "%" + length + "s";
    var replaced = String.format(format, this.getNullableValue().orElse(""))
            .replace(" ", "0");
    return this.newInstance(replaced);
  }
}

文字列の部分取得

文字列の部分取得も実行時エラーがないように、ちょっとした手間を施して、ちょっと楽ができます。

public interface TextSubstring<T extends NullableTextType<T>> extends TextLength<T>, NoArgumentNewInstance<T>, SingleArgumentNewInstance<String, T> {

  /**
   * 文字列の部分文字列である文字のインスタンスを返却します.
   *
   * @param beginIndex 開始位置
   * @return 取得したインスタンス. 開始位置がマイナスの場合は空インスタンス. 開始位置が文字列長を超えていた場合は同じ値のインスタンス.
   */
  default T substring(int beginIndex) {
    return this.substring(beginIndex, Integer.MAX_VALUE);
  }

  /**
   * 文字列の部分文字列である文字のインスタンスを返却します.
   *
   * @param beginIndex 開始位置
   * @param endIndex 終了位置
   * @return 取得したインスタンス. 開始位置がマイナスの場合または終了位置が開始位置よりも起きい場合は空インスタンス. 開始位置が文字列長を超えていた場合は同じ値のインスタンス.
   */
  default T substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
      return this.newInstance();
    }

    int length = this.length();
    int endIndexPos = length < endIndex ? length : endIndex;

    if (endIndexPos < beginIndex) {
      return this.newInstance();
    }

    if (beginIndex == 0 && length <= endIndex) {
      return this.newInstance(this.getNullableValue().get());
    }

    BreakIterator iterator = BreakIterator.getCharacterInstance();
    iterator.setText(this.getNullableValue().orElse(""));

    var strArray = new ArrayList<String>();
    for (int start = iterator.first(), end = iterator.next();
            end != BreakIterator.DONE; start = end, end = iterator.next()) {
      String str = this.getNullableValue().orElse("").substring(start, end);
      strArray.add(str);
    }

    var subList = strArray.subList(beginIndex, endIndexPos);
    var subStr = String.join("", subList);
    return this.newInstance(subStr);
  }

}

NotEmptyText

これはInterfaseではなくてクラスです。
具体的な宣言の書き方と NotEmpty(NonNull)のクラスの実装例として。
Nullを許容しない文字列オブジェクトの宣言のやり方はNullableTextType<T>の実装に対して上書きするように Nullを許容しないInterface(NotEmptyType)を重ねるイメージで宣言をするだけです。

getNullableValueがOptional.empty()を返却することは無いので、使っても害はないですが意味もないものになります。
ちょっとこういうのは残ってしまいますが、このくらいといえば このくらいですね。

public final class NotEmptyText implements NotEmptyType<String>,
        TextJoin<NotEmptyText>, TextZeroPadding<NotEmptyText>, TextRemoveReturn<NotEmptyText>,
        TextSubstring<NotEmptyText>, TextUnaryOperator<NotEmptyText>, Serializable {

  private static final long serialVersionUID = 1L;

  private final String value;

  private NotEmptyText() {
    // 文字列結合の元インスタンスとなるため空文字を初期値として設定します.
    this.value = "";
  }

  private NotEmptyText(String value) {
    // Nullの保持を許容しません.
    if (Objects.isNull(value)) {
      throw new NullPointerException();
    }

    this.value = value;
  }

  /**
   * インスタンスを生成します.
   *
   * @param value
   * @return 生成したインスタンス
   */
  public static NotEmptyText of(String value) {
    return new NotEmptyText(value);
  }

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

  @Override
  public NotEmptyText newInstance(String value) {
    return new NotEmptyText(value);
  }

  /**
   * Nullを許容していないクラスのため、本メソッドを使用する意味はありません.
   *
   * @return プロパティの値
   */
  @Override
  public Optional<String> getNullableValue() {
    return Optional.of(value);
  }

  @Override
  public String getValue() {
    return this.value;
  }

(略)
  • 引数無しコンストラクタはprivate*1
  • createNoValueといったNullableインスタンスを生成するstaticメソッドも準備しない
  • 文字列結合で引数無しのコンストラクタをリフレクションでアクセスするので、そのための対応はやる必要あり

最後の引数無しのコンストラクタのところが、どうしても暗黙知というか作法的には避けられないところになりますが、それ以外は、そこまで酷くはないように思います。

さいごに

TextUnaryOperatorがあったら、immutableインスタンス生成メソッドにして結局なんでも出来るのでは?という批判(?)は恐らく正しいです。
どちらかというと、汎用Textで実装を進めていきつつ、ドメイン特化のクラスを作る過程で「いったん未整理で、でも後から見直す」というマーカーのように使えば良いかな?と思ったりします。
「ガチガチにしてしまうのは良いけれどそれだと使えないから Stringをそのまま使うようになってしまう」というくらいなら、TextUnaryOperatorを使っているところが特化すべきロジックの場所として、後々注目をするというのでも良いのかな?という感じです。

あと、アルアルのはずの文字列置換を作り忘れていました。。未来、必要になった時に作る最有力候補にしようと思います。

次回は数値操作の拡張をテーマにした「数値編」を予定。

*1:InstanceCreatorで使用するためデフォルトコンストラクタが必要