-
Notifications
You must be signed in to change notification settings - Fork 306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add example illustrating how more advanced method-to-method dependencies can be checked #1058
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.tngtech.archunit.example.singleton; | ||
|
||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
/** | ||
* @author Per Lundberg | ||
*/ | ||
public class SingletonClass { | ||
|
||
// Poor man's reimplementation of Guava's memoization support. If your project supports Guava, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would drop this comment. The example is about using ArchUnit, not how you should implement a singleton, given library x, right? 🤔 From my perspective you could even drop all code, leave it empty and just write comments |
||
// replace the stuff below with the following: | ||
// | ||
// private static final Supplier<SingletonClass> INSTANCE = Suppliers.memoize( SingletonClass::new ); | ||
// | ||
// | ||
// ...and retrieve the (singleton) instance by calling `INSTANCE.get()` | ||
// | ||
private static final ConcurrentHashMap<Integer, SingletonClass> INSTANCE_MAP = new ConcurrentHashMap<>(); | ||
private static final int INSTANCE_KEY = 1; | ||
|
||
|
||
private SingletonClass() { | ||
// Private constructor to prevent construction | ||
} | ||
|
||
public void doSomething() { | ||
throw new UnsupportedOperationException(); | ||
} | ||
|
||
public static SingletonClass getInstance() { | ||
return INSTANCE_MAP.computeIfAbsent( INSTANCE_KEY, unused -> new SingletonClass() ); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.tngtech.archunit.example.singleton; | ||
|
||
/** | ||
* An example of how {@link SingletonClass} is used incorrectly. This is expected to be detected by | ||
* the corresponding ArchUnit test. | ||
* | ||
* @author Per Lundberg | ||
*/ | ||
public class SingletonClassInvalidConsumer { | ||
|
||
void doSomething() { | ||
// This pattern (of calling getInstance() in the midst of a method) is both unadvisable and | ||
// dangerous for the following reasons: | ||
// | ||
// - It makes it impossible to override the dependency for tests, which can lead to | ||
// unnecessarily excessive object mocking. | ||
// | ||
// - It postpones object initialization to an unnecessarily late stage (runtime instead of | ||
// startup-time). Problems with classes that cannot be instantiated risks being "hidden" | ||
// until runtime, defeating the purpose of the "fail fast" philosophy. | ||
SingletonClass.getInstance().doSomething(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.tngtech.archunit.example.singleton; | ||
|
||
/** | ||
* An example of how {@link SingletonClass} is used correctly | ||
* | ||
* @author Per Lundberg | ||
*/ | ||
public class SingletonClassValidConsumer { | ||
|
||
private static SingletonClassValidConsumer instance; | ||
|
||
private final SingletonClass singletonClass; | ||
|
||
public SingletonClassValidConsumer( SingletonClass singletonClass ) { | ||
this.singletonClass = singletonClass; | ||
} | ||
|
||
public static SingletonClassValidConsumer getInstance() { | ||
// Valid way to call getInstance() on another class: | ||
// | ||
// - We retrieve the instance for the dependency in our own getInstance() method. It is | ||
// passed to our constructor as a constructor parameter, making it easy to override it for | ||
// tests. | ||
if ( instance == null ) { | ||
instance = new SingletonClassValidConsumer( SingletonClass.getInstance() ); | ||
} | ||
|
||
return instance; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.tngtech.archunit.example.singleton; | ||
|
||
/** | ||
* An example of how {@link SingletonClass} is used incorrectly, but explicitly whitelisted. | ||
* | ||
* @author Per Lundberg | ||
*/ | ||
public class SingletonClassWhitelistedInvalidConsumer { | ||
|
||
void doSomething() { | ||
// This pattern (of calling getInstance() in the midst of a method) is both unadvisable and | ||
// dangerous for reasons described in SingleTonClassInvalidConsumer. | ||
SingletonClass.getInstance().doSomething(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,75 @@ | ||||||||||||||||||
package com.tngtech.archunit.exampletest; | ||||||||||||||||||
|
||||||||||||||||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; | ||||||||||||||||||
|
||||||||||||||||||
import java.util.Arrays; | ||||||||||||||||||
import java.util.Objects; | ||||||||||||||||||
import java.util.Optional; | ||||||||||||||||||
|
||||||||||||||||||
import org.junit.Test; | ||||||||||||||||||
import org.junit.experimental.categories.Category; | ||||||||||||||||||
|
||||||||||||||||||
import com.tngtech.archunit.core.domain.JavaClasses; | ||||||||||||||||||
import com.tngtech.archunit.core.domain.JavaMethod; | ||||||||||||||||||
import com.tngtech.archunit.core.domain.JavaModifier; | ||||||||||||||||||
import com.tngtech.archunit.core.importer.ClassFileImporter; | ||||||||||||||||||
import com.tngtech.archunit.example.singleton.SingletonClass; | ||||||||||||||||||
import com.tngtech.archunit.lang.ArchCondition; | ||||||||||||||||||
import com.tngtech.archunit.lang.ConditionEvents; | ||||||||||||||||||
import com.tngtech.archunit.lang.SimpleConditionEvent; | ||||||||||||||||||
|
||||||||||||||||||
/** | ||||||||||||||||||
* @author Per Lundberg | ||||||||||||||||||
*/ | ||||||||||||||||||
@Category(Example.class) | ||||||||||||||||||
public class SingletonTest { | ||||||||||||||||||
private static final JavaClasses classes = new ClassFileImporter().importPackagesOf( SingletonClass.class); | ||||||||||||||||||
|
||||||||||||||||||
@Test | ||||||||||||||||||
public void getInstance_is_not_used_from_inside_methods() { | ||||||||||||||||||
methods() | ||||||||||||||||||
.that().haveName( "getInstance" ) | ||||||||||||||||||
.and().areStatic() | ||||||||||||||||||
|
||||||||||||||||||
// Note: this is a convoluted way to say "no parameters". | ||||||||||||||||||
.and().haveRawParameterTypes( new String[0] ) | ||||||||||||||||||
|
||||||||||||||||||
.should( onlyBeCalledFromWhitelistedOrigins( | ||||||||||||||||||
// The class below will trigger a violation unless present as a parameter here. | ||||||||||||||||||
"com.tngtech.archunit.example.singleton.SingletonClassWhitelistedInvalidConsumer" | ||||||||||||||||||
) ) | ||||||||||||||||||
.because( "" + | ||||||||||||||||||
"getInstance() calls should not be spread out through the methods of a class. This makes it hard/impossible " + | ||||||||||||||||||
"to override the dependencies for tests, and also means the dependencies are much harder to identify when " + | ||||||||||||||||||
"quickly looking at the code. Instead, move all getInstance() calls to the INSTANCE supplier and pass the " + | ||||||||||||||||||
"dependency to the constructor that way. If this is impossible for a particular case, add the class name to " + | ||||||||||||||||||
"the whitelist in " + getClass().getName() ) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
.check( classes ); | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
private ArchCondition<JavaMethod> onlyBeCalledFromWhitelistedOrigins( String... whitelistedOrigins ) { | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically this is |
||||||||||||||||||
return new ArchCondition<JavaMethod>( "only be called by whitelisted methods" ) { | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
@Override | ||||||||||||||||||
public void check( JavaMethod method, ConditionEvents events ) { | ||||||||||||||||||
method.getCallsOfSelf().stream() | ||||||||||||||||||
// TODO: Add your own exceptions as needed here, if you have particular | ||||||||||||||||||
// TODO: use cases where getInstance() calls are permissible. | ||||||||||||||||||
// Static getInstance() methods are always allowed to call getInstance. This | ||||||||||||||||||
// does not break dependency injection and does not come with significant | ||||||||||||||||||
// design flaws. | ||||||||||||||||||
Comment on lines
+55
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would drop this comment. It's just an example, it's clear that somebody needs to adjust this to their need. If you want to explain theory behind the test case itself I'd rather do it as Javadoc on the test class or test method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Otherwise, the comments just make it harder to quickly get through the code 🤷♂️ |
||||||||||||||||||
.filter( call -> !( Objects.equals( call.getOrigin().getName(), "getInstance" ) && call.getOrigin().getModifiers().contains( JavaModifier.STATIC ) ) ) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe extract this as |
||||||||||||||||||
|
||||||||||||||||||
// Anything not handled by the exceptions above must be explicitly listed in | ||||||||||||||||||
// the whitelistedOrigins parameter. | ||||||||||||||||||
.filter( call -> { | ||||||||||||||||||
Optional<String> result = Arrays.stream( whitelistedOrigins ) | ||||||||||||||||||
.filter( o -> call.getOrigin().getFullName().startsWith( o ) ) | ||||||||||||||||||
.findFirst(); | ||||||||||||||||||
|
||||||||||||||||||
return !result.isPresent(); | ||||||||||||||||||
} ) | ||||||||||||||||||
Comment on lines
+64
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
.forEach( call -> events.add( SimpleConditionEvent.violated( method, call.getDescription() ) ) ); | ||||||||||||||||||
} | ||||||||||||||||||
}; | ||||||||||||||||||
} | ||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We generally don't us the
@author
tag, since you can always see that from the commit history anyway...