1. 前書き(Introduction)
ArchUnit is a free, simple and extensible library for checking the architecture of your Java code. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing given Java bytecode, importing all classes into a Java code structure. ArchUnit’s main focus is to automatically test architecture and coding rules, using any plain Java unit testing framework.
ArchUnitはJavaソースコードのアーキテクチャをチェックする、簡潔性と拡張性に優れたフリーなライブラリです。 ArchUnitはパッケージやクラス間の依存関係に加えて、レイヤーやスライス間の依存関係をチェックできます。それ以外に、循環する依存関係もチェックできます。 依存関係のチェックは、Javaソースコードに登場する全てのクラスをインポートしたバイトコードについて行います。 ArchUnitの主な目的は、一般的なユニットテストフレームワークで、アーキテクチャやコーディングルールを自動的にテストすることです。
1.1. モジュールの概要(Module Overview)
ArchUnit consists of the following production modules: archunit
, archunit-junit4
as well
as archunit-junit5-api
, archunit-junit5-engine
and archunit-junit5-engine-api
.
Also relevant for end users is the archunit-example
module.
ArchUnitの公開モジュールは archunit
と、archunit-junit4
あるいは archunit-junit5-api
、archunit-junit5-engine
、archunit-junit5-engine-api
です。
エンドユーザとしては archunit-example
モジュールも参考になるでしょう。
1.1.1. archunit モジュール(Module archunit)
This module contains the actual ArchUnit core infrastructure required to write architecture
tests: The ClassFileImporter
,
the domain objects, as well as the rule syntax infrastructure.
このモジュールはアーキテクチャテストを記述するために必要な ArchUnit の基本部品を含んでいます。
例えば ClassFileImporter
というドメインオブジェクトや、ルールの文法を定義する部品を含んでいます。
1.1.2. archunit-junit4 モジュール(Module archunit-junit4)
This module contains the infrastructure to integrate with JUnit 4, in particular
the ArchUnitRunner
to cache imported classes.
このモジュールは ArchUnit を JUnit 4 へ統合するための部品を含んでいます。
特に ArchUnitRunner
はインポートしたクラス(のバイトコード)をキャッシュするようになっています。
1.1.3. archunit-junit5-* モジュール(Modules archunit-junit5-*)
These modules contain the infrastructure to integrate with JUnit 5 and contain the respective
infrastructure to cache imported classes between test runs.
archunit-junit5-api
contains the user API to write tests with ArchUnit’s JUnit 5 support,
archunit-junit5-engine
contains the runtime engine to run those tests.
archunit-junit5-engine-api
contains API code for tools that want more detailed control
over running ArchUnit JUnit 5 tests, in particular a FieldSelector
which can be used to
instruct the ArchUnitTestEngine
to run a specific rule field (compare JUnit 4 & 5 Support).
これらのモジュールは ArchUnit を JUnit 5 へ統合するための部品を含んでいます。
また、それぞれのテストケースについてインポートしたクラス(のバイトコード)をキャッシュするための部品も含んでいます。
archunit-junit5-api
は JUnit 5 で ArchUnit のテストを記述するためのユーザ向けAPIを含んでいます。
archunit-junit5-engine
はテストを実行するランタイムエンジンを含んでいます。
archunit-junit5-engine-api
はテストツールがより適切に JUnit 5 の ArchUnit テストを制御するためのAPIコードを含んでいます。
例えば、FieldSelector
は ArchUnitTestEngine
に特定のルールフィールドだけを実行させることができます。
( JUnit 4 & 5 Support とも読み比べてみてください)
1.1.4. archunit-example モジュール(Module archunit-example)
This module contains example architecture rules and sample code that violates these rules. Look here to get inspiration on how to set up rules for your project, or at ArchUnit-Examples for the last released version.
このモジュールはアーキテクチャルールの具体例や、ルール違反しているサンプルコードを含んでいます。 このモジュールを見れば、あなたのプロジェクトで整備するルールについてひらめきを得ることができるでしょう。 最新バージョンは ArchUnit-Examples で参照できます。
2. インストール(Installation)
To use ArchUnit, it is sufficient to include the respective JAR files in the classpath. Most commonly, this is done by adding the dependency to your dependency management tool, which is illustrated for Maven and Gradle below. Alternatively you can obtain the necessary JAR files directly from Maven Central.
ArchUnitを使うには、必要なJarファイルをクラスパスに配置します。 普通ならMavenやGradleなどの依存性管理ツールへ依存ライブラリを追加することになるでしょう。 Maven Centralから必要なJarファイルを直接ダウンロードすることも可能です。
2.1. JUnit 4
To use ArchUnit in combination with JUnit 4, include the following dependency from Maven Central:
ArchUnit を JUnit 4 と組み合わせて使用するには、Maven Central から次のような依存ライブラリを追加します。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>v0.21.0</version>
<scope>test</scope>
</dependency>
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit4:v0.21.0'
}
2.2. JUnit 5
ArchUnit’s JUnit 5 artifacts follow the pattern of JUnit Jupiter. There is one artifact containing
the API, i.e. the compile time dependencies to write tests. Then there is another artifact containing
the actual TestEngine
used at runtime. Just like JUnit Jupiter ArchUnit offers one convenience
artifact transitively including both API and engine with the correct scope, which in turn can be added
as a test compile dependency. Thus to include ArchUnit’s JUnit 5 support, simply add the following dependency
from Maven Central:
ArchUnit の JUnit 5 向けアーティファクトは、JUnit Jupiter と同じ構成になっています。
1つはテストを記述するためのAPIを含む、コンパイル依存関係のアーティファクトです。
もう1つは実際にテストを実行する TestEngine
を含むアーティファクトです。
JUnit Jupiter のように、API とエンジン両方のアーティファクトへ適切なスコープで依存するアーティファクトがあるので、テストコンパイル依存関係として追加すれば推移的依存関係として解決されるので便利です。
ですから、ArchUnit を JUnit 5 と組み合わせて使用するには、Maven Central から次のような依存ライブラリを追加するだけです。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>v0.21.0</version>
<scope>test</scope>
</dependency>
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:v0.21.0'
}
2.3. その他のテストフレームワークについて(Other Test Frameworks)
ArchUnit works with any test framework that executes Java code. To use ArchUnit in such a context, include the core ArchUnit dependency from Maven Central:
ArchUnit は Java に対応するあらゆるテストフレームワークから使用できます。 その場合、Maven Central から取得したコアモジュールを依存ライブラリとして追加します。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>v0.21.0</version>
<scope>test</scope>
</dependency>
dependencies {
testImplementation 'com.tngtech.archunit:archunit:v0.21.0'
}
2.4. Maven プラグイン(Maven Plugin)
There exists a Maven plugin by Société Générale to run ArchUnit rules straight from Maven. For more information visit their GitHub repo: https://github.com/societe-generale/arch-unit-maven-plugin
Société Générale さんが Maven から直接 ArchUnit を実行できるプラグインを公開してくれています。 詳しくは GitHub のリポジトリ https://github.com/societe-generale/arch-unit-maven-plugin を参照してください。
3. ArchUnit 入門(Getting Started)
ArchUnit tests are written the same way as any Java unit test and can be written with any Java unit testing framework. To really understand the ideas behind ArchUnit, one should consult Ideas and Concepts. The following will outline a "technical" getting started.
ArchUnit のテストはユニットテストと同じやり方で、そしてあらゆるユニットテストフレームワークを使用して記述できます。 ArchUnit の背景となる考え方を正確に理解したければ、Ideas and Conceptsを読んでみてください。 以降の説明は「技術的な」入門です。
3.1. クラスのインポート(Importing Classes)
At its core ArchUnit provides infrastructure to import Java bytecode into Java code structures.
This can be done using the ClassFileImporter
ArchUnitの基本部品は、Javaバイトコードをソースコード構造へインポートする ClassFileImporter
です。
JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");
The ClassFileImporter
offers many ways to import classes. Some ways depend on
the current project’s classpath, like importPackages(..)
. However there are other ways
that do not, for example:
ClassFileImpoter
はクラスをインポートするためのさまざまな方法を提供します。
importPackages(..)
のようにプロジェクトのクラスパスへ依存する方法もありますが、次のようにそうでない方法もあります。
JavaClasses classes = new ClassFileImporter().importPath("/some/path");
The returned object of type JavaClasses
represents a collection of elements of type
JavaClass
, where JavaClass
in turn represents a single imported class file. You can
in fact access most properties of the imported class via the public API:
返り値の JavaClasses
型のオブジェクトは、JavaClass
型のオブジェクトの集合です。
JavaClass
はインポートしたいずれかのクラスファイルに対応しています。
public APIにより、インポートしたクラスのほとんどのプロパティを参照できます。
JavaClass clazz = classes.get(Object.class);
System.out.print(clazz.getSimpleName()); // returns 'Object'
3.2. (アーキテクチャにおける)制約のアサーション(Asserting (Architectural) Constraints)
To express architectural rules, like 'Services should only be accessed by Controllers',
ArchUnit offers an abstract DSL-like fluent API, which can in turn be evaluated against
imported classes. To specify a rule, use the class ArchRuleDefinition
as entry point:
例えば「サービスにアクセスできるのはコントローラーだけでなければならない」というアーキテクチャルールを ArchUnit で表現するには、フルーエントAPIを抽象的なDSLとして、インポートしたクラスを評価できます。
実際にルールを定義するときは、ArchRuleDefinition
クラスを起点にします。
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
// ...
ArchRule myRule = classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
The two dots represent any number of packages (compare AspectJ Pointcuts). The returned
object of type ArchRule
can now be evaluated against a set of imported classes:
パッケージ名を指定する文字列に含まれる連続する2つのドットは、任意の数のパッケージ階層を表しています(AspectJ のポイントカットを記述する場合と同様です)。
返り値の ArchRule
型のオブジェクトを使うと、インポートしたクラスの集合を評価できます。
myRule.check(importedClasses);
Thus the complete example could look like
全てを組み合わせた完全な具体例は次のとおりです。
@Test
public void Services_should_only_be_accessed_by_Controllers() {
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.myapp");
ArchRule myRule = classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
myRule.check(importedClasses);
}
3.3. JUnit 4 と JUnit 5 のどちらかを使いましょう(Using JUnit 4 or JUnit 5)
While ArchUnit can be used with any unit testing framework, it provides extended support for writing tests with JUnit 4 and JUnit 5. The main advantage is automatic caching of imported classes between tests (of the same imported classes), as well as reduction of boilerplate code.
ArchUnit はどのようなユニットテストフレームワークでも使用できますが、JUnit4 あるいは JUnit 5 を使ってテストを書くための拡張機能が備わっています。 主な利点はインポートしたクラスのキャッシュをテストケース間で再利用できることです。 そうすると、ボイラープレートコードの記述を減らすことができます。
To use the JUnit support, declare ArchUnit’s ArchUnitRunner
(only JUnit 4), declare the classes
to import via @AnalyzeClasses
and add the respective rules as fields:
JUnit 用の拡張機能を使うには、ArchUnitRunner
を宣言し(JUnit 4 の場合のみ)、インポートするクラスを @AnalyzeClasses
で指定します。
そして、ルールをテストクラスのフィールドとして定義します。
@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!!
@AnalyzeClasses(packages = "com.mycompany.myapp")
public class MyArchitectureTest {
@ArchTest
public static final ArchRule myRule = classes()
.that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
}
The JUnit test support will automatically import (or reuse) the specified classes and
evaluate any rule annotated with @ArchTest
against those classes.
JUnit 用の拡張機能は、指定したクラスを自動的にインポートし、再利用するようになります。
また、@ArchTest
で修飾されたあらゆるルールを評価するようになります。
For further information on how to use the JUnit support refer to JUnit Support.
より詳しい使い方は JUnit Support を参照してください。
3.4. Kotlin で JUnit を使う(Using JUnit support with Kotlin)
Using the JUnit support with Kotlin is quite similar to Java:
JUnit 用の拡張機能を Kotlin から使う方法は Java の場合とほとんど変わりません。
@RunWith(ArchUnitRunner::class) // Remove this line for JUnit 5!!
@AnalyzeClasses(packagesOf = [MyArchitectureTest::class])
class MyArchitectureTest {
@ArchTest
val rule_as_field = ArchRuleDefinition.noClasses().should()...
@ArchTest
fun rule_as_method(importedClasses: JavaClasses) {
val rule = ArchRuleDefinition.noClasses().should()...
rule.check(importedClasses)
}
}
4. チェックの対象(What to Check)
The following section illustrates some typical checks you could do with ArchUnit.
このセクションでは、ArchUnit の典型的なチェック内容を説明します。
4.1. パッケージの依存性チェック(Package Dependency Checks)
noClasses().that().resideInAPackage("..source..")
.should().dependOnClassesThat().resideInAPackage("..foo..")
classes().that().resideInAPackage("..foo..")
.should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..foo..")
4.2. クラスの依存性チェック(Class Dependency Checks)
classes().that().haveNameMatching(".*Bar")
.should().onlyHaveDependentClassesThat().haveSimpleName("Bar")
4.3. クラスおよびパッケージの閉じ込め状況チェック(Class and Package Containment Checks)
classes().that().haveSimpleNameStartingWith("Foo")
.should().resideInAPackage("com.foo")
4.4. 継承関係のチェック(Inheritance Checks)
classes().that().implement(Connection.class)
.should().haveSimpleNameEndingWith("Connection")
classes().that().areAssignableTo(EntityManager.class)
.should().onlyHaveDependentClassesThat().resideInAnyPackage("..persistence..")
4.5. アノテーションのチェック(Annotation Checks)
classes().that().areAssignableTo(EntityManager.class)
.should().onlyHaveDependentClassesThat().areAnnotatedWith(Transactional.class)
4.6. レイヤーのチェック(Layer Checks)
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
5. 考え方と概念(Ideas and Concepts)
ArchUnit is divided into different layers, where the most important ones are the "Core" layer, the "Lang" layer and the "Library" layer. In short the Core layer deals with the basic infrastructure, i.e. how to import byte code into Java objects. The Lang layer contains the rule syntax to specify architecture rules in a succinct way. The Library layer contains more complex predefined rules, like a layered architecture with several layers. The following section will explain these layers in more detail.
ArchUnit は複数のレイヤーに分かれています。 最も重要なレイヤーは「コアレイヤー」「言語レイヤー」「ライブラリレイヤー」です。 「コアレイヤー」はバイトコードを Java オブジェクトにインポートする等の基本的な部品を含みます。 「言語レイヤー」はアーキテクチャルールを簡潔に表現するルール構文を含みます。 「ライブラリレイヤー」はいくつものレイヤーで構成されるレイヤーアーキテクチャのように、より複雑な定義済みルールを含みます。 このセクションではそれぞれのレイヤーについてより詳しく説明します。
5.1. コアレイヤー(Core)
Much of ArchUnit’s core API resembles the Java Reflection API.
There are classes like JavaMethod
, JavaField
, and more,
and the public API consists of methods like getName()
, getMethods()
,
getRawType()
or getRawParameterTypes()
.
Additionally ArchUnit extends this API for concepts needed to talk about dependencies between code,
like JavaMethodCall
, JavaConstructorCall
or JavaFieldAccess
.
For example, it is possible to programmatically iterate over javaClass.getAccessesFromSelf()
and react to the imported accesses between this Java class and other Java classes.
ArchUnit のコア API の大部分は Java のリフレクション API とよく似た形式になっています。
JavaMethod
や JavaField
などのクラスがありますし、getName()
や getMethods()
、getRawType()
や getRawParameterTypes()
等のパブリック API (メソッド)があります。
ArchUnit は、コード間の依存関係を表現するために必要な JavaMethodCall
や JavaConstructorCall
や JavaFieldAccess
等、これらの API の考え方を拡張しています。
例えば、javaClass.getAccessesFromSelf()
のように記述すれば、javaClass
そのものと他のクラスの間に存在する内向きのアクセスを処理できるのです。
To import compiled Java class files, ArchUnit provides the ClassFileImporter
, which can
for example be used to import packages from the classpath:
コンパイルされた Java のクラスファイルをインポートするため、ArchUnit は ClassFileImporter
を提供しています。
このクラスは、クラスパスに存在するパッケージに所属するクラスをインポートできます。
JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");
For more information refer to The Core API.
より詳しい内容は The Core API を参照してください。
5.2. 言語レイヤー(Lang)
The Core API is quite powerful and offers a lot of information about the static structure of a Java program. However, tests directly using the Core API lack expressiveness, in particular with respect to architectural rules.
コア API は、Java プログラムの静的な構造に関するさまざまな情報を取得するための強力な機能を提供します。 しかし、テストコードから直接コア API を使うようにすると、特にアーキテクチャルールについて(意図の)表現力が損なわれてしまいます。
For this reason ArchUnit provides the Lang API, which offers a powerful syntax to express rules in an abstract way. Most parts of the Lang API are composed as fluent APIs, i.e. an IDE can provide valuable suggestions on the possibilities the syntax offers.
ArchUnit はこの問題を解決するため、より抽象的な形式でルールを表現する強力な構文を、言語 API として提供しています。 言語 API の大部分はフルーエント API として組み合わせるものになります。 そうすると、IDE は利用可能な構文の候補を提供できるのです。
An example for a specified architecture rule would be:
次の例は、具体的なアーキテクチャルールの1つです。
ArchRule rule =
classes().that().resideInAPackage("..service..")
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
Once a rule is composed, imported Java classes can be checked against it:
定義したルールは、インポートした Java クラスをチェックできます。
JavaClasses importedClasses = new ClassFileImporter().importPackage("com.myapp");
ArchRule rule = // define the rule
rule.check(importedClasses);
The syntax ArchUnit provides is fully extensible and can thus be adjusted to almost any specific need. For further information, please refer to The Lang API.
ArchUnit の提供する構文は全て拡張可能なので、あらゆる場面で必要に応じて調整できます。 より詳しい内容は The Lang API を参照してください。
5.3. ライブラリレイヤー(Library)
The Library API offers predefined complex rules for typical architectural goals. For example a succinct definition of a layered architecture via package definitions. Or rules to slice the code base in a certain way, for example in different areas of the domain, and enforce these slices to be acyclic or independent of each other. More detailed information is provided in The Library API.
ライブラリ API は典型的なアーキテクチャを対象とする複雑な定義済みのルールを提供します。 例えば、パッケージ定義に基づく簡潔なレイヤーアーキテクチャのルールが挙げられます。 他にも、特定の断面でコードベースを横断するルールも定義されています。 例えば、さまざまな分野を反映したドメインや、それぞれの断面が循環しないように強制したり、それぞれの断面の独立性を高めたりするようなルールがあります。 より詳しい内容は The Library API を参照してください。
6. コア API(The Core API)
The Core API is itself divided into the domain objects and the actual import.
コア API はドメインオブジェクトとインポートに関連する部分へ分かれています。
6.1. インポート(Import)
As mentioned in Ideas and Concepts the backbone of the infrastructure is the ClassFileImporter
,
which provides various ways to import Java classes. One way is to import packages from
the classpath, or the complete classpath via
Ideas and Conceptsでも説明したように、基本部品は ClassFileImporter
クラスです。
このクラスは Java クラスファイルをインポートするためのさまざまな方法を提供します。
クラスパスに存在するパッケージを指定する方法や、完全なクラスパスを指定する方法があります。
JavaClasses classes = new ClassFileImporter().importClasspath();
However, the import process is completely independent of the classpath, so it would be well possible to import any path from the file system:
ただし、インポート処理自体はクラスパスと完全に独立しているため、ファイルシステム上の任意のパスを指定できるようになっています。
JavaClasses classes = new ClassFileImporter().importPath("/some/path/to/classes");
The ClassFileImporter
offers several other methods to import classes, for example locations can be
specified as URLs or as JAR files.
ClassFileImporter
クラスは、クラスをインポートするためのさまざまな方法を提供しています。
例えば、URL や JAR ファイルでインポートする位置を指定できるようになっています。
Furthermore specific locations can be filtered out, if they are contained in the source of classes,
but should not be imported. A typical use case would be to ignore test classes, when the classpath
is imported. This can be achieved by specifying ImportOptions
:
指定した位置の中から一部を除外できるようにもなっているため、インポートするべきではないクラスを無視できます。
テストクラスを除外するのはよくある使い方の1つです。
具体的には ImportOptions
を指定します。
ImportOption ignoreTests = new ImportOption() {
@Override
public boolean includes(Location location) {
return !location.contains("/test/"); // ignore any URI to sources that contains '/test/'
}
};
JavaClasses classes = new ClassFileImporter().withImportOption(ignoreTests).importClasspath();
A Location
is principally an URI, i.e. ArchUnit considers sources as File or JAR URIs
基本的に Location
は URI として扱います。
ArchUnit は指定された Location
を File
あるいは JAR
を指す URI
として扱うということです。
-
jar:file:///home/dev/.m2/repository/some/things.jar!/some/Thing.class
For the two common cases to skip importing JAR files and to skip importing test files
(for typical setups, like a Maven or Gradle build),
there already exist predefined ImportOptions
:
Maven や Gradle のプロジェクトで一般的な2種類のケース、つまり、JARファイルやテストクラスをインポート対象から除外するケースは ImportOptions
に定義済みです。
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importClasspath();
6.1.1. 存在しないクラスの扱い(Dealing with Missing Classes)
While importing the requested classes (e.g. target/classes
or target/test-classes
)
it can happen that a class within the scope of the import has a reference to a class outside of the
scope of the import. This will naturally happen, if the classes of the JDK are not imported,
since then for example any dependency on Object.class
will be unresolved within the import.
target/classes
や target/test-classes
などを指定してインポートしたクラスが、指定した場所に存在しないクラスを参照している場合があります。
自然に発生する状況ですが、例えば JDK に含まれるクラスをインポートしていなければ、 Object.class
に対する全ての依存関係は未解決になってしまいます。
At this point ArchUnit needs to decide how to treat these classes that are missing from the import. By default, ArchUnit searches within the classpath for missing classes and if found imports them. This obviously has the advantage that information about those classes (which interfaces they implement, how they are annotated) is present during rule evaluation.
ArchUnit はインポートしたクラスに存在しないクラスを発見したときにどうするか決めておかなければなりません。 初期設定では存在しないクラスを発見したら、クラスパスを探索するようになっています。 ルールを評価するとき、実装しているインターフェイスやアノテーションなどの情報を利用できるのは大きな利点になります。
On the downside this additional lookup from the classpath will cost some performance and in some
cases might not make sense (e.g. if information about classes not present in the original import
is known to be unnecessary for evaluating rules).
Thus ArchUnit can be configured to create stubs instead, i.e. a JavaClass
that has all the known
information, like the fully qualified name or the method called. However, this stub might
naturally lack some information, like superclasses, annotations or other details that cannot
be determined without importing the bytecode of this class. This behavior will also happen,
if ArchUnit fails to determine the location of a missing class from the classpath.
クラスパスを探索することの欠点は、時間が長くなってしまうことと無意味な結果しか得られない場合があることです。
例えば、インポートできたクラスに存在しないクラスの情報が、ルールの評価に不要なことが分かっている場合が挙げられます。
そういう場合について、ArchUnit ではスタブを生成できるようになっています。
具体的には JavaClass
クラスは完全修飾名やメソッド呼び出しなど必要な全ての情報を持っています。
しかし、本質的にスタブには不足している情報があります。
基底クラスやアノテーション等、クラスファイル(バイトコード)をインポートしなければ決定できない情報のことです。
これは、ArchUnit がクラスパスを探索してもクラスを発見できなかった場合の振る舞いでもあります。
To find out, how to configure the default behavior, refer to Configuring the Resolution Behavior.
振る舞いを変更する方法について知りたければ Configuring the Resolution Behavior を参照してください。
6.2. ドメイン(Domain)
The domain objects represent Java code, thus the naming should be pretty straight forward. Most
commonly, the ClassFileImporter
imports instances of type JavaClass
. A rough overview looks
like this:
ドメインオブジェクトは Java のソースコードを直接的な名前で表現します。
ほとんどの場合 ClassFileImporter
は JavaClass
型のインスタンスをインポートします。
基本的な構造は次のとおりです。
Most objects resemble the Java Reflection API, including inheritance relations. Thus a JavaClass
has JavaMembers
, which can in turn be either JavaField
, JavaMethod
,
JavaConstructor
(or JavaStaticInitializer
). While not present within the reflection API,
it makes sense to introduce an expression for anything that can access other code, which ArchUnit
calls 'code unit', and is in fact either a method, a constructor (including the class initializer)
or a static initializer of a class (e.g. a static { … }
block, a static field assignment,
etc.).
ほとんどのオブジェクトは、継承関係も含めて Java のリフレクション API とよく似た形式になっています。
JavaClass
は JavaMembers
を持っているし、JavaMembers
は JavaField
や JavaMethod
や JavaConstructor
(あるいは JavaStaticInitializer
)のいずれかになります。
リフレクション API には存在しませんが、他のコードへアクセスする表現はあると便利です。
ArchUnit では「コードユニット(code unit)」と呼んでいる要素で、メソッドやコンストラクタ、クラスイニシャライザ、静的イニシャライザ(static { … }
ブロックやクラスフィールドの初期化など)に対応します。
Furthermore one of the most interesting features of ArchUnit that exceeds the Java Reflection API,
is the concept of accesses to another class. On the lowest level accesses can only take place
from a code unit (as mentioned, any block of executable code) to either a field (JavaFieldAccess
),
a method (JavaMethodCall
) or constructor (JavaConstructorCall
).
ArchUnit が Java リフレクション API を拡張している中で最も面白い機能は他のクラスへのアクセス、という考え方です。
最も抽象度が低いのは、コードユニットからフィールド(JavaFieldAccess
)やメソッド(JavaMethodCall
)やコンストラクタ(JavaConstructorCall
)に対するアクセスです。
ArchUnit imports the whole graph of classes and their relationship to each other. While checking
the accesses from a class is pretty isolated (the bytecode offers all this information),
checking accesses to a class requires the whole graph to be built first. To distinguish which
sort of access is referred to, methods will always clearly state fromSelf and toSelf.
For example, every JavaField
allows to call JavaField#getAccessesToSelf()
to retrieve all
code units within the graph that access this specific field. The resolution process through
inheritance is not completely straight forward. Consider for example
ArchUnit は全てのクラスの呼び出しグラフとクラス間の関係性を別々にインポートします。
アクセス元 のクラスに対するチェックは完全に独立しています(全ての情報はバイトコードから取得します)が、 アクセス先 のクラスをチェックするには最初に構築したグラフ情報が必要です。
どちらからアクセスしているのか区別するため、それぞれのメソッドは fromSelf と toSelf の状態を持つようになっています。
例えば、全ての JavaField
は JavaField#getAccessesToSelf()
でそのフィールドへアクセスするグラフ中の全てのコードユニットを取得できるようになっています。
継承関係のあるクラスにおける呼び出し元の解決処理は極めて複雑です。
次のような例を考えてみてください。
The bytecode will record a field access from ClassAccessing.accessField()
to
ClassBeingAccessed.accessedField
. However, there is no such field, since the field is
actually declared in the superclass. This is the reason why a JavaFieldAccess
has no JavaField
as its target, but a FieldAccessTarget
. In other words, ArchUnit models
the situation, as it is found within the bytecode, and an access target is not an actual
member within another class. If a member is queried for accessesToSelf()
though, ArchUnit
will resolve the necessary targets and determine, which member is represented by which target.
The situation looks roughly like
バイトコードには ClassAccessing.accessField()
から ClassBeingAccessed.accessedField
へアクセスしていることが記録されているでしょう。
しかし、実際にはそんなフィールドは存在しません。
基底クラスで定義されているからです。
JavaFieldAccess
の対象に1つも JavaField
が含まれておらず、FieldAccessTarget
が存在するのはそのせいです。
別の言い方をすると、ArchUnit はバイトコードから得られた状況をモデル化していると言えます。
アクセスしている対象自体はそのクラスのメンバーではないのです。
メンバーを accessToSelf()
で問い合わせれば、ArchUnit は必要な対象を発見、特定できます。
対象自体がメンバーを表現しているからです。
例えば次のような状況です。
Two things might seem strange at the first look.
一見すると奇妙なところが2点見つけられます。
First, why can a target resolve to zero matching members? The reason is that the set of classes
that was imported does not need to have all classes involved within this resolution process.
Consider the above example, if SuperclassBeingAccessed
would not be imported, ArchUnit would
have no way of knowing where the actual targeted field resides. Thus in this case the
resolution would return zero elements.
1点目は、対象を解決した結果となるメンバー数が0になる場合があることです。
原因は、インポートしたクラスに、この解決処理に関係する全てのクラスが必要だとは限らないからです。
前の例では SuperclassBeingAccessed
がインポートされていますが、ArchUnit には実際にどのようなフィールドが存在するのか知る手段がありません。
だから、解決結果の要素数が0になる場合があるのです。
Second, why can there be more than one resolved methods for method calls? The reason for this is that a call target might indeed match several methods in those cases, for example:
2点目は、メソッド呼び出しを解決した結果が1以上になる場合があることです。 原因は、実際に対象のメソッドが複数回呼び出されている場合があることです。 例えば次のような場合があります。
While this situation will always be resolved in a specified way for a real program,
ArchUnit cannot do the same. Instead, the resolution will report all candidates that match a
specific access target, so in the above example, the call target C.targetMethod()
would in fact
resolve to two JavaMethods
, namely A.targetMethod()
and B.targetMethod()
. Likewise a check
of either A.targetMethod.getCallsToSelf()
or B.targetMethod.getCallsToSelf()
would return
the same call from D.callTargetMethod()
to C.targetMethod()
.
実際のプログラムなら常に特別な方法で解決するような状況ですが、ArchUnit には解決できません。
代わりに、具体的なアクセス対象にマッチする全ての候補を報告します。
前の例では呼び出し対象の C.targetMethod()
について、A.targetMethod()
および B.targetMethod()
という2つの JavaMethods
を報告します。
同様に、A.targetMethod.getCallsToSelf()
や B.targetMethod.getCallsToSelf()
のどちらも、D.callTargetMethod()
から C.targetMethod()
と同じ呼び出し対象を返すことになります。
6.2.1. ドメインオブジェクト、リフレクション、クラスパス(Domain Objects, Reflection and the Classpath)
ArchUnit tries to offer a lot of information from the bytecode. For example, a JavaClass
provides details like if it is an enum or an interface, modifiers like public
or abstract
,
but also the source, where this class was imported from (namely the URI mentioned in the first
section). However, if information is missing, and the classpath is correct, ArchUnit offers
some convenience to rely on the reflection API for extended details. For this reason, most
Java*
objects offer a method reflect()
, which will in fact try to resolve the respective
object from the Reflection API. For example:
ArchUnit はバイトコードから取得した多くの情報をできるだけ提供できるように試みます。
例えば、JavaClass
ならそれが enum なのか interface なのか教えてくれるし、public
修飾子や abstract
修飾子についても教えてくれます。
また、どこからインポートしたクラスなのかも教えてくれます(前のセクションで説明した URI のことです)。
そして、正しいクラスパスが与えられているのに情報が欠落している場合、ArchUnit はリフレクション API を駆使して更なる詳細の提供を試みます。
ほとんどの Java*
オブジェクトには reflect()
メソッドが存在するのはそのためです。
このメソッドはリフレクション API でオブジェクトの情報を解決しようとします。
JavaClasses classes = new ClassFileImporter().importClasspath(new ImportOptions());
// ArchUnit's java.lang.String
JavaClass javaClass = classes.get(String.class);
// Reflection API's java.lang.String
Class<?> stringClass = javaClass.reflect();
// ArchUnit's public int java.lang.String.length()
JavaMethod javaMethod = javaClass.getMethod("length");
// Reflection API's public int java.lang.String.length()
Method lengthMethod = javaMethod.reflect();
However, this will throw an Exception
, if the respective classes are missing on the classpath
(e.g. because they were just imported from some file path).
ただし、対応するクラスがクラスパス上に存在しない場合(ファイルパスからインポートした場合等)、このメソッドは Exception
を送出します。
This restriction also applies to handling annotations in a more convenient way. Consider the following annotation:
この制限は、アノテーションをより便利に扱う方法を提供します。 例えば、次のようなアノテーションがあるとします。
@interface CustomAnnotation {
String value();
}
If you need to access this annotation without it being on the classpath, you must rely on
このアノテーションがクラスパス上に存在しなくてもアクセスしなければならないときは次のようにするしかありません。
JavaAnnotation<?> annotation = javaClass.getAnnotationOfType("some.pkg.CustomAnnotation");
// result is untyped, since it might not be on the classpath (e.g. enums)
Object value = annotation.get("value");
So there is neither type safety nor automatic refactoring support. If this annotation is on the classpath, however, this can be written way more naturally:
この場合型安全性は保証されませんし、自動的なリファクタリング支援も得られません。 アノテーションがクラスパス上に存在するなら、次のようにより自然に記述できます。
CustomAnnotation annotation = javaClass.getAnnotationOfType(CustomAnnotation.class);
String value = annotation.value();
ArchUnit’s own rule APIs (compare The Lang API) never rely on the classpath though. Thus the evaluation of default rules and syntax combinations, described in the next section, does not depend on whether the classes were imported from the classpath or some JAR / folder.
ArchUnit 自身のルール API (The Lang API を参照)はクラスパスに依存していません。 つまり、次のセクションで説明する、基本ルールやルール構文の組み合わせを評価するときは、クラスパスやJARあるいはフォルダのどこからクラスをインポートしても関係ないのです。
7. 言語 API(The Lang API)
7.1. クラスルールの合成(Composing Class Rules)
The Core API is pretty powerful with regard to all the details from the bytecode that it provides to tests. However, tests written this way lack conciseness and fail to convey the architectural concept that they should assert. Consider:
The Core API はテストするためにインポートしたバイトコードに関するあらゆる情報を扱えるため、とても強力です。 しかし、コア API で記述したテストは簡潔とは言い難く、検証するべきアーキテクチャとしての考え方を上手く伝えられない場合があります。 次のソースコードを検討してみましょう。
Set<JavaClass> services = new HashSet<>();
for (JavaClass clazz : classes) {
// choose those classes with FQN with infix '.service.'
if (clazz.getName().contains(".service.")) {
services.add(clazz);
}
}
for (JavaClass service : services) {
for (JavaAccess<?> access : service.getAccessesFromSelf()) {
String targetName = access.getTargetOwner().getName();
// fail if the target FQN has the infix ".controller."
if (targetName.contains(".controller.")) {
String message = String.format(
"Service %s accesses Controller %s in line %d",
service.getName(), targetName, access.getLineNumber());
Assert.fail(message);
}
}
}
What we want to express, is the rule "no classes that reside in a package 'service' should access classes that reside in a package 'controller'". Nevertheless, it’s hard to read through that code and distill that information. And the same process has to be done every time someone needs to understand the semantics of this rule.
表現したいのは 「パッケージ名に 'service' を含むクラスはパッケージ名に 'controller' を含むクラスへアクセスしてはならない」 というルールです。 しかし、このソースコードからそのような意図を読み取るのは困難です。 それに、このルールの意味を理解しなければならない人は全員が同じ困難なプロセスを体験しなければなりません。
To solve this shortcoming, ArchUnit offers a high level API to express architectural concepts in a concise way. In fact, we can write code that is almost equivalent to the prose rule text mentioned before:
この問題を解決するため、ArchUnit はアーキテクチャとしての考え方を分かりやすく表現するための高水準 API を提供しています。 この API を使用すると、前に説明した文章のようにルールを記述できるのです。
ArchRule rule = ArchRuleDefinition.noClasses()
.that().resideInAPackage("..service..")
.should().accessClassesThat().resideInAPackage("..controller..");
rule.check(importedClasses);
The only difference to colloquial language is the ".." in the package notation,
which refers to any number of packages. Thus "..service.." just expresses
"any package that contains some sub-package 'service'", e.g. com.myapp.service.any
.
If this test fails, it will report an AssertionError
with the following message:
話し言葉との違いはパッケージ記法に含まれる ".." で、これは任意のパッケージを表しています。
つまり "..service.." は 「サブパッケージ 'service' を含む任意のパッケージ」 という意味になるのです。
例えば com.myapp.service.any
などが挙げられます。
このテストが失敗するときは、次のようなメッセージを含む AssertionError
を報告するでしょう。
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] -
Rule 'no classes that reside in a package '..service..'
should access classes that reside in a package '..controller..'' was violated (1 times):
Method <some.pkg.service.SomeService.callController()>
calls method <some.pkg.controller.SomeController.execute()>
in (SomeService.java:14)
So as a benefit, the assertion error contains the full rule text out of the box and reports all violations including the exact class and line number. The rule API also allows to combine predicates and conditions:
表明エラーは、定義済みのルールに関する完全な文章を含んでおり、正確なクラス名と行番号を含む全ての違反を報告します。 ルール API は述語式と条件式を組み合わせることができるようになっています。
noClasses()
.that().resideInAPackage("..service..")
.or().resideInAPackage("..persistence..")
.should().accessClassesThat().resideInAPackage("..controller..")
.orShould().accessClassesThat().resideInAPackage("..ui..")
rule.check(importedClasses);
7.2. メンバールールの合成(Composing Member Rules)
In addition to a predefined API to write rules about Java classes and their relations, there is
an extended API to define rules for members of Java classes. This might be relevant, for example,
if methods in a certain context need to be annotated with a specific annotation, or return
types implementing a certain interface. The entry point is again ArchRuleDefinition
, e.g.
Java クラスとクラス間の関係を説明するルールを記述する定義済みの API に加えて、Java クラスのメンバーに関するルールを定義するための拡張 API があります。
それぞれの API は関連性があります。
例えば、あるメソッドは特定のコンテキストにおいて何らかのアノテーションで修飾しなければならないとか、メソッドの返り値の型は特定のインターフェイスを実装したクラスでなければならないとかです。
ルールの入口はやはり ArchRuleDefinition
です。
ArchRule rule = ArchRuleDefinition.methods()
.that().arePublic()
.and().areDeclaredInClassesThat().resideInAPackage("..controller..")
.should().beAnnotatedWith(Secured.class);
rule.check(importedClasses);
Besides methods()
, ArchRuleDefinition
offers the methods members()
, fields()
, codeUnits()
, constructors()
– and the corresponding negations noMembers()
, noFields()
, noMethods()
, etc.
ArchRuleDefinition
は、methods()
に続けて members()
fields()
codeUnits()
constructors()
のようなメソッドと、それらの反対を意味する noMembers()
noFields()
noMethods()
のようなメソッドを提供しています。
7.3. 独自ルールの作成(Creating Custom Rules)
In fact, most architectural rules take the form
ほとんどのアーキテクチャとしてのルールは次のような形式になります。
classes that ${PREDICATE} should ${CONDITION}
In other words, we always want to limit imported classes to a relevant subset,
and then evaluate some condition to see that all those classes satisfy it.
ArchUnit’s API allows you to do just that, by exposing the concepts of DescribedPredicate
and ArchCondition
.
So the rule above is just an application of this generic API:
別の言い方をしましょう。
私たちはバイトコードからインポートするクラスを関連するクラスだけに限定し、それらのクラスが指定した条件を満たしているのか評価するようにしたいのです。
ArchUnit は DescribePredicate
および ArchCondition
という API でそれを実現します。
この汎用 API で前のセクションに登場したルールを記述すると次のようになります。
DescribedPredicate<JavaClass> resideInAPackageService = // define the predicate
ArchCondition<JavaClass> accessClassesThatResideInAPackageController = // define the condition
noClasses().that(resideInAPackageService)
.should(accessClassesThatResideInAPackageController);
Thus, if the predefined API does not allow to express some concept, it is possible to extend it in any custom way. For example:
定義済みのルールで表現できない考え方があるとしても、例えば次のように拡張できます。
DescribedPredicate<JavaClass> haveAFieldAnnotatedWithPayload =
new DescribedPredicate<JavaClass>("have a field annotated with @Payload"){
@Override
public boolean apply(JavaClass input) {
boolean someFieldAnnotatedWithPayload = // iterate fields and check for @Payload
return someFieldAnnotatedWithPayload;
}
};
ArchCondition<JavaClass> onlyBeAccessedBySecuredMethods =
new ArchCondition<JavaClass>("only be accessed by @Secured methods") {
@Override
public void check(JavaClass item, ConditionEvents events) {
for (JavaMethodCall call : item.getMethodCallsToSelf()) {
if (!call.getOrigin().isAnnotatedWith(Secured.class)) {
String message = String.format(
"Method %s is not @Secured", call.getOrigin().getFullName());
events.add(SimpleConditionEvent.violated(call, message));
}
}
}
};
classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods);
If the rule fails, the error message will be built from the supplied descriptions. In the example above, it would be
ルールの評価が失敗したら、指定した説明文からエラーメッセージが構築されます。 前のコード例では次のようなエラーメッセージになります。
classes that have a field annotated with @Payload should only be accessed by @Secured methods
7.4. 定義済みの述語式と条件式(Predefined Predicates and Conditions)
Custom predicates and conditions like in the last section can often be composed from predefined elements.
ArchUnit’s basic convention for predicates is that they are defined in an inner class Predicates
within the type they target.
For example, one can find the predicate to check for the simple name of a JavaClass
as
1つ前のセクションのように、独自の述語式や条件式は定義済みの部品を組み合わせて実現できる場合がよくあります。
ArchUnit の述語式は、基本的な規約として、評価対象のクラス(型)のインナークラス Predicates
として定義するようになっています。
例えば、JavaClass
における「簡潔な名前」という述語式は次のように見つけられます。
JavaClass.Predicates.simpleName(String)
Predicates can be joined using the methods predicate.or(other)
and predicate.and(other)
.
So for example a predicate testing for a class with simple name "Foo" that is serializable
could be created the following way:
述語式は predicate.or(other)
メソッドや predicate.and(other)
メソッドで結合できます。
ですから、簡潔なクラス名が "Foo" なら、シリアライズ可能でなければならない、という述語式は次のように記述できます。
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleName;
DescribedPredicate<JavaClass> serializableNamedFoo =
simpleName("Foo").and(assignableTo(Serializable.class));
Note that for some properties, there exist interfaces with predicates defined for them.
For example the property to have a name is represented by the interface HasName
;
consequently the predicate to check the name of a JavaClass
is the same as the predicate to check the name of a JavaMethod
,
and resides within
一部のプロパティは述語式のために定義されたインターフェイスになっているので注意してください。
例えば、JavaClass
と JavaMethod
のどちらでも、名前をチェックするプロパティは HasName
というインターフェイスになっています。
HasName.Predicates.name(String)
This can at times lead to problems with the type system, if predicates are supposed to be joined.
Since the or(..)
method accepts a type of DescribedPredicate<? super T>
,
where T
is the type of the first predicate. For example:
これは、特に述語式を結合するとき、型システムとしての問題になる場合があります。
or(..)
メソッドの引数の型は DescribePredicate<? super T>
で、T
は先頭の述語式になるからです。
// Does not compile, because type(..) targets a subtype of HasName
HasName.Predicates.name("").and(JavaClass.Predicates.type(Serializable.class))
// Does compile, because name(..) targets a supertype of JavaClass
JavaClass.Predicates.type(Serializable.class).and(HasName.Predicates.name(""))
// Does compile, because the compiler now sees name(..) as a predicate for JavaClass
DescribedPredicate<JavaClass> name = HasName.Predicates.name("").forSubtype();
name.and(JavaClass.Predicates.type(Serializable.class));
This behavior is somewhat tedious, but unfortunately it is a shortcoming of the Java type system that cannot be circumvented in a satisfying way.
この分かりにくい振る舞いは Java の型システムの欠点に起因するもので、残念ながら安全に回避する方法はありません。
Just like predicates, there exist predefined conditions that can be combined in a similar way.
Since ArchCondition
is a less generic concept, all predefined conditions can be found within ArchConditions
.
Examples:
述語式と同じように合成できる定義済みの条件式が、ArchCondition
に定義されています。
ただし、ArchCondition
はジェネリクスの考え方が少し薄くなっています。
ArchCondition<JavaClass> callEquals =
ArchConditions.callMethod(Object.class, "equals", Object.class);
ArchCondition<JavaClass> callHashCode =
ArchConditions.callMethod(Object.class, "hashCode");
ArchCondition<JavaClass> callEqualsOrHashCode = callEquals.or(callHashCode);
7.5. 独自の考え方に基づくルール(Rules with Custom Concepts)
Earlier we stated that most architectural rules take the form
前のセクションでほとんどのアーキテクチャとしてのルールは次のような形式になることを説明しました。
classes that ${PREDICATE} should ${CONDITION}
However, we do not always talk about classes, if we express architectural concepts. We might have custom language, we might talk about modules, about slices, or on the other hand more detailed about fields, methods or constructors. A generic API will never be able to support every imaginable concept out of the box. Thus ArchUnit’s rule API has at its foundation a more generic API that controls the types of objects that our concept targets.
ところが、私たちはアーキテクチャとしての考え方を表現するとき、常にクラスについて言及しているわけではありません。 自作の言語やモジュール、コードの断面について言及する場合もあるし、フィールドやメソッドやコンストラクタの詳細について言及する場合もあるからです。 規定の汎用 API では、全ての想像上の概念を表現することは不可能です。 そのため、ArchUnit のルール API は、評価対象のオブジェクトの型を操作するより汎用性の高い API で構成されています。
To achieve this, any rule definition is based on a ClassesTransformer
that defines how
JavaClasses
are to be transformed to the desired rule input. In many cases, like the ones
mentioned in the sections above, this is the identity transformation, passing classes on to the rule
as they are. However, one can supply any custom transformation to express a rule about a
different type of input object. For example:
あらゆるルール定義は ClassesTransformer
に基づいています。
これは、JavaClasses
を適切なルールの入力へどのように変換するか定義します。
多くの場合、前のセクションで言及したように何も変更せず、クラスそのものをルールに渡すことになります。
しかし、異なる型を要求するルールにオブジェクトを渡せるよう、独自の変換処理を定義する場合もあります。
ClassesTransformer<JavaPackage> packages = new AbstractClassesTransformer<JavaPackage>("packages") {
@Override
public Iterable<JavaPackage> doTransform(JavaClasses classes) {
Set<JavaPackage> result = new HashSet<>();
classes.getDefaultPackage().accept(alwaysTrue(), new PackageVisitor() {
@Override
public void visit(JavaPackage javaPackage) {
result.add(javaPackage);
}
});
return result;
}
};
all(packages).that(containACoreClass()).should(...);
Of course these transformers can represent any custom concept desired:
もちろん、そういった変換で独自の考え方を表現できます。
// how we map classes to business modules
ClassesTransformer<BusinessModule> businessModules = ...
// filter business module dealing with orders
DescribedPredicate<BusinessModule> dealWithOrders = ...
// check that the actual business module is independent of payment
ArchCondition<BusinessModule> beIndependentOfPayment = ...
all(businessModules).that(dealWithOrders).should(beIndependentOfPayment);
7.6. ルールの文章を制御する(Controlling the Rule Text)
If the rule is straight forward, the rule text that is created automatically should be sufficient in many cases. However, for rules that are not common knowledge, it is good practice to document the reason for this rule. This can be done in the following way:
分かりやすいルールなら、自動的に生成されるルールの説明文で十分な場合が多いでしょう。 しかし、共通知識とは言えないルールについて、根拠をドキュメント化しておくのは良いプラクティスです。 例えば、次のように記述できます。
classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods)
.because("@Secured methods will be intercepted, checking for increased privileges " +
"and obfuscating sensitive auditing information");
Nevertheless, the generated rule text might sometimes not convey the real intention concisely enough, e.g. if multiple predicates or conditions are joined. It is possible to completely overwrite the rule description in those cases:
とはいえ、自動的に生成したルールの説明文が、本来の意図を十分に簡潔に表現できていない場合もあるでしょう。 複数の述語式や条件式を結合した場合は特にそうです。 そういうときは、ルールの説明文を全て置き換えることができます。
classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods)
.as("Payload may only be accessed in a secure way");
7.7. 違反を無視する(Ignoring Violations)
In legacy projects there might be too many violations to fix at once. Nevertheless, that code
should be covered completely by architecture tests to ensure that no further violations will
be added to the existing code. One approach to ignore existing violations is
to tailor the that(..)
clause of the rules in question to ignore certain violations.
A more generic approach is to ignore violations based on simple regex matches.
For this one can put a file named archunit_ignore_patterns.txt
in the root of the classpath.
Every line will be interpreted as a regular expression and checked against reported violations.
Violations with a message matching the pattern will be ignored. If no violations are left,
the check will pass.
レガシープロジェクトでは1度に直しきれないほど多数の違反が見つかるものです。
とはいえ、既存のコードに新しい違反を持ち込むことがないよう、違反しているコードもアーキテクチャテストの対象にしなければなりません。
既存の違反を無視する1つのやり方として、ルールの that(..)
節を調整する方法があります。
より汎用的な解決方法は、単純に正規表現で一致させる方法です。
クラスパスのルートに archunit_ignore_patterns.txt
というテキストファイルを配置し、それぞれの行には、報告された違反にマッチする正規表現を記述します。
違反のメッセージがいずれかの正規表現に一致するなら、その違反を無視するようになります。
違反が1つも無くなれば、チェックは成功します。
For example, suppose the class some.pkg.LegacyService
violates a lot of different rules.
It is possible to add
例えば、some.pkg.LegacyService
というクラスがいくつものルールに違反しているとしたら、次のような正規表現を追加すればいいでしょう。
.*some\.pkg\.LegacyService.*
All violations mentioning some.pkg.LegacyService
will consequently be ignored, and rules that
are only violated by such violations will report success instead of failure.
some.pkg.LegacyService
に言及する全ての違反が無視されるようになります。
そうして、他の違反が見つからなければ、失敗ではなく成功と報告するようになります。
It is possible to add comments to ignore patterns by prefixing the line with a '\#':
行頭に \#
を指定することで、正規表現の内容を説明するコメントを記述できます。
# There are many known violations where LegacyService is involved; we'll ignore them all
.*some\.pkg\.LegacyService.*
8. ライブラリ API(The Library API)
The Library API offers a growing collection of predefined rules, which offer a more concise API for more complex but common patterns, like a layered architecture or checks for cycles between slices (compare What to Check).
ライブラリ API は発展途上の定義済みのルール集合を提供します。 レイヤーアーキテクチャや、ソースコード断面の循環チェック(What to Checkも参照)のように、複雑だけど一般的なパターンに関する簡潔な API を提供するのです。
8.1. アーキテクチャ(Architectures)
The entrance point for checks of common architectural styles is:
Architectures
は一般的なアーキテクチャスタイルに関するチェックの入口です。
com.tngtech.archunit.library.Architectures
At the moment this only provides a convenient check for a layered architecture and onion architecture. But in the future it might be extended for styles like a pipes and filters, separation of business logic and technical infrastructure, etc.
現時点では、レイヤーアーキテクチャとオニオンアーキテクチャに関する便利なチェックしか提供していません。 将来的にはパイプ・フィルタースタイルや、「業務ロジックと技術要素の分離(separation of business logic and technical infrastructure)」スタイルにも対応する予定です。
8.1.1. レイヤーアーキテクチャ(Layered Architecture)
In layered architectures, we define different layers and how those interact with each other. An example setup for a simple 3-tier architecture can be found in Layer Checks.
「レイヤーアーキテクチャ」では、いくつものレイヤーを定義し、それぞれのレイヤーのやりとりを定義できます。 単純な3層アーキテクチャの例を Layer Checks で説明しています。
8.1.2. オニオンアーキテクチャ(Onion Architecture)
In an "Onion Architecture" (also known as "Hexagonal Architecture" or "Ports and Adapters"), we can define domain packages and adapter packages as follows.
「オニオンアーキテクチャ」(「ポート・アダプター」あるいは「ヘキサゴナルアーキテクチャ」として知られている)では、ドメインとアダプターのためのパッケージを次のように定義できます。
onionArchitecture()
.domainModels("com.myapp.domain.model..")
.domainServices("com.myapp.domain.service..")
.applicationServices("com.myapp.application..")
.adapter("cli", "com.myapp.adapter.cli..")
.adapter("persistence", "com.myapp.adapter.persistence..")
.adapter("rest", "com.myapp.adapter.rest..");
The semantic follows the descriptions in https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/. More precisely, the following holds:
それぞれの意味については https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/ を参照してください。 ここでは次のような意味を表しています。
-
domain
パッケージ - アプリケーションの中核です。次のような部品を含んでいます-
domainModels
パッケージ - ドメインエンティティを配置します -
domainServices
パッケージ - ドメインエンティティを使用するサービスを配置します
-
-
applicationServices
パッケージ - アプリケーションとユースケースを実行するためのサービスおよび設定を配置します。domain
パッケージに配置した要素を使用しますが、domain
パッケージに配置した要素からapplication
パッケージに配置した要素は使用できません -
adapter
パッケージ - 外部システムやインフラへ接続するためのロジックを配置します。 アダプタは他のアダプタを使用できません。 アダプタはdomain
パッケージやappliacation
パッケージに配置した要素を使用しますが、それぞれのパッケージに配置した要素からadapter
パッケージに配置した要素は使用できません
8.2. ソースコードの断面(Slices)
Currently there are two "slice" rules offered by the Library API. These are basically rules that slice the code by packages, and contain assertions on those slices. The entrance point is:
ライブラリ API は、今のところ「ソースコードの断面(slice)」について2種類の基本的なルールを提供しています。
パッケージに基づいて断面を生成するルールと、それぞれの断面に適用できるチェックからなるルールです。
入口は SlicesRuleDefinition
です。
com.tngtech.archunit.library.dependencies.SlicesRuleDefinition
The API is based on the idea to sort classes into slices according to one or several package infixes, and then write assertions against those slices. At the moment this is for example:
この API が元にしている考え方は、1つ以上のパッケージに配置したクラスをまとめて、共通するチェックを適用する、というものです。 例えば次のように記述できます。
// sort classes by the first package after 'myapp'
// then check those slices for cyclic dependencies
SlicesRuleDefinition.slices().matching("..myapp.(*)..").should().beFreeOfCycles()
// checks all subpackages of 'myapp' for cycles
SlicesRuleDefinition.slices().matching("..myapp.(**)").should().notDependOnEachOther()
// sort classes by packages between 'myapp' and 'service'
// then check those slices for not having any dependencies on each other
SlicesRuleDefinition.slices().matching("..myapp.(**).service..").should().notDependOnEachOther()
If this constraint is too rigid, e.g. in legacy applications where the package structure is rather
inconsistent, it is possible to further customize the slice creation. This can be done by specifying
a mapping of JavaClass
to SliceIdentifier
where classes with the same SliceIdentifier
will
be sorted into the same slice. Consider this example:
パッケージ構造に一貫性のないレガシーアプリケーションのように、この制約が極めて柔軟性に欠けるとしたら、断面の作成自体を詳細化できます。
JavaClass
に対応する SliceIdentifier
を定義し、同じ SliceIdentifier
を同じ断面へ格納させるのです。
具体的には次のように記述できます。
SliceAssignment legacyPackageStructure = new SliceAssignment() {
// this will specify which classes belong together in the same slice
@Override
public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
if (javaClass.getPackageName().startsWith("com.oldapp")) {
return SliceIdentifier.of("Legacy");
}
if (javaClass.getName().contains(".esb.")) {
return SliceIdentifier.of("ESB");
}
// ... further custom mappings
// if the class does not match anything, we ignore it
return SliceIdentifier.ignore();
}
// this will be part of the rule description if the test fails
@Override
public String getDescription() {
return "legacy package structure";
}
};
SlicesRuleDefinition.slices().assignedFrom(legacyPackageStructure).should().beFreeOfCycles()
8.2.1. 循環参照の検出に関する設定(Configurations)
There are two configuration parameters to adjust the behavior of the cycle detection.
They can be configured via archunit.properties
(compare Advanced Configuration).
循環参照の検出に影響する2種類の設定パラメータがあります。
いずれも archunit.properties
で設定できます。(Advanced Configuration を参照)
# This will limit the maximum number of cycles to detect and thus required CPU and heap.
# default is 100
cycles.maxNumberToDetect=50
# This will limit the maximum number of dependencies to report per cycle edge.
# Note that ArchUnit will regardless always analyze all dependencies to detect cycles,
# so this purely affects how many dependencies will be printed in the report.
# Also note that this number will quickly affect the required heap since it scales with number.
# of edges and number of cycles
# default is 20
cycles.maxNumberOfDependenciesPerEdge=5
8.3. 一般的なコーディングルール(General Coding Rules)
The Library API also offers a small set of coding rules that might be useful in various projects. Those can be found within
ライブラリ API は様々なプロジェクトで利用できるであろうコーディングルールを少しだけ提供しています。
com.tngtech.archunit.library
パッケージを参照してください。
com.tngtech.archunit.library
8.3.1. GeneralCodingRules
The class GeneralCodingRules
contains a set of very general rules and conditions for coding.
For example:
GeneralCodingRules
クラスはコーディングに関する一般的なルールや条件式を含みます。
-
System.out
やSystem.err
を使わずに、ロギング API を使用していることのチェック -
汎用的な例外を送出せず、具体的な例外を送出していることのチェック
-
java.util.logging
を使わずに、Log4j や Logback や SLF4J などのライブラリを使用していることのチェック -
フィールドインジェクションを使わずに、コンストラクタインジェクションを使用していることのチェック
8.3.2. DependencyRules
The class DependencyRules
contains a set of rules and conditions for checking dependencies between classes.
For example:
DependencyRules
クラスはクラス間の依存関係に関するルールや条件式を含みます。
-
To check that classes do not depend on classes from upper packages.
-
上位層のパッケージに配置したクラスに依存していないことのチェック
8.3.3. ProxyRules
The class ProxyRules
contains a set of rules and conditions for checking the usage of proxy objects.
For example:
ProxyRules
クラスはプロキシオブジェクトの使い方に関するルールや条件式を含みます。
-
To check that methods that matches a predicate are not called directly from within the same class.
-
述語式にマッチするメソッドを、同じクラスから直接呼び出してしないことのチェック
8.4. PlantUML のコンポーネント図をルールとして扱う(PlantUML Component Diagrams as rules)
The Library API offers a feature that supports PlantUML diagrams. This feature is located in
ライブラリ API は PlantUML の図を扱うことができます。
com.tngtech.archunit.library.plantuml
パッケージを参照してください。
com.tngtech.archunit.library.plantuml
ArchUnit can derive rules straight from PlantUML diagrams and check to make sure that all imported
JavaClasses
abide by the dependencies of the diagram. The respective rule can be created in the following way:
ArchUnit は PlantUML の図からルールを生成し、インポートした全ての JavaClasses
が図に表現されている依存関係を満たしているかチェックできます。
次のように記述できます。
URL myDiagram = getClass().getResource("my-diagram.puml");
classes().should(adhereToPlantUmlDiagram(myDiagram, consideringAllDependencies()));
Diagrams supported have to be component diagrams and associate classes to components via stereotypes.
The way this works is to use the respective package identifiers (compare
ArchConditions.onlyHaveDependenciesInAnyPackage(..)
) as stereotypes:
対応している図の種類はコンポーネント図です。コンポーネントに関連するクラスはステレオタイプで表現します。
ステレオタイプはパッケージ識別子として機能することになります(ArchConditions.onlyHaveDependenciesInAnyPackage(..)
を参照)。
@startuml
[Some Source] <<..some.source..>>
[Some Target] <<..some.target..>> as target
[Some Source] --> target
@enduml
Consider this diagram applied as a rule via adhereToPlantUmlDiagram(..)
, then for example
a class some.target.Target
accessing some.source.Source
would be reported as a violation.
この図を adhereToPlantUmlDiagram(..)
へ適用して生成したルールは、some.target.Target
クラスから some.source.Source
クラスへのアクセスを発見すると、違反として報告するようになります。
8.4.1. PlantUML に関する設定(Configurations)
There are different ways to deal with dependencies of imported classes not covered by the
diagram at all. The behavior of the PlantUML API can be configured by supplying a respective
Configuration
:
PlantUML の図に表現されていないクラスの依存関係を扱う方法はいろいろあります。
PlantUML API の振る舞いは Configuration
を指定して制御できます。
// considers all dependencies possible (including java.lang, java.util, ...)
classes().should(adhereToPlantUmlDiagram(
mydiagram, consideringAllDependencies()))
// considers only dependencies specified in the PlantUML diagram
// (so any unknown dependency will be ignored)
classes().should(adhereToPlantUmlDiagram(
mydiagram, consideringOnlyDependenciesInDiagram()))
// considers only dependencies in any specified package
// (control the set of dependencies to consider, e.g. only com.myapp..)
classes().should(adhereToPlantUmlDiagram(
mydiagram, consideringOnlyDependenciesInAnyPackage("..some.package..")))
It is possible to further customize which dependencies to ignore:
無視する依存関係を詳しく指定できます。
// there are further ignore flavors available
classes().should(adhereToPlantUmlDiagram(mydiagram).ignoreDependencies(predicate))
A PlantUML diagram used with ArchUnit must abide by a certain set of rules:
ArchUnit から使用する PlantUML の図は、次のようなルールに従わなければなりません。
-
コンポーネントは角括弧形式で表記しなければなりません(
[Some Component]
) -
コンポーネントには1つ以上(複数も可)のステレオタイプが必要です。図中のステレオタイプ名は一意で、適切なパッケージ識別子でなければなりません(例えば
<<..example..>>
。..
は任意のパッケージ階層を表す。詳しくは The Core API を参照) -
コンポーネントには別名を指定できます(
[Some Component] <<..example..>> as myalias
)。別名に使用できるのは英数字のみで、クォートで囲んではいけません -
コンポーネントには色を指定できます(
[Some Component] <<..example..>> \#OrangeRed
) -
依存関係を表す線分に使えるのは矢印付きの破線だけです(
-->
) -
依存関係を表す線分の方向は左から右(
-->
)、あるいは、右から左のみです(<--
) -
依存関係を表す線分には任意の数の破線を使えます(
->
や----->
) -
依存関係を表す線分には方向を示すヒントを指定したり(
-up->
)、色を指定できます(-[\#green]->
)
You can compare this diagram of ArchUnit-Examples.
diagram of ArchUnit-Examples の図を参照してみてください。
8.5. アーキテクチャルール違反の永続化(Freezing Arch Rules)
When rules are introduced in grown projects, there are often hundreds or even thousands of violations, way too many to fix immediately. The only way to tackle such extensive violations is to establish an iterative approach, which prevents the code base from further deterioration.
発展中のプロジェクトにルールを導入すると、数百どころか数千の違反を検出してしまい、とてもすぐに修正できない状態になる場合があります。 大量に違反が発生している状況を解決するには、さらに悪化するのを防ぐため反復的なアプローチを確立するしかありません。
FreezingArchRule
can help in these scenarios by recording all existing violations to a ViolationStore
.
Consecutive runs will then only report new violations and ignore known violations.
If violations are fixed, FreezingArchRule
will automatically reduce the known stored violations to prevent any regression.
そういう場合に役立つのが FreezingArchRule
です。
FreezingArchRule
は検出済みの全ての違反を ViolationStore
へ記録します。
そうすると、一度記録した違反は無視して、新たに検出した違反だけを報告するようになります。
記録済みの違反を修正すると、FreezingArchRule
はリグレッションを防ぐため自動的に記録済みの違反を削除します。
8.5.1. 使い方(Usage)
To freeze an arbitrary ArchRule
just wrap it into a FreezingArchRule
:
使い方は、永続化したい任意の ArchRule
を FreezingArchRule
で包み込むだけです。
ArchRule rule = FreezingArchRule.freeze(classes().should()./*complete ArchRule*/);
On the first run all violations of that rule will be stored as the current state. On consecutive runs only
new violations will be reported. By default FreezingArchRule
will ignore line numbers, i.e. if a
violation is just shifted to a different line, it will still count as previously recorded
and will not be reported.
導入してから始めて実行したテストで検出した違反は、最新の状態として記録されます。
その後は、新しく検出した違反だけを報告するようになります。
初期設定では FreezingArchRule
は行番号を無視します。
つまり、違反した箇所が数行移動しただけでも、最初に記録した違反と同じ違反として認識し、報告しないということです。
8.5.2. 設定(Configuration)
By default FreezingArchRule
will use a simple ViolationStore
based on plain text files.
This is sufficient to add these files to any version control system to continuously track the progress.
You can configure the location of the violation store within archunit.properties
(compare Advanced Configuration):
初期設定の FreezingArchRule
は平文テキストファイルを使用する単純な ViolationStore
を使うようになっています。
継続的に状況を追跡するなら、テキストファイルをバージョン管理システムへ登録するだけで十分でしょう。
違反記録ファイルの場所は archunit.properties
で指定できます(Advanced Configuration を参照)。
freeze.store.default.path=/some/path/in/a/vcs/repo
Furthermore, it is possible to configure
他にも次のような設定項目があります。
# must be set to true to allow the creation of a new violation store
# default is false
freeze.store.default.allowStoreCreation=true
# can be set to false to forbid updates of the violations stored for frozen rules
# default is true
freeze.store.default.allowStoreUpdate=false
This can help in CI environments to prevent misconfiguration: For example, a CI build should probably never create a new the violation store, but operate on an existing one.
これらの設定項目は CI 環境で実行するときの設定誤りを予防するために役立ちます。 具体的には、CI環境で実行するビルドは新しい違反記録ファイルを作るべきではないし、既存のファイルがあるならそれを参照するべきなのです。
As mentioned in Overriding configuration, these properties can be passed as system properties as needed. For example to allow the creation of the violation store in a specific environment, it is possible to pass the system property via
Overriding configuration で説明したように、これらの設定項目は必要ならシステムプロパティとして指定できるようになっています。 例えば、任意の環境で違反記録ファイルを作成させたいときは、次のようなシステムプロパティを指定すればいいでしょう。
-Darchunit.freeze.store.default.allowStoreCreation=true
It is also possible to allow all violations to be "refrozen", i.e. the store will just be updated
with the current state, and the reported result will be success. Thus, it is effectively the same behavior
as if all rules would never have been frozen.
This can e.g. make sense, because current violations are consciously accepted and should be added to the store,
or because the format of some violations has changed. The respective property to allow refreezing
all current violations is freeze.refreeze=true
, where the default is false
.
全ての違反を「再凍結(refrozen)」させることも可能です。
そうすると、違反記録ファイルを最新の状態へ更新し、実行結果は成功として終了します。
実質的に全てのルールを永続化していないときの振る舞いと同じになります。
これに意味があるのは、今の違反状況を認識し、受け入れており、違反記録ファイルへ追加してもよいと考えている場合だけです。
前から存在していた違反の形式が変化しているのかもしれません。
今の違反状態を再凍結させるにはシステムプロパティ freez.refreeze=true
を指定します。初期値は false
です。
8.5.3. 拡張可能な部分について(Extension)
FreezingArchRule
provides two extension points to adjust the behavior to custom needs.
The first one is the ViolationStore
, i.e. the store violations will be recorded to. The second one
is the ViolationLineMatcher
, i.e. how FreezingArchRule
will associate lines of stored violations
with lines of actual violations. As mentioned, by default this is a line matcher that ignores the
line numbers of violations within the same class.
FreezingArchRule
には拡張可能な部分が2か所あり、振る舞いを調整するのに使用できます。
1つ目は ViolationStore
で、違反の情報をどこに記録するのか制御します。
2つ目は ViolationLineMatcher
で、FreezingArchRule
が違反を記録するとき、行番号を含めるかどうかを制御します。
前述したとおり、初期設定では行番号を記録しないようになっています。
違反記録ストア(Violation Store)
As mentioned in Configuration, the default ViolationStore
is a simple text based store.
It can be exchanged though, for example to store violations in a database.
To provide your own implementation, implement com.tngtech.archunit.library.freeze.ViolationStore
and
configure FreezingArchRule
to use it. This can either be done programmatically:
Configuration で説明したとおり、初期設定の ViolationStore
は単純に平文テキストファイルへ記録するようになっています。
ファイルではなく、データベースへ記録させることができるということです。
自分で実装するなら、com.thgtech.archunit.library.freeze.ViolationStore
インターフェイスを実装したクラスを FreezingArchRule
に指定すればいいでしょう。
次のようにプログラムで記述できます。
FreezingArchRule.freeze(rule).persistIn(customViolationStore);
Alternatively it can be configured via archunit.properties
(compare Advanced Configuration):
あるいは、次のように archunit.properties
の設定項目にも記述できます(Advanced Configuration を参照)。
freeze.store=fully.qualified.name.of.MyCustomViolationStore
You can supply properties to initialize the store by using the namespace freeze.store
.
For properties
名前空間 freeze.store
に、違反記録ストアを初期化するためのプロパティを指定できます。
freeze.store.propOne=valueOne
freeze.store.propTwo=valueTwo
the method ViolationStore.initialize(props)
will be called with the properties
これらのプロパティは VilationStore.initialize(props)
の引数に指定されます。
propOne=valueOne
propTwo=valueTwo
違反行マッチャー(Violation Line Matcher)
The ViolationLineMatcher
compares lines from occurred violations with lines from the store.
The default implementation ignores line numbers and numbers of anonymous classes or lambda expressions,
and counts lines as equivalent when all other details match.
A custom ViolationLineMatcher
can again either be defined programmatically:
VilationLineMatcher
は違反と、違反の発生した行番号を、違反記録ストアと照合します。
既定の実装では、行番号と、無名クラスやラムダ式の数を無視して、全体の行数が一致すればそれ以外の全ての詳細も一致したものと見做すようになっています。
独自の ViolationLineMatcher
があるときは、次のようにプログラムで記述できます。
FreezingArchRule.freeze(rule).associateViolationLinesVia(customLineMatcher);
or via archunit.properties
:
あるいは、次のように archunit.properties
の設定項目にも記述できます(Advanced Configuration を参照)。
freeze.lineMatcher=fully.qualified.name.of.MyCustomLineMatcher
8.6. ソフトウェアアーキテクチャのメトリクス(Software Architecture Metrics)
Similar to code quality metrics, like cyclomatic complexity or method length,
software architecture metrics strive to measure the structure and design of software.
ArchUnit can be used to calculate some well-known software architecture metrics.
The foundation of these metrics is generally some form of componentization, i.e.
we partition the classes/methods/fields of a Java application into related units
and provide measurements for these units. In ArchUnit this concept is expressed by
com.tngtech.archunit.library.metrics.MetricsComponent
. For some metrics, like the
Cumulative Dependency Metrics by John Lakos, we also need to know the dependencies
between those components, which are naturally derived from the dependencies between
the elements (e.g. classes) within these components.
ソースコード品質のメトリクスにおけるサイクロマティック複雑度やメソッド長のように、ソフトウェアアーキテクチャのメトリクスとして、ソフトウェアの設計や構造を計測する試みが続けられています。
ArchUnit は既知のソフトウェアアーキテクチャメトリクスの一部を算出できるようになっています。
メトリクスの根拠になるのは、基本的になんらかの形でコンポーネント化に貢献する要素です。
例えば、私たちは Java アプリケーションをクラス、メソッド、フィールドなどの単位に分割し、それぞれの単位がどれだけあるのか計測します。
ArchUnit ではこの考え方を com.tngtech.archunit.library.metrics.MetricsCOmponent
として表現しています。
ジョン・レイコスの提案した「累積依存性メトリクス(Cumulative Dependency Metrics)」のように、一部のメトリクスでは、コンポーネント間の依存関係に関する情報が必要になります。
これは、コンポーネントに含まれる全ての要素(クラスなど)に関する依存関係から導出できます。
A very simple concrete example would be to consider some Java packages as components and the classes within these packages as the contained elements. From the dependencies between the classes we can derive which package depends on which other package.
簡単な具体例を考えると、Java パッケージをコンポーネントとした場合、コンポーネントの構成要素はクラスになります。 従って、全てのクラスの依存関係が分かれば、そのパッケージが他のパッケージにどれだけ依存しているのか導出できるのです。
The following will give a quick overview of the metrics that ArchUnit can calculate. However, for further background information it is recommended to rely on some dedicated literature that explains these metrics in full detail.
ArchUnit で計算できるメトリクスの例は次のとおりです。 それぞれのメトリクスに関する正確な説明が必要なときは、出典の文献を参照してください。
8.6.1. ジョン・レイコスの提案した「累積依存性メトリクス」(Cumulative Dependency Metrics by John Lakos)
These are software architecture metrics as defined by John Lakos in his book
"Large-Scale C++ Software Design". The basic idea is to calculate the DependsOn
value for each component, which is the sum of all components that can be
transitively reached from some component including the component itself.
From these values we can derive
これは、ジョン・レイコスが「Large-Scale C++ Software Design」でソフトウェアアーキテクチャのメトリクスです。
それぞれのコンポーネントについて DependsOn
を集計するのが基本的な考え方です。
末端のコンポーネントそれ自体を含むコンポーネントの一部から、推移的に到達可能なコンポーネントの依存関係の総和を、全てのコンポーネントの依存関係の総和と見做す考え方です。
次のような指標を導出できます。
-
CCD - 累積コンポーネント依存性: 全てのコンポーネントに関する
DependsOn
の総和。 -
ACD - 平均コンポーネント依存性:
CCD
を全てのコンポーネント数で除算した値。 -
RACD - 相対平均コンポーネント依存性:
ACD
を全てのコンポーネント数で除算した値。 -
NACD - 正規化平均コンポーネント依存性: システムの
CCD
を、同じ数のコンポーネントからなる平衡二分木のCCD
で除算した値。
具体例(Example)
Thus these metrics provide some insights into the complexity of the dependency graph of a system.
Note that in a cycle all elements have the same DependsOn
value which will lead to an increased
CCD. In fact for any non-trivial (n >= 5
) acyclic graph of components the RACD is bound by 0.6
.
これらのメトリクスは、システムの依存関係グラフの複雑さに関する洞察をもたらします。
巡回グラフの全ての要素の DependsOn
が同じ値になると、CCD は増加するので注意が必要です。
実際には、任意の自明でない非巡回グラフ(n >= 5
)では、RACD の上限値は 0.6
になります。
API の使い方(How to use the API)
The values described for these metrics can be calculated in the following way:
それぞれのメトリクスは次のように算出できます。
import com.tngtech.archunit.library.metrics.ArchitectureMetrics;
// ...
JavaClasses classes = // ...
Set<JavaPackage> packages = classes.getPackage("com.example").getSubpackages();
// These components can also be created in a package agnostic way, compare MetricsComponents.from(..)
MetricsComponents<JavaClass> components = MetricsComponents.fromPackages(packages);
LakosMetrics metrics = ArchitectureMetrics.lakosMetrics(components);
System.out.println("CCD: " + metrics.getCumulativeComponentDependency());
System.out.println("ACD: " + metrics.getAverageComponentDependency());
System.out.println("RACD: " + metrics.getRelativeAverageComponentDependency());
System.out.println("NCCD: " + metrics.getNormalizedCumulativeComponentDependency());
8.6.2. ロバート・C・マーチンの提案したコンポーネント依存性メトリクス(Component Dependency Metrics by Robert C. Martin)
These software architecture metrics were defined by Robert C. Martin in various sources, for example in his book "Clean architecture : a craftsman’s guide to software structure and design".
ロバート・C・マーチンはさまざまなソフトウェアアーキテクチャのメトリクスをいろんなところで紹介しています。 例えば「Clean archtecture : a craftsman’s guide to software structure and design」など。
The foundation are again components, that must in this case contain classes as their elements (i.e. these are purely object-oriented metrics that need a concept of abstract classes).
The metrics are based on the following definitions:
対象はやはりコンポーネントで、構成要素としてクラスを持っていることになっています(ここで紹介するメトリクスは、抽象クラスの考え方が必須の純粋なオブジェクト指向ソフトウェアのメトリクスです)。
それぞれのメトリクスは次のような定義に基づいています。
-
外向きの結合(Efferent Coupling)(Ce): 他のコンポーネントへ向かう外向きの依存関係の総数
-
内向きの結合(Afferent Coupling)(Ca): 他のコンポーネントから向かってくる内向きの依存関係の総数
-
不安定性(Instability)(I):
Ce / (Ca + Ce)
全ての依存関係に対する外向きの依存関係の割合 -
抽象度(Abstractness)(A):
num(抽象クラス) / num(全てのクラス)
コンポーネント中の全てのクラスに対する抽象クラスの割合 -
主系列からの距離(Distance from Main Sequence)(D):
| A + I - 1 |
理想直線(A=1, I=0)
と(A=0, I=1)
からの正規化した距離
Note that ArchUnit slightly differs from the original definition. In ArchUnit the Abstractness value is only based on public classes, i.e. classes that are visible from the outside. The reason is that Ce, Ca and I all are metrics with respect to coupling of components. But only classes that are visible to the outside can affect coupling between components, so it makes sense to only consider those classes to calculate the A value.
ArchUnit の実装は本来の定義とはやや異なる内容になっているので注意してください。 ArchUnit では、抽象度を算出するのに、public クラス(つまり外部から参照できるクラス)だけを集計しています。 抽象度以外の全てのメトリクスはコンポーネント間の結合に関するメトリクスなので、コンポーネント間の結合に寄与するクラスだけを対象にするほうが適切だと考えたからです。
具体例(Example)
The following provides some example where the A
values assume some random factor
of abstract classes within the respective component.
次の例では、コンポーネント中にランダムな数の抽象クラスが存在するものとして、抽象度 A
を算出しています。
API の使い方(How to use the API)
The values described for these metrics can be calculated in the following way:
それぞれのメトリクスは次のように算出できます。
import com.tngtech.archunit.library.metrics.ArchitectureMetrics;
// ...
JavaClasses classes = // ...
Set<JavaPackage> packages = classes.getPackage("com.example").getSubpackages();
// These components can also be created in a package agnostic way, compare MetricsComponents.from(..)
MetricsComponents<JavaClass> components = MetricsComponents.fromPackages(packages);
ComponentDependencyMetrics metrics = ArchitectureMetrics.componentDependencyMetrics(components);
System.out.println("Ce: " + metrics.getEfferentCoupling("com.example.component"));
System.out.println("Ca: " + metrics.getAfferentCoupling("com.example.component"));
System.out.println("I: " + metrics.getInstability("com.example.component"));
System.out.println("A: " + metrics.getAbstractness("com.example.component"));
System.out.println("D: " + metrics.getNormalizedDistanceFromMainSequence("com.example.component"));
8.6.3. ハーバート・ドワリルの提案した可視性メトリクス(Visibility Metrics by Herbert Dowalil)
These software architecture metrics were defined by Herbert Dowalil in his book "Modulare Softwarearchitektur: Nachhaltiger Entwurf durch Microservices, Modulithen und SOA 2.0". They provide a measure for the Information Hiding Principle, i.e. the relation of visible to hidden elements within a component.
The metrics are composed from the following definitions:
これらのソフトウェアアーキテクチャのメトリクスは、ハーバート・ドワリルが「Modulare Softwarearchitektur: Nachhaltiger Entwurf durch Microservices, Modulithen und SOA 2.0」で提案したものです。 情報隠蔽の原則に基づく指標として、コンポーネント中の可視要素から不可視要素への関連などを提供します。
それぞれのメトリクスは次のような定義に基づいています。
-
相対可視性(RV):
num(可視要素) / num(全ての要素)
それぞれのコンポーネントについて計算します -
平均相対可視性(ARV): 全てのコンポーネントに関する RV の平均値
-
全体相対可視性(GRV):
num(可視要素) / num(全ての要素)
全てのコンポーネントについて計算します
API の使い方(How to use the API)
The values described for these metrics can be calculated in the following way:
それぞれのメトリクスは次のように算出できます。
import com.tngtech.archunit.library.metrics.ArchitectureMetrics;
// ...
JavaClasses classes = // ...
Set<JavaPackage> packages = classes.getPackage("com.example").getSubpackages();
// These components can also be created in a package agnostic way, compare MetricsComponents.from(..)
MetricsComponents<JavaClass> components = MetricsComponents.fromPackages(packages);
VisibilityMetrics metrics = ArchitectureMetrics.visibilityMetrics(components);
System.out.println("RV : " + metrics.getRelativeVisibility("com.example.component"));
System.out.println("ARV: " + metrics.getAverageRelativeVisibility());
System.out.println("GRV: " + metrics.getGlobalRelativeVisibility());
9. JUnit 拡張機能(JUnit Support)
At the moment ArchUnit offers extended support for writing tests with JUnit 4 and JUnit 5. This mainly tackles the problem of caching classes between test runs and to remove some boilerplate.
Consider a straight forward approach to write tests:
現時点の ArchUnit は、JUnit 4 と JUnit 5 でテストを書きやすくするための機能を提供しています。 主な目的は、実行するテストの間でクラスをキャッシュする仕組みを提供し、典型的な準備コードを不要にすることです。
まず、単純なテストを書く方法について検討してみましょう。
@Test
public void rule1() {
JavaClasses importedClasses = new ClassFileImporter().importClasspath();
ArchRule rule = classes()...
rule.check(importedClasses);
}
@Test
public void rule2() {
JavaClasses importedClasses = new ClassFileImporter().importClasspath();
ArchRule rule = classes()...
rule.check(importedClasses);
}
For bigger projects, this will have a significant performance impact, since the import can take
a noticeable amount of time. Also rules will always be checked against the imported classes, thus
the explicit call of check(importedClasses)
is bloat and error prone (i.e. it can be forgotten).
プロジェクトが大きくなってくると、このテストはクラスのインポートが完了するまでの時間がとても長くなってしまいます。
また、ルールはインポートした全てのクラスをチェックするため、check(importedClasses)
の呼び出しは多数のクラスをチェックすることになり、間違いを見逃しやすくなってしまいます(あるいは忘れやすくなってしまいます)。
9.1. JUnit 4 および JUnit 5 への対応(JUnit 4 & 5 Support)
Make sure you follow the installation instructions at Installation, in particular to include the correct dependency for the respective JUnit support.
Installation に記載したインストールの手順に従い、JUnit のバージョンに合わせて適切な依存ライブラリを追加してください。
9.1.1. テストを書く(Writing tests)
Tests look and behave very similar between JUnit 4 and 5. The only difference is, that with JUnit 4
it is necessary to add a specific Runner
to take care of caching and checking rules, while JUnit 5
picks up the respective TestEngine
transparently. A test typically looks the following way:
JUnit 4 と JUnit 5 のテストは見た目も振る舞いも非常によく似ています。
違いは、JUnit 4 でキャッシュやルールをチェックする仕組みを有効にするには、特定の Runner
を指定しなければならないところです。
JUnit 5 なら適切な TestEngine
を透過的に選択するだけです。
いずれにしてもテストは次のような形式になります。
@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!!
@AnalyzeClasses(packages = "com.myapp")
public class ArchitectureTest {
// ArchRules can just be declared as static fields and will be evaluated
@ArchTest
public static final ArchRule rule1 = classes().should()...
@ArchTest
public static final ArchRule rule2 = classes().should()...
@ArchTest
public static void rule3(JavaClasses classes) {
// The runner also understands static methods with a single JavaClasses argument
// reusing the cached classes
}
}
The JavaClass
cache will work in two ways. On the one hand it will cache the classes by test,
so they can be reused by several rules declared within the same class. On the other hand, it
will cache the classes by location, so a second test that wants to import classes from the same
URLs will reuse the classes previously imported as well. Note that this second caching uses
soft references, so the classes will be dropped from memory, if the heap runs low.
For further information see Controlling the Cache.
JavaClass
のキャッシュは2種類の場面で仕事します。
1つ目は、テストメソッドで扱っているクラスをキャッシュすることです。
同じテストクラスにフィールドとして定義したいくつものルールで再利用できるのです。
2つ目は、位置に対応するクラスをキャッシュすることです。
同じURLからインポートする2つ目のテストでは、前にインポートしたクラスを再利用できるのです。
ただし、このキャッシュはソフト参照を利用しているため、クラスがヒープメモリから追い出されると無効になってしまいます。
より詳しく知りたければ Controlling the Cache を参照してください。
9.1.2. インポートの制御(Controlling the Import)
Which classes will be imported can be controlled in a declarative way through @AnalyzeClasses
.
If no packages or locations are provided, the whole classpath will be imported.
You can specify packages to import as strings:
インポートするクラスは @AnalyzeClasses
により宣言的に指定できるようになっています。
パッケージや位置を指定しなかった場合、クラスパス上の全てのクラスがインポートされることになります。
パッケージは文字列で指定できます。
@AnalyzeClasses(packages = {"com.myapp.subone", "com.myapp.subtwo"})
To better support refactorings, packages can also be declared relative to classes, i.e. the packages these classes reside in will be imported:
リファクタリングを楽にするため、パッケージを指定するときはクラスに対する相対的な要素として指定するといいでしょう。 次のようにクラスを指定すると、クラスの所属するパッケージと、そのサブパッケージをインポートするようになります。
@AnalyzeClasses(packagesOf = {SubOneConfiguration.class, SubTwoConfiguration.class})
As a third option, locations can be specified freely by implementing a LocationProvider
:
もう1つの選択肢は、LocationProvider
を実装して位置を自由に制御することです。
public class MyLocationProvider implements LocationProvider {
@Override
public Set<Location> get(Class<?> testClass) {
// Determine Locations (= URLs) to import
// Can also consider the actual test class, e.g. to read some custom annotation
}
}
@AnalyzeClasses(locations = MyLocationProvider.class)
Furthermore to choose specific classes beneath those locations, ImportOptions
can be
specified (compare The Core API). For example, to import the classpath, but only consider
production code, and only consider code that is directly supplied and does not come from JARs:
また、位置を指定しつつクラスも指定したければ ImportOptions
が利用できます(The Core API を参照)。
例えば、クラスパスからインポートするけど、テストに含まれない製品コードや、JARファイルに含まないけど参照可能なコードを参照したい場合は次のように記述します。
@AnalyzeClasses(importOptions = {DoNotIncludeTests.class, DoNotIncludeJars.class})
As explained in The Core API, you can write your own custom implementation of ImportOption
and then supply the type to @AnalyzeClasses
.
The Core API で説明したように、@AnalyzeClasses
には、自分で実装した ImportOption
を指定できます。
9.1.3. キャッシュの制御(Controlling the Cache)
By default all classes will be cached by location. This means that between different test class runs imported Java classes will be reused, if the exact combination of locations has already been imported.
初期設定では、全てのクラスがインポートした位置と共にキャッシュされます。 つまり、複数のテストクラスで同じ位置からインポートしている場合、キャッシュを再利用できるのです。
If the heap runs low, and thus the garbage collector has to do a big sweep in one run, this can cause a noticeable delay. On the other hand, if it is known that no other test class will reuse the imported Java classes, it would make sense to deactivate this cache.
利用できるヒープメモリが狭すぎる場合や、GC のスイープ処理に時間がかかりすぎる場合は、無視できない遅れが発生します。 一方、インポートしたクラスを再利用するテストクラスが他に存在しないことが分かっているなら、キャッシュを破棄できることになります。
This can be achieved by configuring CacheMode.PER_CLASS
, e.g.
@AnalyzeClasses(packages = "com.myapp.special", cacheMode = CacheMode.PER_CLASS)
The Java classes imported during this test run will not be cached by location and just be reused within the same test class. After all tests of this class have been run, the imported Java classes will simply be dropped.
このテストを実行してインポートした Java クラスは、位置に対してキャッシュしませんし、同じテストクラスの中でしか再利用しません。 このテストクラスの全てのテストが完了したら、インポートした Java クラスは単純に捨てることができます。
9.1.4. テストの無視(Ignoring Tests)
It is possible to skip tests by annotating them with @ArchIgnore
, for example:
@ArcIgnore
アノテーションを指定するとテストをスキップできます。
public class ArchitectureTest {
// will run
@ArchTest
public static final ArchRule rule1 = classes().should()...
// won't run
@ArchIgnore
@ArchTest
public static final ArchRule rule2 = classes().should()...
}
Note for users of JUnit 5: the annotation @Disabled
has no effect here.
Instead, @ArchIgnore
should be used.
JUnit 5 を使っている場合は @Disabled
アノテーションが無効になるので注意してください。
あくまでも @ArchIgnore
を指定しなければなりません。
9.1.5. ルールのグループ化(Grouping Rules)
Often a project might end up with different categories of rules, for example "service rules" and "persistence rules". It is possible to write one class for each set of rules, and then refer to those sets from another test:
最終的にプロジェクトのルールはさまざまな分類に落ち着くことでしょう。 例えば、「サービスのためのルール」とか「永続化層のためのルール」とか。 それぞれの分類に対応するルールを1つのクラスとして記述し、テストから参照できます。
public class ServiceRules {
@ArchTest
public static final ArchRule ruleOne = ...
// further rules
}
public class PersistenceRules {
@ArchTest
public static final ArchRule ruleOne = ...
// further rules
}
@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!!
@AnalyzeClasses
public class ArchitectureTest {
@ArchTest
static final ArchTests serviceRules = ArchTests.in(ServiceRules.class);
@ArchTest
static final ArchTests persistenceRules = ArchTests.in(PersistenceRules.class);
}
The runner will include all @ArchTest
annotated members within ServiceRules
and PersistenceRules
and evaluate
them against the classes declared within @AnalyzeClasses
on ArchitectureTest
.
This also allows an easy reuse of a rule library in different projects or modules.
テストランナーは ServiceRules
と PersistenceRules
から @ArchTest
アノテーションで修飾した全てのメンバーを抽出し、ArchitectureTest
の @AnalyzeClasses
で指定した全てのクラスに対して評価します。
他のプロジェクトやモジュールをルールライブラリとして再利用するのも簡単でしょう。
10. 高度な設定(Advanced Configuration)
Some behavior of ArchUnit can be centrally configured by adding a file archunit.properties
to the root of the classpath (e.g. under src/test/resources
).
This section will outline some global configuration options.
ArchUnit の振る舞いの一部は archunit.properties
をクラスパスのルートに配置すると集中管理できるようになります(たとえば src/test/resources
に配置するといいでしょう)。
このセクションでは大域的な設定オプションについて解説します。
10.1. 設定の上書き(Overriding configuration)
ArchUnit will use exactly the archunit.properties
file returned by the context
ClassLoader
from the classpath root, via the standard Java resource loading mechanism.
ArchUnit は、Java の標準的なリソース読み取りの仕組みに従い、ClassLoader
の発見したクラスパスのルートに配置されている archunit.properties
の内容そのものを参照します。
It is possible to override any property from archunit.properties
, by passing a system property
to the respective JVM process executing ArchUnit:
archunit.properties
で指定した設定項目は、ArchUnit を実行する JVM のシステムプロパティで上書きできます。
-Darchunit.propertyName=propertyValue
E.g. to override the property resolveMissingDependenciesFromClassPath
described in the next section, it would be possible to pass:
例えば、次のセクションで説明する設定項目 resolveMissingDependenciesFromClassPath
を上書きするときは、次のように指定します。
-Darchunit.resolveMissingDependenciesFromClassPath=false
10.2. 解決のための振る舞いの構成(Configuring the Resolution Behavior)
As mentioned in Dealing with Missing Classes, it might be preferable to configure a different import behavior if dealing with missing classes wastes too much performance. One way that can be chosen out of the box is to never resolve any missing class from the classpath:
Dealing with Missing Classes で説明したように、存在しないクラスの扱いが重大な性能劣化に影響する場合、別のインポートの振る舞いをさせるほうが望ましいです。 1つの方法として、存在しないクラスをクラスパスから探索しない組み込みの振る舞いを選択できます。
resolveMissingDependenciesFromClassPath=false
If you want to resolve just some classes from the classpath (e.g. to import missing classes from your own organization but avoid the performance impact of importing classes from 3rd party packages), it is possible to configure only specific packages to be resolved from the classpath:
クラスパスから解決させたいクラスがいくつか存在するなら、特定のパッケージだけをクラスパスから解決させるように構成できます(例えば、あなたの会社内で利用しているクラスをクラスパスから解決させたいけど、サードパーティのパッケージを探索して性能劣化させたくない場合など)。
classResolver=com.tngtech.archunit.core.importer.resolvers.SelectedClassResolverFromClasspath
classResolver.args=some.pkg.one,some.pkg.two
This configuration would only resolve the packages some.pkg.one
and some.pkg.two
from the
classpath, and stub all other missing classes.
このように設定すると、some.pkg.one
と some.pkg.two
のパッケージに所属するクラスをクラスパスから解決し、それ以外のクラスはスタブにすることができます。
The last example also demonstrates, how the behavior can be customized freely, for example if classes are imported from a different source and are not on the classpath:
最後の例では、振る舞いの構成の自由度を示すことにします。 次のように設定すると、クラスパスではない別のファイルシステムパスからクラスをインポートできるのです。
First Supply a custom implementation of
まず、ClassResolver
を実装したクラスを用意します。
com.tngtech.archunit.core.importer.resolvers.ClassResolver
Then configure it
そして次のように設定します。
classResolver=some.pkg.MyCustomClassResolver
If the resolver needs some further arguments, create a public constructor with one List<String>
argument, and supply the concrete arguments as
リゾルバーに引数が必要なら、List<String>
型の引数を1つ持つ public コンストラクタを作成すれば、次のように引数を設定できます。
classResolver.args=myArgOne,myArgTwo
For further details, compare the sources of SelectedClassResolverFromClasspath
.
より詳しい内容が知りたいときは SelectedClassResolverFromClasspath
のソースコードを参照してください。
10.3. クラスの MD5 チェックサム(MD5 Sums of Classes)
Sometimes it can be valuable to record the MD5 sums of classes being imported to track unexpected behavior. Since this has a performance impact, it is disabled by default, but it can be activated the following way:
インポートするクラスの MD5 チェックサムを記録しておくと、予期せぬ振る舞いの調査に役立つ場合があります。 この機能は性能に影響するため初期設定では無効になっていますが、次のように有効化できます。
enableMd5InClassSources=true
If this feature is enabled, the MD5 sum can be queried as
この機能を有効化していると、次のように MD5 チェックサムを取得できるようになります。
javaClass.getSource().get().getMd5sum()
10.4. エラーメッセージのカスタマイズ(Custom Error Messages)
You can configure a custom format to display the failures of a rule.
ルールの評価に失敗したときに表示するメッセージの書式を設定できます。
First Supply a custom implementation of
まず、FailureDisplayFormat
を実装したクラスを用意します。
com.tngtech.archunit.lang.FailureDisplayFormat
Then configure it
そして次のように設定します。
failureDisplayFormat=some.pkg.MyCustomFailureDisplayFormat
One example would be to shorten the fully qualified class names in failure messages:
例えば、失敗メッセージの中で完全修飾クラス名の短縮形にしたい場合は次のようにします。
private static class SimpleClassNameFailureFormat implements FailureDisplayFormat {
@Override
public String formatFailure(HasDescription rule, FailureMessages failureMessages, Priority priority) {
String failureDetails = failureMessages.stream()
.map(message -> message.replaceAll("<(?:\\w+\\.)+([A-Z][^>]*)>", "<$1>"))
.collect(joining(lineSeparator()));
return String.format("Architecture Violation [Priority: %s] - Rule '%s' was violated (%s):%n%s",
priority.asString(), rule.getDescription(), failureMessages.getInformationAboutNumberOfViolations(), failureDetails);
}
}
Note that due to the free format how violation texts can be composed, in particular by custom predicates and conditions, there is at the moment no more sophisticated way than plain text parsing. Users can tailor this to their specific environments where they know which sorts of failure formats can appear in practice.
特に独自の述語式と条件式を使っている場合はそうなのですが、違反の説明文は自由に構成できるため、今のところ平文テキストを解析する以外に効率的な方法はありません。 ユーザーは、自身の環境で実際にどのような書式の失敗メッセージが得られるのか試してみるしかありません。