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

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

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で使用するためデフォルトコンストラクタが必要