Architectural fitness functions

Inspired by Neal Ford’s presentation at our Change is the Only constant event I started experimenting with architectural fitness functions. An architectural fitness function provides an objective integrity assessment of some architectural characteristic(s). If you want to take a deeper dive into evolutionary architectures including fitness functions take look at Neals book: Building Evolutionary Architectures: Support Constant Change. Neal’s slides contained an example of verifying package dependencies from a Unit Test using JDepend.

Verifying code modularity

In this blog post we’ll elaborate on that approach and create a Unit Test that verifies that our code complies to the chosen packaging strategies using an alternative to JDepend named code-assert. We’ll verify two types of packaging strategies; package by layer and package by feature. For a definition of these strategies please have a look at this blog from Simon Brown.

20150308 package by layer 300x225 20150308 package by feature 300x225

Creating the fitness functions

To illustrate both types and their fitness functions I created a small demo project that contains a package structure for each of this strategies containing dummy classes/components. Needless to say that this is for demonstrating purposes only and normally you would choose only one packaging strategy for a project and stick to that, preferable package by feature ;)

Code packaged by layer

Screen Shot 2017 10 06 at 13.42.52 300x248

Code packaged by feature

Screen Shot 2017 10 06 at 13.42.34 300x248

Adding code-assert to our Gradle project

Lets start by adding the dependency to code-assert to our Gradle build file dependencies.

dependencies {
    compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.6.1'
    testCompile group: 'junit', name: 'junit', version: '4.12'
    testCompile group: 'guru.nidi', name: 'code-assert', version: '0.8.2'
}

Configuring code-assert for Gradle

Update: as of version 0.8.4 code-asserts now supports Gradle with AnalyzerConfig.gradle() directly. The first step in each Unit Test is to configure code-assert and specify where the sources and classes resist on our filesystem. For Maven code-assert includes an easy way to create this configuration but it lacks this feature for Gradle (for now). I’ve created a GradleAnalyzerConfig class which you can use to create the correct configuration. A pull request to the code-assert project is on it’s way.

Verifying package by layer

Now we can create a Unit Test that will verify if our code is packaged by layer.

public class VerifyPackageByLayerTest {

    @Test
    public void verifyPackageByLayer() {

        /// Create an analyzer config for the package we'd like to verify
        AnalyzerConfig analyzerConfig = GradleAnalyzerConfig.gradle().main("com.jdriven.fitness.packaging.by.layer");

        // Dependency rules for Packaging by Layer
        // NOTE: the classname should match the packagename
        class ComJdrivenFitnessPackagingByLayer extends DependencyRuler {

            // Rules for layer child packages
            // NOTE: they should match the name of the sub packages
            DependencyRule controller, service, repository;

            @Override
            public void defineRules() {
                // Our App classes depends on all subpackages because it constructs all of them
                base().mayUse(base().allSub());
                // Controllers may use Services
                controller.mayUse(service);
                // Services may use Repositories
                service.mayUse(repository);
            }
        }

        // All dependencies are forbidden, except the ones defined in ComJdrivenFitnessPackagingByLayer
        // java, org, net packages may be used freely
        DependencyRules rules = DependencyRules.denyAll()
                .withRelativeRules(new ComJdrivenFitnessPackagingByLayer())
                .withExternals("java.*", "org.*", "net.*");

        DependencyResult result = new DependencyAnalyzer(analyzerConfig).rules(rules).analyze();
        assertThat(result, matchesRulesExactly());
    }
}

If we execute this Unit Test it will pass because our demo code complies with the specified rules. Now we modify the code by accessing a Repository directly from our Controller and change App.java accordingly.

public class ControllerA {

    private final ServiceA serviceA;
    private final RepositoryA repositoryA;

    public ControllerA(ServiceA serviceA, RepositoryA repositoryA) {
        this.serviceA = serviceA;
        this.repositoryA = repositoryA;
    }
}

Execute the Unit Test again and it will fail, because we violated our rules by accessing the Repository directly from our Controller:

java.lang.AssertionError:
 Expected: Comply with rules
 but: DENIED com.jdriven.fitness.packaging.by.layer.controller ->
 com.jdriven.fitness.packaging.by.layer.repository (by com.jdriven.fitness.packaging.by.layer.controller.ControllerA)

Verifying Package By Feature

The following Unit Test will verify our package by feature strategy:

public class VerifyPackageByFeatureTest {

    @Test
    public void verifyPackageByFeature() {

        /// Create an analyzer config for the package we'd like to verify
        AnalyzerConfig analyzerConfig = GradleAnalyzerConfig.gradle().main("com.jdriven.fitness.packaging.by.feature");

        // Dependency Rules for Packaging By Feature
        // NOTE: the classname should match the packagename
        class ComJdrivenFitnessPackagingByFeature extends DependencyRuler {

            // Rules for feature child packages
            // NOTE: they should match the name of the sub packages
            DependencyRule a, b;

            @Override
            public void defineRules() {
                // Our App classes depends on all subpackages because it constructs all of them
                base().mayUse(base().allSub());
            }
        }

        // All dependencies are forbidden, except the ones defined in ComJdrivenFitnessPackagingByFeature
        // java, org, net packages may be used freely
        DependencyRules rules = DependencyRules.denyAll()
                .withRelativeRules(new ComJdrivenFitnessPackagingByFeature())
                .withExternals("java.*", "org.*", "net.*");

        DependencyResult result = new DependencyAnalyzer(analyzerConfig).rules(rules).analyze();
        assertThat(result, matchesRulesExactly());
    }
}

The Unit Test is almost identical to the previous one, the rules are even less complicated. We allow our App class to access all, that’s it. There are no dependencies allowed between packages a and b. But what if we need to use ServiceB from ControllerA?

public class ControllerA {

    private final ServiceA serviceA;
    private final ServiceB serviceB;

    public ControllerA(ServiceA serviceA, ServiceB serviceB) {
        this.serviceA = serviceA;
        this.serviceB = serviceB;
    }
}

Initially our test will fail:

java.lang.AssertionError:
 Expected: Comply with rules
 but: DENIED com.jdriven.fitness.packaging.by.feature.a ->
 com.jdriven.fitness.packaging.by.feature.b (by com.jdriven.fitness.packaging.by.feature.a.ControllerA)

But if the dependency is really needed and we really thought this through we make this explicit by changing the Unit Test and adding the following rule:

// Allow package / module a to acces b
a.mayUse(b);

Bonus: a cyclic dependency test

Adding the inter package dependencies might lead to a cyclic dependency. Using code-assert it’s easy to add a Unit Test that detects this:

public class CyclicDependencyTest {

    @Test
    public void verifyThatThereAreNoCyclicDependencies() {
        /// Create an analyzer for the whole project
        AnalyzerConfig analyzerConfig = GradleAnalyzerConfig.gradle().main();

        // Check that we have no CyclicDependencies
        assertThat(new DependencyAnalyzer(analyzerConfig).analyze(), hasNoCycles());
    }
}

Demo source code

The source code of the demo project is on GitHub: https://github.com/robbrinkman/architectural-fitness-functions

shadow-left