はじめに
コードの全量のリンクをこちらの記事に書いているので、先読みで全量を見たい方はこちらを参照してください。
こちらでファーストクラスコレクションを扱う実装例を考えてみました。
少し工夫をすれば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が使えれば良いのですが無いものは仕方ないので…