Skip to content

Commit 3aa755d

Browse files
committed
Add example illustrating how more advanced method-to-method dependencies can be checked
Based on an example provided in #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 3e01795 commit 3aa755d

File tree

5 files changed

+172
-0
lines changed

5 files changed

+172
-0
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+
}
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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
import org.junit.experimental.categories.Category;
11+
12+
import com.tngtech.archunit.core.domain.JavaClasses;
13+
import com.tngtech.archunit.core.domain.JavaMethod;
14+
import com.tngtech.archunit.core.domain.JavaModifier;
15+
import com.tngtech.archunit.core.importer.ClassFileImporter;
16+
import com.tngtech.archunit.example.singleton.SingletonClass;
17+
import com.tngtech.archunit.lang.ArchCondition;
18+
import com.tngtech.archunit.lang.ConditionEvents;
19+
import com.tngtech.archunit.lang.SimpleConditionEvent;
20+
21+
/**
22+
* @author Per Lundberg
23+
*/
24+
@Category(Example.class)
25+
public class SingletonTest {
26+
private static final JavaClasses classes = new ClassFileImporter().importPackagesOf( SingletonClass.class);
27+
28+
@Test
29+
public void getInstance_is_not_used_from_inside_methods() {
30+
methods()
31+
.that().haveName( "getInstance" )
32+
.and().areStatic()
33+
34+
// Note: this is a convoluted way to say "no parameters".
35+
.and().haveRawParameterTypes( new String[0] )
36+
37+
.should( onlyBeCalledFromWhitelistedOrigins(
38+
// The class below will trigger a violation unless present as a parameter here.
39+
"com.tngtech.archunit.example.singleton.SingletonClassWhitelistedInvalidConsumer"
40+
) )
41+
.because( "" +
42+
"getInstance() calls should not be spread out through the methods of a class. This makes it hard/impossible " +
43+
"to override the dependencies for tests, and also means the dependencies are much harder to identify when " +
44+
"quickly looking at the code. Instead, move all getInstance() calls to the INSTANCE supplier and pass the " +
45+
"dependency to the constructor that way. If this is impossible for a particular case, add the class name to " +
46+
"the whitelist in " + getClass().getName() )
47+
.check( classes );
48+
}
49+
50+
private ArchCondition<JavaMethod> onlyBeCalledFromWhitelistedOrigins( String... whitelistedOrigins ) {
51+
return new ArchCondition<JavaMethod>( "only be called by whitelisted methods" ) {
52+
@Override
53+
public void check( JavaMethod method, ConditionEvents events ) {
54+
method.getCallsOfSelf().stream()
55+
// TODO: Add your own exceptions as needed here, if you have particular
56+
// TODO: use cases where getInstance() calls are permissible.
57+
// Static getInstance() methods are always allowed to call getInstance. This
58+
// does not break dependency injection and does not come with significant
59+
// design flaws.
60+
.filter( call -> !( Objects.equals( call.getOrigin().getName(), "getInstance" ) && call.getOrigin().getModifiers().contains( JavaModifier.STATIC ) ) )
61+
62+
// Anything not handled by the exceptions above must be explicitly listed in
63+
// the whitelistedOrigins parameter.
64+
.filter( call -> {
65+
Optional<String> result = Arrays.stream( whitelistedOrigins )
66+
.filter( o -> call.getOrigin().getFullName().startsWith( o ) )
67+
.findFirst();
68+
69+
return !result.isPresent();
70+
} )
71+
.forEach( call -> events.add( SimpleConditionEvent.violated( method, call.getDescription() ) ) );
72+
}
73+
};
74+
}
75+
}

0 commit comments

Comments
 (0)