ArchUnit 실천 : 집약 조작 전용의 리포지토리(나 DAO)에 의해서만, 집약이 영속화되는 것을 강제하는 ①<개별 ver.>

// 実行環境
* AdoptOpenJDK 11.0.9.1+1
* JUnit 5.7.0
* ArchUnit 0.14.1

아키텍처 테스트 동기 부여



집계를 구성하는 오브젝트는, 데이터베이스 등의 영속화층으로부터, 개별적으로 참조나 갱신하는 것이 아니라, 집약 루트를 기점으로서 집약(객체의 정합)으로서의 일관성을 유지하면서, 참조나 갱신하고 싶다.

아키텍처 테스트 구현



테스트 대상의 집계와 클래스의 샘플은 후술.
package com.example;
 
import com.example.domain.order.DenpyoAggregateDao;
import com.example.domain.order.DenpyoDao;
import com.example.domain.order.MeisaiDao;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.theClass;

class ArchitectureTest {

    // 検査対象のクラス
    private static final JavaClasses CLASSES =
            new ClassFileImporter()
                    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                    .importPackages("com.example");

    @ParameterizedTest
    @ValueSource(classes = {
        // 集約を構成する各エンティティに対応するDAO
        DenpyoDao.class,
        MeisaiDao.class
    })
    void 集約を構成する各エンティティに対応するDAOは集約操作専用のDAOによってのみ操作される(
        final Class<?> daoClass
    ) {
        // 集約操作専用のDAO
        Class<DenpyoAggregateDao> aggregateDaoClass = DenpyoAggregateDao.class;

        theClass(daoClass)
            .should()
            .onlyBeAccessed()
                .byClassesThat()
                .haveFullyQualifiedName(aggregateDaoClass.getName())
            .check(CLASSES);
    }
}

(참고) 테스트 대상 집계 샘플



다음과 같은 집계가 있다고 가정한다.



엔티티의 식별자를 나타내는 값 객체
public final class Identity<ENTITY> {
    //...
}

문서 엔티티와 Dao
@Entity
public class Denpyo {
    @Id
    Identity<Denpyo> id;

    @Transient
    List<Meisai> meisaiList;
}

public interface DenpyoDao {
    Optional<Denpyo> findById(Identity<Denpyo> id);
    int insert(Denpyo denpyo);
    int update(Denpyo denpyo);
}

품목 엔티티와 Dao
@Entity
public class Meisai {
    @Id
    Identity<Meisai> id;

    Identity<Denpyo> denpyoId;
}

public interface MeisaiDao {
    List<Meisai> findByDenpyoId(Identity<Denpyo> denpyoId);
    int[] insert(List<Meisai> meisaiList);
    int[] update(List<Meisai> meisaiList);
}

문서 및 품목을 집계로 조작하기 Aggregate Dao
public class DenpyoAggregateDao {

    private final DenpyoDao denpyoDao;
    private final MeisaiDao meisaiDao;

    public DenpyoAggregateDao(final DenpyoDao denpyoDao, final MeisaiDao meisaiDao) {
        this.denpyoDao = denpyoDao;
        this.meisaiDao = meisaiDao;
    }

    // 集約の取得
    Optional<Denpyo> findById(final Identity<Denpyo> id) {
        return denpyoDao.findById(id).map(denpyo -> {
            denpyo.meisaiList = meisaiDao.findByDenpyoId(denpyo.id);
            return denpyo;
        });
    }

    // 集約の登録
    @Transactional
    void insert(final Denpyo denpyo) {
        assert denpyo.meisaiList != null;

        denpyoDao.insert(denpyo);
        // `伝票ID`がデータベース等により自動採番される場合はその値を`明細.伝票ID`に反映する
        // denpyo.meisaiList.forEach(meisai -> meisai.denpyoId = denpyo.id);
        meisaiDao.insert(denpyo.meisaiList);
    }

    // 集約の更新
    @Transactional
    void update(final Denpyo denpyo) {
        assert denpyo.meisaiList != null;

        denpyoDao.update(denpyo);
        meisaiDao.update(denpyo.meisaiList);
    }
}

좋은 웹페이지 즐겨찾기