Skip to content

Commit

Permalink
Makes Slices more customizable. Background: In legacy applications th…
Browse files Browse the repository at this point in the history
…e package structure might not be as neat as myapp.(*) where * catches all reasonable domain slices. Instead it might be necessary to really tweak which classes from which locations form a 'slice'. The new approach allows to freely define a mapping JavaClass -> SliceIdentifier to group arbitrary classes together into one slice (defined by the common SliceIdentifier).

Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
  • Loading branch information
codecholeric committed Mar 10, 2019
1 parent ee43a5e commit e2c9b22
Show file tree
Hide file tree
Showing 11 changed files with 501 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.ArchUnitRunner;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;

Expand Down Expand Up @@ -54,4 +56,30 @@ public class CyclicDependencyRulesTest {
.should().beFreeOfCycles()
.ignoreDependency(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class, ClassCallingConstructorInSliceFive.class)
.ignoreDependency(resideInAPackage("..slice4.."), DescribedPredicate.<JavaClass>alwaysTrue());

@ArchTest
public static final ArchRule no_cycles_in_freely_customized_slices =
slices().assignedFrom(inComplexSliceOneOrTwo())
.namingSlices("$1[$2]")
.should().beFreeOfCycles();

private static SliceAssignment inComplexSliceOneOrTwo() {
return new SliceAssignment() {
@Override
public String getDescription() {
return "complex slice one or two";
}

@Override
public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
if (javaClass.getPackageName().contains("complexcycles.slice1")) {
return SliceIdentifier.of("Complex-Cycle", "One");
}
if (javaClass.getPackageName().contains("complexcycles.slice2")) {
return SliceIdentifier.of("Complex-Cycle", "Two");
}
return SliceIdentifier.ignore();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.tngtech.archunit.exampletest.junit5;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.example.cycle.complexcycles.slice1.SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree;
import com.tngtech.archunit.example.cycle.complexcycles.slice3.ClassCallingConstructorInSliceFive;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTag;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;

import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;
Expand Down Expand Up @@ -50,4 +53,30 @@ public class CyclicDependencyRulesTest {
.should().beFreeOfCycles()
.ignoreDependency(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class, ClassCallingConstructorInSliceFive.class)
.ignoreDependency(resideInAPackage("..slice4.."), alwaysTrue());

@ArchTest
static final ArchRule no_cycles_in_freely_customized_slices =
slices().assignedFrom(inComplexSliceOneOrTwo())
.namingSlices("$1[$2]")
.should().beFreeOfCycles();

private static SliceAssignment inComplexSliceOneOrTwo() {
return new SliceAssignment() {
@Override
public String getDescription() {
return "complex slice one or two";
}

@Override
public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
if (javaClass.getPackageName().contains("complexcycles.slice1")) {
return SliceIdentifier.of("Complex-Cycle", "One");
}
if (javaClass.getPackageName().contains("complexcycles.slice2")) {
return SliceIdentifier.of("Complex-Cycle", "Two");
}
return SliceIdentifier.ignore();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.tngtech.archunit.exampletest;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.example.cycle.complexcycles.slice1.SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree;
import com.tngtech.archunit.example.cycle.complexcycles.slice3.ClassCallingConstructorInSliceFive;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;
import org.junit.Test;
import org.junit.experimental.categories.Category;

Expand Down Expand Up @@ -74,4 +77,32 @@ public void no_cycles_in_complex_scenario_with_custom_ignore() {
.ignoreDependency(resideInAPackage("..slice4.."), alwaysTrue())
.check(classes);
}

@Test
public void no_cycles_in_freely_customized_slices() {
slices().assignedFrom(inComplexSliceOneOrTwo())
.namingSlices("$1[$2]")
.should().beFreeOfCycles()
.check(classes);
}

private SliceAssignment inComplexSliceOneOrTwo() {
return new SliceAssignment() {
@Override
public String getDescription() {
return "complex slice one or two";
}

@Override
public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
if (javaClass.getPackageName().contains("complexcycles.slice1")) {
return SliceIdentifier.of("Complex-Cycle", "One");
}
if (javaClass.getPackageName().contains("complexcycles.slice2")) {
return SliceIdentifier.of("Complex-Cycle", "Two");
}
return SliceIdentifier.ignore();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -443,12 +443,19 @@ Stream<DynamicTest> CyclicDependencyRulesTest() {
.by(cycleFromComplexSlice1To2())
.by(cycleFromComplexSlice1To2To3To5())

.ofRule("slices assigned from complex slice one or two should be free of cycles")
.by(cycleFromComplexSlice1To2("Complex-Cycle[One]", "Complex-Cycle[Two]"))

.toDynamicTests();
}

private static CyclicErrorMatcher cycleFromComplexSlice1To2() {
return cycleFromComplexSlice1To2("slice1 of complexcycles", "slice2 of complexcycles");
}

private static CyclicErrorMatcher cycleFromComplexSlice1To2(String sliceOneDescription, String sliceTwoDescription) {
return cycle()
.from("slice1 of complexcycles")
.from(sliceOneDescription)
.by(callFromMethod(ClassOfMinimalCycleCallingSliceTwo.class, "callSliceTwo")
.toMethod(ClassOfMinimalCycleCallingSliceOne.class, "callSliceOne")
.inLine(9))
Expand All @@ -457,7 +464,7 @@ private static CyclicErrorMatcher cycleFromComplexSlice1To2() {
.by(callFromMethod(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class, "callSliceTwo")
.toConstructor(InstantiatedClassInSliceTwo.class)
.inLine(10))
.from("slice2 of complexcycles")
.from(sliceTwoDescription)
.by(inheritanceFrom(SliceTwoInheritingFromSliceOne.class)
.extending(SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree.class))
.by(field(ClassOfMinimalCycleCallingSliceOne.class, "classInSliceOne")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;

/**
* A collection of {@link JavaClass JavaClasses} modelling some domain aspect of a code basis. This is conceptually
* a cut through a code base according to business logic. Take for example
* <pre><code>
* com.mycompany.myapp.order
* com.mycompany.myapp.customer
* com.mycompany.myapp.user
* com.mycompany.myapp.authorization
* </code></pre>
* The top level packages under 'myapp' could be considered slices according to different domain aspects.<br>
* Thus there could be a slice 'Order' housing all the classes from the {@code order} package, a slice 'Customer'
* housing all the classes from the {@code customer} package and so on.
*/
public final class Slice extends ForwardingSet<JavaClass> implements HasDescription, CanOverrideDescription<Slice> {
private final List<String> matchingGroups;
private Description description;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2019 TNG Technology Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.library.dependencies;

import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.HasDescription;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;

import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE;

/**
* A mapping {@link JavaClass} -&gt; {@link SliceIdentifier} which defines how to partition
* a set of {@link JavaClasses} into {@link Slices}. All classes that are mapped to the same
* {@link SliceIdentifier} will belong to the same {@link Slice}.<br>
* A {@link SliceAssignment} must provide a description to be used for the rule text of a rule
* formed via {@link SlicesRuleDefinition}.
*/
@PublicAPI(usage = INHERITANCE)
public interface SliceAssignment extends HasDescription {
SliceIdentifier getIdentifierOf(JavaClass javaClass);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2019 TNG Technology Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.library.dependencies;

import java.util.Collections;
import java.util.List;
import java.util.Objects;

import com.google.common.collect.ImmutableList;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;

/**
* An unique identifier of a {@link Slice}. All {@link JavaClasses} that are assigned to the same
* {@link SliceIdentifier} are considered to belong to the same {@link Slice}.<br>
* A {@link SliceIdentifier} consists of textual parts. Two {@link SliceIdentifier} are considered to
* be equal if and only if their parts are equal. The parts can also be referred to from
* {@link Slices#namingSlices(String)} via '{@code $x}' where '{@code x}' is the number of the part.
*/
public final class SliceIdentifier {
private final List<String> parts;

private SliceIdentifier(List<String> parts) {
this.parts = ImmutableList.copyOf(parts);
}

List<String> getParts() {
return parts;
}

@Override
public int hashCode() {
return Objects.hash(parts);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final SliceIdentifier other = (SliceIdentifier) obj;
return Objects.equals(this.parts, other.parts);
}

@Override
public String toString() {
return getClass().getSimpleName() + parts;
}

@PublicAPI(usage = ACCESS)
public static SliceIdentifier of(String... parts) {
return of(ImmutableList.copyOf(parts));
}

@PublicAPI(usage = ACCESS)
public static SliceIdentifier of(List<String> parts) {
checkNotNull(parts, "Supplied parts may not be null");
checkArgument(!parts.isEmpty(),
"Parts of a %s must not be empty. Use %s.ignore() to ignore a %s",
SliceIdentifier.class.getSimpleName(), SliceIdentifier.class.getSimpleName(), JavaClass.class.getSimpleName());

return new SliceIdentifier(parts);
}

@PublicAPI(usage = ACCESS)
public static SliceIdentifier ignore() {
return new SliceIdentifier(Collections.<String>emptyList());
}
}
Loading

0 comments on commit e2c9b22

Please sign in to comment.