Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.tngtech.archunit.example.singleton;

import java.util.function.Supplier;

import com.google.common.base.Suppliers;

/**
* @author Per Lundberg
*/
public class SingletonClass {

private static final Supplier<SingletonClass> INSTANCE = Suppliers.memoize( SingletonClass::new );

private SingletonClass() {
// Private constructor to prevent construction
}

public void doSomething() {
throw new UnsupportedOperationException();
}

public static SingletonClass getInstance() {
return INSTANCE.get();
}
}
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,34 @@
package com.tngtech.archunit.example.singleton;

import java.util.function.Supplier;

import com.google.common.base.Suppliers;

/**
* 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,73 @@
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 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
*/
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() )
.check( classes );
}

private ArchCondition<JavaMethod> onlyBeCalledFromWhitelistedOrigins( String... whitelistedOrigins ) {
return new ArchCondition<JavaMethod>( "only be called by whitelisted methods" ) {
@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.
.filter( call -> !( Objects.equals( call.getOrigin().getName(), "getInstance" ) && call.getOrigin().getModifiers().contains( JavaModifier.STATIC ) ) )

// 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.isEmpty();
} )
.forEach( call -> events.add( SimpleConditionEvent.violated( method, call.getDescription() ) ) );
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@ Method <com.tngtech.archunit.example.layers.service.ComplexServiceAnnotation.ser
Method <com.tngtech.archunit.example.layers.service.ComplexServiceAnnotation.simpleServiceAnnotation()> has return type <com.tngtech.archunit.example.layers.service.SimpleServiceAnnotation> in (ComplexServiceAnnotation.java:0)
Method <com.tngtech.archunit.example.layers.service.ServiceType.values()> calls method <[Lcom.tngtech.archunit.example.layers.service.ServiceType;.clone()> in (ServiceType.java:3)
Method <com.tngtech.archunit.example.layers.service.ServiceType.values()> has return type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0)
Method <com.tngtech.archunit.example.layers.service.ServiceType.$values()> has return type <[Lcom.tngtech.archunit.example.layers.service.ServiceType;> in (ServiceType.java:0)
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)
Method <com.tngtech.archunit.example.layers.service.ServiceViolatingProxyRules.executeAsync()> is annotated with <com.tngtech.archunit.example.layers.service.Async> in (ServiceViolatingProxyRules.java:0)