在 Java 中实施分层架构

Enforcing layered architecture in Java

给定一个用Java编写的软件系统,由三层组成,A -> B -> C,即A层使用B层,B层使用C层。

我想确保一层的 class 只能访问同一层的 classes 或其直接依赖项,即 B 应该能够访问 C 但不能A. 另外 A 应该可以访问 B 但不能访问 C。

有没有一种简单的方法来强制执行这样的限制?理想情况下,如果有人试图访问错误层的 class,我希望 eclipse 立即抱怨。

软件目前使用maven。因此,我尝试将 A、B 和 C 放入不同的 Maven 模块并正确声明依赖项。这可以很好地阻止 B 访问 A,但不会阻止 A 访问 C。

接下来我尝试将 C 从对 B 的依赖中排除。这现在也阻止了从 A 到 C 的访问。但是现在我不再能够使用复制依赖来收集 [=26= 所需的所有传递依赖] 时间。

有没有一种好方法可以让我清楚地分离层,还可以让我收集所有需要的运行时间依赖性?

嗯嗯 - 有趣。我以前确实 运行 遇到过这个问题,但从未尝试过实施解决方案。我想知道您是否可以将接口作为抽象层引入 - 类似于 Facade 模式,然后声明对其的依赖关系。

例如,对于 B 层和 C 层,创建仅包含这些层的接口的新 Maven 项目,我们将这些项目称为 B' 和 C'。然后,您将只声明对接口层的依赖关系,而不是实现层。

所以 A 将依赖于 B'(仅)。 B 将依赖于 B'(因为它将实现在那里声明的接口)和 C'。然后 C 将依赖于 C'。这将防止 "A uses C" 问题,但您将无法获得 运行 时间依赖性。

从那里,您将需要使用 Maven 范围标记来获取 运行 时间依赖项 (http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html)。这是我真正没有探索过的部分,但我认为您可以使用 'runtime' 范围来添加依赖项。因此,您需要添加 A 依赖于 B(具有 运行 时间范围),类似地,B 依赖于 C(具有 运行 时间范围)。使用 运行time scope 不会引入编译时依赖,因此应该避免重新引入 "A uses C" 问题。但是,我不确定这是否会提供您正在寻找的完整传递依赖闭包。

我很想知道您是否能提出可行的解决方案。

我将从模块 B 中提取接口,即您将拥有 B 和 B-Impl

在这种情况下,您将获得以下依赖项:

  • A依赖B
  • B-Impl 依赖于 B 和 C

为了组装部署工件,您可以创建一个单独的模块,无需任何依赖于 A 和 B-Impl 的代码

可能这不是您正在寻找的解决方案,我也没有尝试过,但也许您可以尝试使用 checkstyle。

假设模块 C 中的包称为“org.project.modulec...”,模块 B 中的包称为“org.project.moduleb” ....”和模块 A 中的包“org.project.modulea.....”.

您可以在每个模块中配置 maven-checkstyle-plugin 并查找非法包名。 IE。在模块 A 中,将名为 org.project.modulec 的包的导入配置为非法。 查看 http://checkstyle.sourceforge.net/config_imports.html(非法导入)

你可以配置maven-checkstyle-plugin,每次编译检查非法导入,使编译失败。

您可以在 Eclipse 中定义类路径工件的访问规则。访问规则可用于映射模式,例如"com.example.*" 到一个决议,例如"Forbidden"。当定义到受限位置的导入时,这会导致编译器警告。

虽然这对于小型代码集非常有效,但在大型项目中定义访问规则可能会非常乏味。请记住,这是专有的 Eclipse 功能,因此访问规则存储在 Eclpise 特定项目配置中。

要定义访问规则,请遵循以下点击路径: 项目属性 > Java 构建路径 > 库 > [你的库或 Maven 模块] > 访问规则 > 单击 "Edit"

也可以在“设置”菜单中全局定义访问规则。

也许你可以在 A:

的 pom 中试试这个
<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>

这对你有帮助吗?

我将建议一些我自己从未实际尝试过的方法 -- 使用 JDepend 编写单元测试以验证体系结构依赖性。 JDepend documentation 给出了一个 "Dependency Constraint Test" 的例子。两个主要警告是

  1. 我还没有看到社区采用这种做法,
  2. JDepend 项目似乎被放弃了。

我所知的最佳解决方案是 Structure101 software。它允许您定义有关代码依赖性的规则,并在 IDE 或构建期间检查它们。

看起来你正在尝试做一些 maven 开箱即用的事情。

如果模块 A 依赖于 B 并带有 exclude C 子句,则 C 类 在 A 中无法访问而不显式依赖于 C。但它们存在于 B 中,因为 B 直接依赖于它们。

然后当你打包你的解决方案时,你 运行 组件或模块 R 上的任何东西,它是 A、B 和 C 的父级,并毫不费力地收集它们的依赖关系。

您可以通过制作 JAR 工件来实现此目的 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

构建时设计层控制的替代工具是 Macker. There is also a maven plugin

如果你想这样做,你需要一个只能在中定义的对象,它是一个 B 层 需要。 Layer C 也是如此:它只能通过提供 key(一个对象)来访问,它只能从 B层.

这是我刚刚创建的代码,向您展示了如何使用 3 Classes:

实现这个想法

Class一个:

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");
    }
}

现在,如果您想将此行为扩展到特定的 ,您可以按如下所示进行操作:

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();
    }
}

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");
    }
}

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");
    }
}

每一层依此类推。

对于软件结构,您需要利用最佳编码实践和设计模式。我在下面列出了几点肯定会有所帮助。

  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.

拥有良好的系统设计将完成并超越您的目标。

在 maven 中,您可以使用 maven-macker-plugin,如下例所示:

<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>

这里是一个 macker-rules.xml 示例文件:(将其放在与您的 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>

并且在一个简单的多模块maven项目中只需将macker-rules.xml放在一个中心位置并指向它存储的目录。 那么你需要在你的父级中配置插件 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>

如果我是你,我会执行以下步骤:

  • 为每一层创建两个模块。一个用于接口,另一个用于实现。
  • 做一个适​​当的 maven 依赖,避免传递依赖。
  • 安装Sonargraph-Architect plugin in eclipse。它会让你配置你的图层规则。

您可以使用 Sonargraph's 新 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/**"
}

DSL 在一系列 BLOG articles.

中进行了描述

然后您可以 运行 通过 Maven 或 Gradle 或您的构建中的类似方法 运行 Sonargraph,并在发生违反规则时使构建失败。

为什么不简单地为每一层使用不同的项目?您将它们放入您的工作区并根据需要管理构建依赖项。

有一个名为 archunit 的项目。

我以前从未使用过它,但您可以编写 JUnit 测试来验证您的架构。

只需要添加如下依赖,就可以开始写测试了

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

你会有测试错误,但不是编译时警告,而是不依赖于IDE。

如果你经常使用 Spring 框架,你可以看看使用 https://github.com/odrotbohm/moduliths 的强制模式 Oliver 也有一些关于这个主题的不错的视频演示。使用 java 本机访问修饰符(public、私有)也有很大帮助。