デトフィア

プログラミング

Mapのunionを行い、keyが重複していたら例外とする

org.apache.commons.collections4.CollectionUtilsは便利な上にソースコードも優しい。 例えばaddIgnoreNullメソッドは安全にnull以外の要素をコレクションに追加してくれる

public static <T> boolean addIgnoreNull(final Collection<T> collection, final T object) {
    if (collection == null) {
        throw new NullPointerException("The collection must not be null");
    }
    return object != null && collection.add(object);
}

以下のように使います

@Test
public void test_collection_utils_apache(){
    List<String> names = new ArrayList<>();
    names.add("nameA");
    names.add("nameB");
    CollectionUtils.addIgnoreNull(names,"nameC");
    CollectionUtils.addIgnoreNull(names,null);
    assertEquals(3,names.size());
}

nullを渡してもそれは追加されずにスルーされます。 当然ですがジェネリクスを利用しているので型が異なる要素も入れることはできません

次にuinonでコレクション同士の結合を行ないます

@Test
public void test_collection_utils_apache(){
    List<String> names = new ArrayList<>();
    names.add("nameA");
    names.add("nameB");

    List<String> tags = new ArrayList<>();
    tags.add("tagA");
    tags.add("tagB");

    Collection<String> newList = CollectionUtils.union(names,tags);
    
    newList.stream().forEach(System.out::println);
    assertEquals(4,newList.size());
}

このように便利なメソッドがいくつも用意されています

今回はこのようなコレクションに対する機能としてMapのunionを作成します。 MapにはputAllというメソッドがありますが、これは同一キーがあった場合は上書きされるのとvoidメソッドであるという特徴があります

Map<String,String> mapA = new HashMap<>();
mapA.put("keyA","valueA");
mapA.put("keyB","valueB");
mapA.put("keyC","valueC");

Map<String,String> mapB = Map.of(
        "keyA","valueC",
        "keyD","valueD",
        "keyE","valueE"
);
mapA.putAll(mapB);
  • mapAをofで作っていないのは、ofで作ると要素数が固定になるからです

今回作成するunion機能はこんな感じでシンプルです

  • 戻り値は新しいMapオブジェクト
  • 同一keyのものがあったら例外を吐く
public static <T,R> Map<T,R> union(Map<T,R> mapa, Map<T,R> mapb){
    for(T key : mapa.keySet()){
        if(mapb.containsKey(key)){
            throw new RuntimeException("重複したkeyが存在します");
        }
    }
    Map<T,R> result = new HashMap<>();
    result.putAll(mapa);
    result.putAll(mapb);
    return result;
}

テストしてみましょう

@Test
public void test_my_map_ng(){
    Map<String,String> mapA = new HashMap<>();
    mapA.put("keyA","valueA");
    mapA.put("keyB","valueB");
    mapA.put("keyC","valueC");
    Map<String,String> mapB = Map.of(
            "keyA","valueC",
            "keyD","valueD",
            "keyE","valueE"
    );
    assertThrows(RuntimeException.class, () -> MyMapUtils.union(mapA,mapB));
}

@Test
public void test_my_map_ok(){
    Map<String,String> mapA = Map.of(
            "keyA","valueC",
            "keyB","valueD",
            "keyC","valueE"
    );
    Map<String,String> mapB = Map.of(
            "keyD","valueC",
            "keyE","valueD",
            "keyF","valueE"
    );
    Map<String,String> newMap = MyMapUtils.union(mapA,mapB);
    assertEquals(6,newMap.size());
}

Java 関数型プログラミング

以下のプロジェクトはSpring公式のプロジェクトなので勉強には最適です

GitHub - spring-projects/spring-petclinic: A sample Spring-based application

このプロジェクトのprocessNewVisitFormの処理を関数型プログラミングで置き換えていきます

Add Visitの挙動確認

AddVisitを押した時の動作です

  • Visitはth:objectでフォームの内容がバリデーションされた状態でバインドされています
  • Add Visitボタンを押下した時点でOwnerの情報が引数にバインドされています
  • petIdはhidden項目で送信されます

この中で不自然なのはOwnerの情報が引数にバインドされているところですが、loadPetWithVisitが走っています

確認箇所

Ownerが保持している特定のpetに対してvisitオブジェクトを設定して保存する処理ですが、それは以下のメソッドです

public Owner addVisit(Integer petId, Visit visit) {

    Assert.notNull(petId, "Pet identifier must not be null!");
    Assert.notNull(visit, "Visit must not be null!");

    Pet pet = getPet(petId);

    Assert.notNull(pet, "Invalid Pet identifier!");

    pet.addVisit(visit);

    return this;
}

この中のPet pet = getPet(petId);で利用されているgetPetメソッドはifがネストしており短いコードなのに複雑になってしまっています

public Pet getPet(Integer id) {
    for (Pet pet : getPets()) {
        if (!pet.isNew()) {
            Integer compId = pet.getId();
            if (compId.equals(id)) {
                return pet;
            }
        }
    }
    return null;
}

nullを返すというバッドパターンのまま、ストリームを利用した処理に書き変えます

public Pet getPet(Integer id) {
    Optional<Pet> optpet = getPets().stream()
        .filter(pet -> !pet.isNew())
        .filter(pet -> pet.getId().equals(id))
        .findFirst();

    if(optpet.isPresent()){
        return optpet.get();
    }
    return null;
}

ここからif文を更に取り除いてみます

public Pet getPet(Integer id) {
    Optional<Pet> optpet = getPets().stream()
        .filter(pet -> !pet.isNew())
        .filter(pet -> pet.getId().equals(id))
        .findFirst();
    return optpet.orElse(null);
}

orElseは値が存在した場合はその値を返して、そうでない場合は引数の値を返します 他にも例外を扱えるorElseThrowなどがあります

もしもOptionalを使った設計にしていればコードはもっとキレイになります

public Optional<Pet> getPet(Integer id) {
    return  getPets().stream()
        .filter(pet -> !pet.isNew())
        .filter(pet -> pet.getId().equals(id))
        .findFirst();
}

filterの中で!を使うのに疑問を持った場合、Predicate.notが利用できます

大きな処理

最初に見た処理はドメインオブジェクトであるOwnerが自身の所持しているpetに対してVisitを付与する処理です その際にAssertで簡易チェックを行っています。

public Owner addVisit(Integer petId, Visit visit) {

    Assert.notNull(petId, "Pet identifier must not be null!");
    Assert.notNull(visit, "Visit must not be null!");

    Pet pet = getPet(petId);

    Assert.notNull(pet, "Invalid Pet identifier!");

    pet.addVisit(visit);

    return this;
}

本来はドメインモデルの簡単な検証なのですが、 Assertの処理をバリデーション処理として捉えてインターフェースを作って使ってみました

public Owner addVisit(Integer petId, Visit visit) {
    Validation<Integer> petIdNotNull = new ValidationImpl<>(
        _petId -> Objects.nonNull(_petId), "Pet identifier must not be null!"
    );
    petIdNotNull.check(petId);

    Validation<Visit> visitNotNull = new ValidationImpl<>(
        _visit -> Objects.nonNull(_visit), "Visit must not be null!"
    );
    visitNotNull.check(visit);
    // 新しいチェックを追加してみる
    Validation<Visit> descriptionNotBlank = new ValidationImpl<>(
        _vist -> !_vist.getDescription().isBlank(), "Description must not be blank!"
    );
    descriptionNotBlank.check(visit);

    Pet pet = getPet(petId);

    // ValidationImplを継承させることもできる
    Validation<Pet> petNotNull = new NotNullValidation<>(
        () -> pet,"pet");
    petNotNull.check(pet);

    pet.addVisit(visit);

    return this;
}

以前より大分コード量が増えてしまいましたが、いったんそれは置いといて、インターフェースと実装クラスは以下のようにしています

public interface Validation<T> {
    void check(T target);
}
public class ValidationImpl<T> implements Validation<T>{

    private final Predicate<T> predicate;

    private final String errorMessage;

    public ValidationImpl(Predicate<T> predicate,String errorMessage){
        this.predicate = predicate;
        this.errorMessage = errorMessage;
    }

    @Override
    public void check(T target) {
        if(this.predicate.test(target)){
            // todo set valid result
        } else {
            // todo set invalid result
            throw new RuntimeException(this.errorMessage);
        }
    }
}
public class NotNullValidation<T> extends ValidationImpl<T>{
    public NotNullValidation(Supplier<T> supplier, String filedName) {
        super( target -> Objects.nonNull(supplier.get()), filedName + " must not be null!");
    }
}

この辺のバリデーションクラスは参考サイトが丁寧に実装してあります ここでは簡易的に作っています

バリデーション用のクラスを一個一個作成しなくても、関数さえ渡してしまえばバリデーションを実装することができます 例えば今回はDescriptionの空白文字に関するバリデーションを追加しています

もちろんAssert文を使っている方が簡潔ですが、チェック項目が増えてきたりした時や、確認観点が複雑になってくると対応が厳しくなってしまいます

addVisitメソッドは別途privateな関数を定義してしまえばキレイになります

public Owner addVisit(Integer petId, Visit visit) {
    this.doGetPetValidation(petId,visit);
    Pet pet = getPet(petId);

    this.doAddVisitValidation(pet);
    pet.addVisit(visit);

    return this;
}

これはドメインモデルの簡易チェック的なものなのでバリデーションクラスのようなものを設置することはないかもしれませんが 実際の検証を行う処理を関数として渡すことができるのは、とてもスピーディーで管理も楽です。

より詳細なことは参考サイトのコードを見てみてください。

その他メモ

  • FindOwnerボタン押下
    • processFindForm
  • OwnerのNameリンクボタン押下
    • showOwner
      • add petした時もオーナー詳細画面を開くので実行される

参考

GitHub - prashantbasawa/simple-validation-framework: A simple validation framework to validate domain objects using Java 8 features