Skip to content

Commit f0557a2

Browse files
committed
Add example illustrating how more advanced method-to-method dependencies can be checked
Based on an example provided in TNG/ArchUnit#235, this example aims to help people who are working on similar use cases in their projects. Signed-off-by: Per Lundberg <[email protected]>
1 parent ff56fa6 commit f0557a2

File tree

6 files changed

+170
-1
lines changed

6 files changed

+170
-1
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.tngtech.archunit.example.singleton;
2+
3+
import java.util.function.Supplier;
4+
5+
import com.google.common.base.Suppliers;
6+
7+
/**
8+
* @author Per Lundberg
9+
*/
10+
public class SingletonClass {
11+
12+
private static final Supplier<SingletonClass> INSTANCE = Suppliers.memoize( SingletonClass::new );
13+
14+
private SingletonClass() {
15+
// Private constructor to prevent construction
16+
}
17+
18+
public void doSomething() {
19+
throw new UnsupportedOperationException();
20+
}
21+
22+
public static SingletonClass getInstance() {
23+
return INSTANCE.get();
24+
}
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.tngtech.archunit.example.singleton;
2+
3+
/**
4+
* An example of how {@link SingletonClass} is used incorrectly. This is expected to be detected by
5+
* the corresponding ArchUnit test.
6+
*
7+
* @author Per Lundberg
8+
*/
9+
public class SingletonClassInvalidConsumer {
10+
11+
void doSomething() {
12+
// This pattern (of calling getInstance() in the midst of a method) is both unadvisable and
13+
// dangerous for the following reasons:
14+
//
15+
// - It makes it impossible to override the dependency for tests, which can lead to
16+
// unnecessarily excessive object mocking.
17+
//
18+
// - It postpones object initialization to an unnecessarily late stage (runtime instead of
19+
// startup-time). Problems with classes that cannot be instantiated risks being "hidden"
20+
// until runtime, defeating the purpose of the "fail fast" philosophy.
21+
SingletonClass.getInstance().doSomething();
22+
}
23+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.tngtech.archunit.example.singleton;
2+
3+
import java.util.function.Supplier;
4+
5+
import com.google.common.base.Suppliers;
6+
7+
/**
8+
* An example of how {@link SingletonClass} is used correctly
9+
*
10+
* @author Per Lundberg
11+
*/
12+
public class SingletonClassValidConsumer {
13+
14+
private static SingletonClassValidConsumer instance;
15+
16+
private final SingletonClass singletonClass;
17+
18+
public SingletonClassValidConsumer( SingletonClass singletonClass ) {
19+
this.singletonClass = singletonClass;
20+
}
21+
22+
public static SingletonClassValidConsumer getInstance() {
23+
// Valid way to call getInstance() on another class:
24+
//
25+
// - We retrieve the instance for the dependency in our own getInstance() method. It is
26+
// passed to our constructor as a constructor parameter, making it easy to override it for
27+
// tests.
28+
if ( instance == null ) {
29+
instance = new SingletonClassValidConsumer( SingletonClass.getInstance() );
30+
}
31+
32+
return instance;
33+
}
34+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.tngtech.archunit.example.singleton;
2+
3+
/**
4+
* An example of how {@link SingletonClass} is used incorrectly, but explicitly whitelisted.
5+
*
6+
* @author Per Lundberg
7+
*/
8+
public class SingletonClassWhitelistedInvalidConsumer {
9+
10+
void doSomething() {
11+
// This pattern (of calling getInstance() in the midst of a method) is both unadvisable and
12+
// dangerous for reasons described in SingleTonClassInvalidConsumer
13+
SingletonClass.getInstance().doSomething();
14+
}
15+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.tngtech.archunit.exampletest;
2+
3+
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;
4+
5+
import java.util.Arrays;
6+
import java.util.Objects;
7+
import java.util.Optional;
8+
9+
import org.junit.Test;
10+
11+
import com.tngtech.archunit.core.domain.JavaClasses;
12+
import com.tngtech.archunit.core.domain.JavaMethod;
13+
import com.tngtech.archunit.core.domain.JavaModifier;
14+
import com.tngtech.archunit.core.importer.ClassFileImporter;
15+
import com.tngtech.archunit.example.singleton.SingletonClass;
16+
import com.tngtech.archunit.lang.ArchCondition;
17+
import com.tngtech.archunit.lang.ConditionEvents;
18+
import com.tngtech.archunit.lang.SimpleConditionEvent;
19+
20+
/**
21+
* @author Per Lundberg
22+
*/
23+
public class SingletonTest {
24+
private static final JavaClasses classes = new ClassFileImporter().importPackagesOf( SingletonClass.class);
25+
26+
@Test
27+
public void getInstance_is_not_used_from_inside_methods() {
28+
methods()
29+
.that().haveName( "getInstance" )
30+
.and().areStatic()
31+
32+
// Note: this is a convoluted way to say "no parameters".
33+
.and().haveRawParameterTypes( new String[0] )
34+
35+
.should( onlyBeCalledFromWhitelistedOrigins(
36+
// The class below will trigger a violation unless present as a parameter here.
37+
"com.tngtech.archunit.example.singleton.SingletonClassWhitelistedInvalidConsumer"
38+
) )
39+
.because( "" +
40+
"getInstance() calls should not be spread out through the methods of a class. This makes it hard/impossible " +
41+
"to override the dependencies for tests, and also means the dependencies are much harder to identify when " +
42+
"quickly looking at the code. Instead, move all getInstance() calls to the INSTANCE supplier and pass the " +
43+
"dependency to the constructor that way. If this is impossible for a particular case, add the class name to " +
44+
"the whitelist in " + getClass().getName() )
45+
.check( classes );
46+
}
47+
48+
private ArchCondition<JavaMethod> onlyBeCalledFromWhitelistedOrigins( String... whitelistedOrigins ) {
49+
return new ArchCondition<JavaMethod>( "only be called by whitelisted methods" ) {
50+
@Override
51+
public void check( JavaMethod method, ConditionEvents events ) {
52+
method.getCallsOfSelf().stream()
53+
// TODO: Add your own exceptions as needed here, if you have particular
54+
// TODO: use cases where getInstance() calls are permissible.
55+
// Static getInstance() methods are always allowed to call getInstance. This
56+
// does not break dependency injection and does not come with significant
57+
// design flaws.
58+
.filter( call -> !( Objects.equals( call.getOrigin().getName(), "getInstance" ) && call.getOrigin().getModifiers().contains( JavaModifier.STATIC ) ) )
59+
60+
// Anything not handled by the exceptions above must be explicitly listed in
61+
// the whitelistedOrigins parameter.
62+
.filter( call -> {
63+
Optional<String> result = Arrays.stream( whitelistedOrigins )
64+
.filter( o -> call.getOrigin().getFullName().startsWith( o ) )
65+
.findFirst();
66+
67+
return result.isEmpty();
68+
} )
69+
.forEach( call -> events.add( SimpleConditionEvent.violated( method, call.getDescription() ) ) );
70+
}
71+
};
72+
}
73+
}

example-plain/src/test/resources/frozen/a81a2b54-5a18-4145-b544-7a580aba0425

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,5 @@ Method <com.tngtech.archunit.example.layers.service.ComplexServiceAnnotation.ser
2222
Method <com.tngtech.archunit.example.layers.service.ComplexServiceAnnotation.simpleServiceAnnotation()> has return type <com.tngtech.archunit.example.layers.service.SimpleServiceAnnotation> in (ComplexServiceAnnotation.java:0)
2323
Method <com.tngtech.archunit.example.layers.service.ServiceType.values()> calls method <[Lcom.tngtech.archunit.example.layers.service.ServiceType;.clone()> in (ServiceType.java:3)
2424
Method <com.tngtech.archunit.example.layers.service.ServiceType.values()> has return type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0)
25-
Method <com.tngtech.archunit.example.layers.service.ServiceType.$values()> has return type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0)
2625
Method <com.tngtech.archunit.example.layers.service.ServiceViolatingDaoRules.illegallyUseEntityManager()> calls method <com.tngtech.archunit.example.layers.service.ServiceViolatingDaoRules$MyEntityManager.persist(java.lang.Object)> in (ServiceViolatingDaoRules.java:27)
2726
Method <com.tngtech.archunit.example.layers.service.ServiceViolatingProxyRules.executeAsync()> is annotated with <com.tngtech.archunit.example.layers.service.Async> in (ServiceViolatingProxyRules.java:0)

0 commit comments

Comments
 (0)