ArchUnit

ArchUnit

Yazılım projelerinin başlangıç aşamasında alınan tasarım kuralları nettir. Bu aşamada mimari tasarımlar çizeriz, diyagramları çıkartırız, hangi bileşeni nereye koyacağımızı kolayca söyleyebilir durumda oluruz. Yeni projeye başlama hevesiyle, dikkatli bir şekilde aldığımız bu tasarım kurallarını uygularız.

Ancak bilindiği üzere yazılım geliştirme projelerinin karmaşıklıkları giderek artmakta ve büyümektedir. Proje büyüdükçe, ekibe yeni kişiler katıldıkça ya da ekipteki kişiler projeden ayrıldıkça bu tasarım kuralları sadece eskiden dokümante edilen bir sayfada kalabilir.

Projelerin sürdürülebilirliği, bakımı ve geliştirilebilirliği, doğru bir yazılım mimarisi oluşturmak ve bu mimariyi bozmadan aynı standartlarla devam ettirmekle başlar. Bunu sağlamanın yolu ise yazılan kodların, belirlenen mimari tasarım kurallarına uygunluğundan emin olmaktır. İş kurallarını kontrol etmek için nasıl birim ve entegrasyon testleri yazıyorsak, mimari tasarım kurallarımızı ihlal edip etmediğimizi kontrol etmek için de mimari testler yazmalıyız. Bu konuda ArchUnit, mimari tasarım kurallarını test edebilmemiz için önemli ve etkin bir araç olarak karşımıza çıkıyor.

ArchUnit’in Temel Kavramları

ArchRule

“ArchRule” ArchUnit’in temelini oluşturur. Yazılım mimarisi üzerinde kontrol etmek istediğimiz kuralları tanımlamak için kullanırız. Bu kurallar, projemizin uzun vadeli istikrarını korumak ve beklenmeyen değişikliklerin önüne geçmek için oluşturulur. Mesela, belirli bir paketteki sınıfların belirli bir süper sınıfa sahip olmasını istiyorsak;

1
2
3
4
5
   ArchRule superClassRule = ArchRuleDefinition.classes()
            .that()
            .resideInAPackage("com.example.myapp")
            .should()
            .beAssignableTo(MySuperClass.class); 

Code Unit

Yazılım mimarisini incelemek ve belirli sınıfların, arayüzlerin, metotların veya paketlerin üzerinde kontrol sağlamak için kullanırız. Bu aracı kullanarak yazılım projelerinin belirli parçalarını hedef alabiliriz. Örneğin, belirli bir paketteki sınıfları veya belirli bir işlevi yerine getiren sınıfları seçmek istiyorsak “Classes” kullanılırız.

1
2
classes().that().resideInAPackage("..ortak..")
    .should().onlyHaveDependentClassesThat().resideInAPackage("..ortak..")

Conditions

ArchUnit kütüphanesinde kullanılan ArchRule içinde tanımlanan kuralları ve koşulları ifade eder. Bu kurallar, sınıflar arasındaki ilişkileri, bağımlılıkları ve mimari kısıtlamaları belirlemek için kullanılır. “onlyHaveDependentClassesThat()” ifadesi, bir sınıfın yalnızca belirli başka sınıflara bağımlı olması gerektiğini ifade eder. Örneğin; “UserService” sınıfına yalnızca “com.example.myapp.service” paketi tarafından erişilebilir.

1
2
3
4
5
6
 ArchRule userServiceDependencyRule = classes()
            .that()
            .haveSimpleName("UserService")
            .should()
            .onlyHaveDependentClassesThat()
            .resideInAPackage("com.example.myapp.service");

Bu tür koşullar, projenizin mimarisini ve sınıf ilişkilerini daha iyi kontrol etmemize ve korumamıza yardımcı olur.

Predicates

ArchUnit kurallarının oluşturulmasında kullanılan koşulları belirleyen ifadelerdir. Bu koşullar, sınıfları veya paketleri belirlemek için kullanılır. Örneğin,”resideInAPackage()” ifadesini kullanarak belirli bir paketi kural uygulamak için seçebiliriz.

1
2
3
4
  ArchRule classesInPackageRule = classes()
            .that()
            .resideInAPackage("com.example.myapp.service")
            ...

ArchUnit’in Temel Özellikleri

1.Kodlama Standartları : Projelerimizde belirlediğimiz kodlama standartlarımıza uymayan sınıf veya metotlar için ArchUnit kullanarak kontroller yapabiliriz.

Örneğin; ‘port.input’ paketi altındaki bütün interfacelerin isimlerinin ‘InputPort’ ile bitmesini istiyorsak aşağıdaki gibi bir kural ekleyebiliriz.

1
2
3
4
classes()
         .that().resideInAPackage("..port.input..")
         .and().areInterfaces()
         .should().haveNameMatching(".*InputPort");

Belirli bir anotasyonla işaretlenmiş olan sınıfların isminde bir kontrole ihtiyacımız varsa, örneğin service paketi altında ‘MyService’ anotasyonu ile işaretlenmiş sınıfların ismi ‘Service’ ile başlasın istiyorsak;

1
2
3
4
classes()
         .that().resideInAPackage("..service..")
         .and().areAnnotatedWith(MyService.class)
         .should().haveSimpleNameStartingWith("Service");

2.Katmanlı Mimari Kontrolü : ArchUnit, katmanlar arası etkileşim kurallarını belirlemek ve bunların uygunluğunu kontrol etmek amacıyla kullanıma sunulan bir araçtır. Mesela, ‘service’ katmanına sadece ‘service’ ve ‘controller’ katmanlarından erişilebilmesine izin veren kurallar oluşturabiliriz ya da Onion Architecture / Layered Architecture gibi mimariler izliyorsak bu mimarilerin sınırlarının düzgün işlediğini görmek için de kurallar yazabiliriz.

1
2
classes().that().resideInAPackage("..service..")
                    .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

Onion;

1
2
3
4
5
6
7
8
@ArchTest
static final ArchRule onion_architecture_is_respected = onionArchitecture()
        .domainModels("..domain.model..")
        .domainServices("..domain.service..")
        .applicationServices("..application..")
        .adapter("cli", "..adapter.cli..")
        .adapter("persistence", "..adapter.persistence..")
        .adapter("rest", "..adapter.rest..");

Layered

1
2
3
4
5
6
7
8
9
10
@ArchTest
static final ArchRule layer_dependencies_are_respected = layeredArchitecture().consideringAllDependencies()
 
        .layer("Controllers").definedBy("com.tngtech.archunit.example.layers.controller..")
        .layer("Services").definedBy("com.tngtech.archunit.example.layers.service..")
        .layer("Persistence").definedBy("com.tngtech.archunit.example.layers.persistence..")
 
        .whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
        .whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers")
        .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Services");

3.Bağımlılık Kuralları : Eğer bazı katmanlarımızın başka katmanlara erişimlerini kısıtlamak istiyorsak bunu ArchUnit kullanarak yapabiliriz. Örneğin ortak bilgilerin olduğu bir paketin başka paketlere bağımlılığının olmadığından emin olmak istiyorsak kullanabiliriz.

1
2
classes().that().resideInAPackage("..ortak..")
    .should().onlyHaveDependentClassesThat().resideInAPackage("..ortak..")

Kurulum

Maven veya Gradle aracılığıyla ArchUnit’i uygulamalarımıza kolayca ekleyebiliriz.

Maven için:

1
2
3
4
5
6
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit</artifactId>
    <version>1.0.1</version>
    <scope>test</scope>
</dependency>

Gradle için:

1
2
3
dependencies {
    testImplementation 'com.tngtech.archunit:archunit:1.0.1'
}
Kaynaklar:
  1. https://www.archunit.org/
  2. https://github.com/TNG/ArchUnit-Examples