TNG / ArchUnit

A Java architecture test library, to specify and assert architecture rules in plain Java
http://archunit.org
Apache License 2.0
3.18k stars 287 forks source link

Onion Architecture main class #318

Closed cdietze closed 4 years ago

cdietze commented 4 years ago

Hello, I tried out the onionArchitecture() style on a test project. My test code (Kotlin):

    fun onionTest(importedClasses: JavaClasses) {
        val root = "hexarch_test"
        val rule = onionArchitecture()
            .domainModels("$root.domain.model..")
            .domainServices("$root.domain.service..")
            .applicationServices("$root.application..")
            .adapter("persistence", "$root.adapter.persistence..")
        rule.check(importedClasses)
    }

I put a main class which bootstraps the application the base package. Now archunit complains that this main class accesses adapter code (which is the case).

I tried to add an exception for that class using: importedClasses.that(doNot(equivalentTo(Main::class.java))) but it would still complain that the adapter packages are accessed.

Maybe this issue does not occur if I were using DI where the adapter code would not be accessed directly (but actually still are indirectly / at runtime).

What's a good way to solve this? Using a DI-framework should not be mandatory IMO.

hankem commented 4 years ago

Did I understand correctly that you're basically doing the following?

val filteredClasses = importedClasses.that(doNot(equivalentTo(Main::class.java)))
rule.check(filteredClasses)

Restricting importedClasses after the import does not help in your case because ArchUnit builds the dependencies already during the class file import, as you can see with (I'm switching to Java syntax 😉):

System.out.println(filteredClasses.get(YourAdapter.class).getDirectDependenciesToSelf());

As a temporarily workaround, you might be able to use a tuned ClassFileImporter to excluding Main already during import:

new ClassFileImporter().withImportOption(new ImportOption() {
        @Override
        public boolean includes(Location location) {
            return !location.contains(Main.class.getName().replaceAll("\\.", "/"));
        }
    })

For a proper solution, we should IMO extend OnionArchitecture to support an ignoreDependency​ mechanism just as LayeredArchitecture does

cdietze commented 4 years ago

Thanks, @hankem. Your workaround works! Your suggestion to specify dependencies to ignore on OnionArchitecture sounds good to me.

codecholeric commented 4 years ago

Another possibility might be to use https://www.archunit.org/userguide/html/000_Index.html#_ignoring_violations and simply ignore all violations that contain com.myapp.Main. But yes, I knew that the need for ignoreDependency would sooner or later come up :wink:

michaelbannister commented 4 years ago

Please forgive if this is a dumb question, but what is the syntax I need to use .ignoreDependency for the same case as described by @cdietze? My 'main' class constructs my application – no magic DI frameworks here – and I want to say that it is allowed to depend on anything.

I've tried several variants on

.ignoreDependency("com.mycompany.myapp.ApplicationKt", ".*")

but can't seem to make it work. Help much appreciated!

I get failures such as (somewhat redacted)

Method <com.mycompany.myapp.ApplicationKt.myApp(com.mycompany.myapp.config.AppConfig, com.mycompany.myapp.adapter.repository.SecretsManager)> calls constructor <com.mycompany.myapp.adapter.thirdparty.ThirdPartyConnector.<init>(com.thirdparty.ThirdPartyGateway)> in (Application.kt:53)
cdietze commented 4 years ago

@michaelbannister In my project I use:

.ignoreDependency(resideInAPackage("com.example.testproject.test.."), DescribedPredicate.alwaysTrue())
michaelbannister commented 4 years ago

Thank you very much @cdietze, that was enough to get me what I needed!

codecholeric commented 4 years ago

The string versions of ignoreDependency(..) actually only take fully qualified class names of origin and target and no wildcards (so they are very specific). With the predicate version you should be able to cover everything, e.g.

.ignoreDependency(name("com.mycompany.myapp.ApplicationKt"), alwaysTrue())

If you want to very specifically only ignore dependencies from ApplicationKt to any other class (the resideInAPackage version of course works, too, as long as you're okay to ignore all dependencies from the whole package).

Manfred73 commented 4 months ago

@michaelbannister In my project I use:

.ignoreDependency(resideInAPackage("com.example.testproject.test.."), DescribedPredicate.alwaysTrue())

Took me a while to figure out how to exclude a test package until I found this post. This works perfectly!