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

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

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が使えれば良いのですが無いものは仕方ないので…