如何在 ArchUnit 中只允许某些循环依赖?
How to allow only certain cyclic dependencies in ArchUnit?
在 ArchUnit 中,我可以检查包 .should().beFreeOfCycles()
。如何为某些周期指定此规则的例外情况?
例如,给定这些包及其依赖项:
A <-> B <-> C
如何允许 A <-> B
,但仍然禁止 A
和 B
成为任何其他循环的一部分,例如B <-> C
?
Freezing Arch Rules 始终是允许某些违规行为但捕获其他违规行为的选项。
这对你来说可行吗?
基于Manfred's ,这里有一个似乎工作正常的解决方案(在 Kotlin 中实现):
fun test() {
SlicesRuleDefinition.slices().matching("(com.mypackage.*..)")
.should()
// Using an extension method which allows us to specify allowed
// cycles succinctly:
.beFreeOfCyclesExcept("com.mypackage.a" to "com.mypackage.b")
.check(someClasses)
}
fun SlicesShould.beFreeOfCyclesExcept(
vararg allowed: Pair<String, String>
): ArchRule =
FreezingArchRule
// In case you are not familiar with Kotlin:
// We are in an extension method. 'this' will be substituted with
// 'SlicesRuleDefinition.slices().matching("(com.mypackage.*..)")
// .should()';
.freeze(this.beFreeOfCycles())
.persistIn(
// Using a custom ViolationStore instead of the default
// TextFileBasedViolationStore so we can configure the
// allowed violations in code instead of a text file:
object : ViolationStore {
override fun initialize(properties: Properties) {}
override fun contains(rule: ArchRule): Boolean = true
override fun save(rule: ArchRule, violations: List<String>) {
// Doing nothing here because we do not want ArchUnit
// to add any additional allowed violations.
}
override fun getViolations(rule: ArchRule): List<String> =
allowed
// ArchUnit records cycles in the form
// A -> B -> A. I.e., A -> B -> A and
// B -> A -> B are different violations.
// We add the reverse cycle to make sure
// both directions are allowed:
.flatMap { pair ->
listOf(pair, Pair(pair.second, pair.first))
}
// .distinct() is not necessary, but using it is
// cleaner because by adding the reverse cycles
// we may possibly have added duplicates:
.distinct()
.map { (sliceA, sliceB) ->
// This is a prefix of the format that
// ArchUnit uses:
"Cycle detected: Slice $sliceA -> \n" +
" Slice $sliceB -> \n" +
" Slice $sliceA\n"
}
}
)
// The lines that ArchUnit uses are very specific, including
// info about which methods etc. create the cycle. That is
// exactly what is desirable when establishing a baseline for
// legacy code. But we want to permanently allow certain
// cycles, regardless of which current or future code creates
// the cycle. Thus, we only compare the prefixes of violation
// lines:
.associateViolationLinesVia {
lineFromFirstViolation,
lineFromSecondViolation ->
lineFromFirstViolation.startsWith(lineFromSecondViolation)
}
请注意,我只在一个小项目上对此进行了测试。
在 ArchUnit 中,我可以检查包 .should().beFreeOfCycles()
。如何为某些周期指定此规则的例外情况?
例如,给定这些包及其依赖项:
A <-> B <-> C
如何允许 A <-> B
,但仍然禁止 A
和 B
成为任何其他循环的一部分,例如B <-> C
?
Freezing Arch Rules 始终是允许某些违规行为但捕获其他违规行为的选项。
这对你来说可行吗?
基于Manfred's
fun test() {
SlicesRuleDefinition.slices().matching("(com.mypackage.*..)")
.should()
// Using an extension method which allows us to specify allowed
// cycles succinctly:
.beFreeOfCyclesExcept("com.mypackage.a" to "com.mypackage.b")
.check(someClasses)
}
fun SlicesShould.beFreeOfCyclesExcept(
vararg allowed: Pair<String, String>
): ArchRule =
FreezingArchRule
// In case you are not familiar with Kotlin:
// We are in an extension method. 'this' will be substituted with
// 'SlicesRuleDefinition.slices().matching("(com.mypackage.*..)")
// .should()';
.freeze(this.beFreeOfCycles())
.persistIn(
// Using a custom ViolationStore instead of the default
// TextFileBasedViolationStore so we can configure the
// allowed violations in code instead of a text file:
object : ViolationStore {
override fun initialize(properties: Properties) {}
override fun contains(rule: ArchRule): Boolean = true
override fun save(rule: ArchRule, violations: List<String>) {
// Doing nothing here because we do not want ArchUnit
// to add any additional allowed violations.
}
override fun getViolations(rule: ArchRule): List<String> =
allowed
// ArchUnit records cycles in the form
// A -> B -> A. I.e., A -> B -> A and
// B -> A -> B are different violations.
// We add the reverse cycle to make sure
// both directions are allowed:
.flatMap { pair ->
listOf(pair, Pair(pair.second, pair.first))
}
// .distinct() is not necessary, but using it is
// cleaner because by adding the reverse cycles
// we may possibly have added duplicates:
.distinct()
.map { (sliceA, sliceB) ->
// This is a prefix of the format that
// ArchUnit uses:
"Cycle detected: Slice $sliceA -> \n" +
" Slice $sliceB -> \n" +
" Slice $sliceA\n"
}
}
)
// The lines that ArchUnit uses are very specific, including
// info about which methods etc. create the cycle. That is
// exactly what is desirable when establishing a baseline for
// legacy code. But we want to permanently allow certain
// cycles, regardless of which current or future code creates
// the cycle. Thus, we only compare the prefixes of violation
// lines:
.associateViolationLinesVia {
lineFromFirstViolation,
lineFromSecondViolation ->
lineFromFirstViolation.startsWith(lineFromSecondViolation)
}
请注意,我只在一个小项目上对此进行了测试。