简体   繁体   中英

Enforcing layered architecture in Java

Given a software system written in Java consisting of three layers, A -> B -> C, ie layer A uses layer B and B uses layer C.

I want to make sure that a class of one layer only has access only to classes of the same layer or its direct dependency, ie B should be able to access C but not A. Also A should be able to access B but not C.

Is there an easy way to enforce such a restriction? Ideally I want eclipse to complain at once if one tries to access a class of the wrong layer.

The software currently uses maven. Therefore I tried to put A, B, and C into different maven modules and to declare dependencies properly. This works fine to prevent B to access A, but does not prevent A to access C.

Next I tried to exclude C from the dependency to B. This now also prevents access from A to C. However now I am no longer able to use copy-dependencies to collect all transitive dependencies needed for run time.

Is there a good way that allows me a clean separation of layers, but also allows me to collect all needed runtime dependencies?

in maven you can use the maven-macker-plugin as following example:

<build>
    <plugins>
        <plugin>
            <groupId>de.andrena.tools.macker</groupId>
            <artifactId>macker-maven-plugin</artifactId>
            <version>1.0.2</version>
            <executions>
                <execution>
                    <phase>compile</phase>
                    <goals>
                        <goal>macker</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

and here is an example macker-rules.xml example file: (put it on the same level as your pom.xml)

<?xml version="1.0"?>
<macker>

    <ruleset name="Layering rules">
        <var name="base" value="org.example" />

        <pattern name="appl" class="${base}.**" />
        <pattern name="common" class="${base}.common.**" />
        <pattern name="persistence" class="${base}.persistence.**" />
        <pattern name="business" class="${base}.business.**" />
        <pattern name="web" class="${base}.web.**" />

        <!-- =============================================================== -->
        <!-- Common -->
        <!-- =============================================================== -->
        <access-rule>
            <message>zugriff auf common; von überall gestattet</message>
            <deny>
                <to pattern="common" />
                <allow>
                    <from>
                        <include pattern="appl" />
                    </from>
                </allow>
            </deny>
        </access-rule>

        <!-- =============================================================== -->
        <!-- Persistence -->
        <!-- =============================================================== -->
        <access-rule>
            <message>zugriff auf persistence; von web und business gestattet</message>
            <deny>
                <to pattern="persistence" />
                <allow>
                    <from>
                        <include pattern="persistence" />
                        <include pattern="web" />
                        <include pattern="business" />
                    </from>
                </allow>
            </deny>
        </access-rule>

        <!-- =============================================================== -->
        <!-- Business -->
        <!-- =============================================================== -->
        <access-rule>
            <message>zugriff auf business; nur von web gestattet</message>
            <deny>
                <to pattern="business" />
                <allow>
                    <from>
                        <include pattern="business" />
                        <include pattern="web" />
                    </from>
                </allow>
            </deny>
        </access-rule>

        <!-- =============================================================== -->
        <!-- Web -->
        <!-- =============================================================== -->
        <access-rule>
            <message>zugriff auf web; von nirgends gestattet</message>
            <deny>
                <to pattern="web" />
                <allow>
                    <from>
                        <include pattern="web" />
                    </from>
                </allow>
            </deny>
        </access-rule>

        <!-- =============================================================== -->
        <!-- Libraries gebunden an ein spezifisches Modul -->
        <!-- =============================================================== -->
        <access-rule>
            <message>nur in web erlaubt</message>
            <deny>
                <to>
                    <include class="javax.faces.**" />
                    <include class="javax.servlet.**" />
                    <include class="javax.ws.*" />
                    <include class="javax.enterprise.*" />
                </to>
                <allow>
                    <from pattern="web" />
                </allow>
            </deny>
        </access-rule>

        <access-rule>
            <message>nur in business und persistence erlaubt</message>
            <deny>
                <to>
                    <include class="javax.ejb.**" />
                    <include class="java.sql.**" />
                    <include class="javax.sql.**" />
                    <include class="javax.persistence.**" />
                </to>
                <allow>
                    <from>
                        <include pattern="business" />
                        <include pattern="persistence" />
                    </from>
                </allow>
            </deny>
        </access-rule>

    </ruleset>

</macker>

and in a simple multi module maven project simply put the macker-rules.xml in a central place and point to the directory where it is stored. then you need to configure the plugin in your parent pom.xml

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>de.andrena.tools.macker</groupId>
                <artifactId>macker-maven-plugin</artifactId>
                <version>1.0.2</version>
                <executions>
                    <execution>
                        <phase>compile</phase>
                        <goals>
                            <goal>macker</goal>
                        </goals>
                        <configuration>
                            <rulesDirectory>../</rulesDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

If I was you I would do the following steps:

  • For each layer create two modules. One for interfaces another for the implementation.
  • Do a proper maven dependency avoiding transitive dependencies.
  • Install Sonargraph-Architect plugin in eclipse . It will let you to configure your layer rules.

Hmmmm - interesting. I've certainly run into this problem before, but have never tried to implement a solution. I'm wondering if you could introduce interfaces as an abstraction layer - something similar to the Facade pattern and then declare dependencies on that.

For example, for layers B, and C, create new maven projects that contain just the interfaces into those layers, let's call those projects B' and C'. Then, you would declare dependencies to just the interface layer, rather than the implementation layer.

So A would depend on B' (only). B would depend on B' (because it would implement the interfaces declared there) and C'. Then C would depend on C'. This would prevent the "A uses C" problem, but you would not be able to get the runtime dependencies.

From there, you would need to use maven scope tags to get the runtime dependencies ( http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html ). This is the part that I really haven't explored, but I think you could use a 'runtime' scope to add the dependencies. So you would need to add A depends on B (with runtime scope) and similarly, B depends on C (with runtime scope). Using runtime scope will not introduce compile-time dependencies, so that should avoid reintroducing the "A uses C" problem. However, I'm not sure if this will provide the full transitive dependency closure that you are looking for.

I'd be very interested to hear if you can come up with a working solution.

Probably this is not the solution that you are looking for, and I have no tried it, but maybe you could try using checkstyle.

Imagine the packages in module C are called as " org.project.modulec... ", packages in module B " org.project.moduleb.... " and packages in module A " org.project.modulea.... ".

You could configure the maven-checkstyle-plugin in each module and look for illegal package names. Ie in module A configure as illegal the imports of packages called org.project.modulec. Look at http://checkstyle.sourceforge.net/config_imports.html (IllegalImport)

You could configure maven-checkstyle-plugin and each time you compile check for illegal imports and make the compilation fail.

Maybe you can try this in the pom of A:

<dependency>
    <groupId>the.groupId</groupId>
    <artifactId>moduleB</artifactId>
    <version>1.0</version>
    <exclusions>
        <exclusion>
            <groupId>the.groupId</groupId>
            <artifactId>moduleC</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>the.groupId</groupId>
    <artifactId>moduleC</artifactId>
    <version>1.0</version>
    <scope>runtime</scope>
</dependency>

Can this help you?

I'll suggest something that I've never actually tried myself -- writing unit tests with JDepend to verify architectural dependencies. JDepend documentation gives an example of this as a "Dependency Constraint Test". The two major caveats are

  1. I haven't seen any adoption of this practice in the community,
  2. The JDepend project seems to be abandoned.

The best solution known to me is Structure101 software . It allows you to define rules about your code dependencies and to check them right in the IDE or during your build.

There is a project called archunit .

I never used it before, but you can write JUnit tests to validate you architecture.

You only need to add the following dependency, and you can start writing tests.

<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit</artifactId>
    <version>0.13.1</version>
    <scope>test</scope>
</dependency>

You will have test errors, but not compile time warning, but is does not depend on the IDE.

I would extract interfaces from module B, ie you will have B and B-Impl

In this cases you will get following dependencies:

  • A depends on B
  • B-Impl depends on B and C

For assembling the deployment artifact you can create a separate module without any code which will depend on A and B-Impl

You can define access rules for classpath artifacts in Eclipse. Access rules may be used to map a pattern eg "com.example.*" to a resolution, eg "Forbidden". This leads to a compiler warning when an import to a restricted location is defined.

While this works very well for small code sets, defining access rules can be very tedious on larger projects. Please keep in mind that this is a proprietary Eclipse feature and thus access rules are stored in the Eclpise specific project configuration.

To define access rules follow this clickpath: Project Properties > Java Build Path > Libraries > [Your Library or Maven Module] > Access Rules > Click "Edit"

Access rules may also be defined globally in the Settings menu.

Looks like you're trying to do something that maven does out of the box.

If module A depends on B with an exclude C clause, C classes are not accessible in A without the explicit dependency on C. But they are there for B, since B depends on them directly.

Then when you're packaging your solution, you run assembly or whatever on module R, which is the parent of A, B and C, and collects their dependencies effortlessly.

You can achieve this by making your JAR artifacts OSGI bundles that enforce such layers. Either by hand-crafting your JAR-MANIFEST (also possible via Maven) using OSGI directives or by using tool-support. If you use Maven, you can choose between a variety of maven plugins to achieve this. Likewise for IDE's like Eclipse, where you can choose between different Eclipse plugins like PDE or bndtools .

An alternative tool for build time design layer control is Macker . There is also a maven plugin for that.

If you want to do that, you need an object which can be defined only in the A Layer and which is a key needed from the Layer B . The same thing for the Layer C : it can be accessed only by giving a key (an object) which can only be created from the Layer B .

This is a code I just created which show you how to implement this idea with 3 Classes :

Class A :

public class A
{
    /* only A can create an instance of AKey */
    public final class AKey
    {
        private AKey() {

        }
    }


    public A() {
        B b = new B(new AKey());
        b.f();
    }
}

Class B :

public class B
{
    /* only B can create an instance of BKey */
    public final class BKey
    {
        private BKey() {

        }
    }


    /* B wants an instance of AKey, and only A can create it */
    public B(A.AKey key) {
        if (key == null)
            throw new IllegalArgumentException();

        C c = new C(new BKey());
        c.g();
    }


    public void f() {
        System.out.println("I'm a method of B");
    }
}

Class C :

public class C
{
    /* C wants an instance of BKey, and only B can create it */
    public C(B.BKey key) {
        if (key == null)
            throw new IllegalArgumentException();
    }


    public void g() {
        System.out.println("I'm a method of C");
    }
}

Now, if you want to extend this behaviour to a specific Layer , you can do as shown:

Layer A :

public abstract class AbstractA
{
    /* only SUBCLASSES can create an instance of AKey */
    public final class AKey
    {
        protected AKey() {

        }
    }
}

public class A extends AbstractA
{
    public A() {
        B b = new B(new AKey());
        b.f();

        BB bb = new BB(new AKey());
        bb.f();
    }
}

public class AA extends AbstractA
{
    public AA() {
        B b = new B(new AKey());
        b.f();

        BB bb = new BB(new AKey());
        bb.f();
    }
}

Layer B :

public abstract class AbstractB
{
    /* only SUBCLASSES can create an instance of BKey */
    public final class BKey
    {
        protected BKey() {

        }
    }
}

public class B extends AbstractB
{
    /* B wants an instance of AKey, and only A Layer can create it */
    public B(AbstractA.AKey key) {
        if (key == null)
            throw new IllegalArgumentException();

        C c = new C(new BKey());
        c.g();

        CC cc = new CC(new BKey());
        cc.g();
    }


    public void f() {
        System.out.println("I'm a method of B");
    }
}

public class BB extends AbstractB
{
    /* BB wants an instance of AKey, and only A Layer can create it */
    public BB(AbstractA.AKey key) {
        if (key == null)
            throw new IllegalArgumentException();

        C c = new C(new BKey());
        c.g();

        CC cc = new CC(new BKey());
        cc.g();
    }


    public void f() {
        System.out.println("I'm a method of BB");
    }
}

Layer C :

public class C
{
    /* C wants an instance of BKey, and only B Layer can create it */
    public C(B.BKey key) {
        if (key == null)
            throw new IllegalArgumentException();
    }


    public void g() {
        System.out.println("I'm a method of C");
    }
}

public class CC
{
    /* CC wants an instance of BKey, and only B Layer can create it */
    public CC(B.BKey key) {
        if (key == null)
            throw new IllegalArgumentException();
    }


    public void g() {
        System.out.println("I'm a method of CC");
    }
}

And so on for each layer.

For software structuring, you need to take advantage of the best coding practices and design patterns. I have outline below few points that will definitely help.

  1. Creation of object(s) should be done only in the specialized Factory class(es)
  2. You should code-to and expose only the necessary "interfaces" between layers
  3. You should take advantage of the package scope (default one) class visibility.
  4. If necessary you should split your code into separate sub-projects and (if needed) create separate jar(s) to assure proper inter-layer dependency.

Having a good system design would complete and exceed your goal.

You can describe your architecture using Sonargraph's new DSL:

artifact A
{
  // Pattern matching classes belonging to A
  include "**/a/**"
  connect to B
}  
artifact B
{
  include "**/b/**"
  connect to C
}
artifact C
{
  include "**/c/**"
}

The DSL is described in a series of BLOG articles .

Then you can run Sonargraph via Maven or Gradle or similar in your build and make the build fail when rule violations occur.

Why not simply use different projects for each layer? You put them into your workspace and manage build dependencies as you want.

If you use Spring framework much, you can have a look at enforcing patterns using https://github.com/odrotbohm/moduliths Oliver have also some nice video presentation for this topic. Using java native access modifiers (public, private) can also help a lot.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM