Въведение в ArchUnit

1. Общ преглед

В тази статия ще покажем как да проверите архитектурата на системата с помощта на ArchUnit .

2. Какво е ArchUnit?

Връзката между архитектурните черти и поддръжката е добре изучена тема в софтуерната индустрия. Определянето на звукова архитектура за нашите системи обаче не е достатъчно. Трябва да проверим дали изпълненият код се придържа към него.

Най-просто казано, ArchUnit е тестова библиотека, която ни позволява да проверим дали приложението се придържа към даден набор от архитектурни правила . Но какво е архитектурно правило? Още повече, какво имаме предвид под архитектура в този контекст?

Нека започнем с последното. Тук използваме термина архитектура, за да обозначим начина, по който организираме различните класове в нашето приложение в пакети .

Архитектурата на системата също така определя как взаимодействат пакетите или групите пакети - известни също като слоеве . В по-практически смисъл, той определя дали кодът в даден пакет може да извика метод в клас, принадлежащ на друг. Да предположим например, че архитектурата на нашето приложение съдържа три слоя: презентация , услуга и постоянство .

Един от начините да визуализираме как тези слоеве си взаимодействат е чрез използване на диаграма на UML пакета с пакет, представляващ всеки слой:

Само като разгледаме тази диаграма, можем да разберем някои правила:

  • Презентационните класове трябва да зависят само от класовете на обслужване
  • Класовете на обслужване трябва да зависят само от класовете на постоянство
  • Класовете за упоритост не трябва да зависят от никой друг

Разглеждайки тези правила, вече можем да се върнем назад и да отговорим на първоначалния си въпрос. В този контекст едно архитектурно правило е твърдение за начина, по който нашите класове на приложения взаимодействат помежду си.

И така, как да проверим дали изпълнението ни спазва тези правила? Тук идва ArchUnit . Той ни позволява да изразим нашите архитектурни ограничения, използвайки плавен API и да ги потвърдим заедно с други тестове по време на редовно изграждане.

3. Настройка на проекта ArchUnit

ArchUnit се интегрира добре с тестовата рамка на JUnit и така те обикновено се използват заедно. Всичко, което трябва да направим, е да добавим зависимостта archunit-junit4, за да съответства на нашата версия JUnit :

 com.tngtech.archunit archunit-junit4 0.14.1 test  

Както подсказва неговият artefactId , тази зависимост е специфична за рамката JUnit 4.

Има и зависимост от archunit-junit5, ако използваме JUnit 5:

 com.tngtech.archunit archunit-junit5 0.14.1 test 

4. Писане на ArchUnit тестове

След като добавим подходящата зависимост към нашия проект, нека започнем да пишем нашите архитектурни тестове. Нашето тестово приложение ще бъде просто приложение SpringBoot REST, което задава въпроси за Smurfs. За простота, това тестово приложение съдържа само класовете Controller , Service и Repository .

Искаме да проверим дали това приложение отговаря на правилата, които споменахме преди. И така, нека започнем с прост тест за правилото „класовете за представяне трябва да зависят само от класовете на услуги“.

4.1. Първият ни тест

Първата стъпка е да създадете набор от Java класове, които ще бъдат проверени за нарушения на правилата . Правим това, като създаваме инстанция на класа ClassFileImporter и след това използваме един от методите им importXXX () :

JavaClasses jc = new ClassFileImporter() .importPackages("com.baeldung.archunit.smurfs");

В този случай екземплярът JavaClasses съдържа всички класове от нашия основен пакет за приложения и неговите подпакети. Можем да мислим за този обект като за аналогичен на типичен тестов субект, използван в редовни модулни тестове, тъй като той ще бъде целта за оценяване на правилата.

Архитектурните правила използват един от статичните методи от класа ArchRuleDefinition като отправна точка за своите плавни API извиквания. Нека се опитаме да приложим първото правило, дефинирано по-горе, използвайки този API. Ще използваме метода classes () като котва и ще добавим допълнителни ограничения от там:

ArchRule r1 = classes() .that().resideInAPackage("..presentation..") .should().onlyDependOnClassesThat() .resideInAPackage("..service.."); r1.check(jc);

Забележете, че трябва да извикаме метода check () на правилото, което сме създали, за да стартираме проверката. Този метод взема обект JavaClasses и ще изведе изключение, ако има нарушение.

Всичко това изглежда добре, но ще получим списък с грешки, ако се опитаме да го стартираме срещу нашия код:

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..presentation..' should only depend on classes that reside in a package '..service..'' was violated (6 times): ... error list omitted 

Защо? Основният проблем с това правило е onlyDependsOnClassesThat () . Въпреки това, което сме сложили в диаграмата на пакета, действителната ни реализация има зависимости от JVM и Spring рамковите класове, следователно и грешката.

4.2. Пренаписване на първия ни тест

Един от начините за разрешаване на тази грешка е добавянето на клауза, която отчита тези допълнителни зависимости:

ArchRule r1 = classes() .that().resideInAPackage("..presentation..") .should().onlyDependOnClassesThat() .resideInAPackage("..service..", "java..", "javax..", "org.springframework.."); 

С тази промяна нашата проверка ще спре да се проваля. Този подход обаче страда от проблеми с поддръжката и се чувства малко хакерски. Можем да избегнем тези проблеми с пренаписването на нашето правило, като използваме статичния метод noClasses () като начална точка:

ArchRule r1 = noClasses() .that().resideInAPackage("..presentation..") .should().dependOnClassesThat() .resideInAPackage("..persistence.."); 

Of course, we can also point that this approach is deny-based instead of the allow-based one we had before. The critical point is that whatever approach we choose, ArchUnit will usually be flexible enough to express our rules.

5. Using the LibraryAPI

ArchUnit makes the creation of complex architectural rules an easy task thanks to its built-in rules. Those, in turn, can also be combined, allowing us to create rules using a higher level of abstraction. Out of the box, ArchUnit offers the Library API, a collection of prepackaged rules that address common architecture concerns:

  • Architectures: Support for layered and onion (a.k.a. Hexagonal or “ports and adapters”) architectures rule checks
  • Slices: Used to detect circular dependencies, or “cycles”
  • General: Collection of rules related to best coding practices such as logging, use of exceptions, etc.
  • PlantUML: Checks whether our code base adheres to a given UML model
  • Freeze Arch Rules: Save violations for later use, allowing to report only new ones. Particularly useful to manage technical debts

Covering all those rules is out of scope for this introduction, but let's take a look at the Architecture rule package. In particular, let's rewrite the rules in the previous section using the layered architecture rules. Using these rules requires two steps: first, we define the layers of our application. Then, we define which layer accesses are allowed:

LayeredArchitecture arch = layeredArchitecture() // Define layers .layer("Presentation").definedBy("..presentation..") .layer("Service").definedBy("..service..") .layer("Persistence").definedBy("..persistence..") // Add constraints .whereLayer("Presentation").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation") .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service"); arch.check(jc);

Here, layeredArchitecture() is a static method from the Architectures class. When invoked, it returns a new LayeredArchitecture object, which we then use to define names layers and assertions regarding their dependencies. This object implements the ArchRule interface so that we can use it just like any other rule.

Готиното за този конкретен API е, че той ни позволява да създадем само няколко реда правила на кода, които иначе биха изисквали да комбинираме множество отделни правила.

6. Заключение

В тази статия разгледахме основите на използването на ArchUnit в нашите проекти. Приемането на този инструмент е сравнително проста задача, която може да има положително въздействие върху цялостното качество и да намали разходите за поддръжка в дългосрочен план.

Както обикновено, целият код е достъпен в GitHub.