[英]Enforcing layered architecture in Java
給定一個用Java編寫的軟件系統,由三層組成,A -> B -> C,即A層使用B層,B層使用C層。
我想確保一層的類只能訪問同一層的類或其直接依賴項,即 B 應該能夠訪問 C 但不能訪問 A。A 也應該能夠訪問 B 但不能訪問 C。
有沒有一種簡單的方法來強制執行這樣的限制? 理想情況下,如果有人試圖訪問錯誤層的類,我希望 eclipse 立即抱怨。
該軟件目前使用maven。 因此,我嘗試將 A、B 和 C 放入不同的 maven 模塊並正確聲明依賴項。 這可以很好地阻止 B 訪問 A,但不會阻止 A 訪問 C。
接下來我嘗試從對 B 的依賴中排除 C。這現在也阻止了從 A 到 C 的訪問。但是現在我不再能夠使用復制依賴來收集運行時所需的所有傳遞依賴。
有沒有一種好方法可以讓我清晰地分離層,而且還可以讓我收集所有需要的運行時依賴項?
在 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 項目中,只需將 macer-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>
如果我是你,我會執行以下步驟:
嗯——有趣。 我以前肯定遇到過這個問題,但從未嘗試過實施解決方案。 我想知道您是否可以將接口作為抽象層引入 - 類似於 Facade 模式的東西,然后聲明對其的依賴關系。
例如,對於層 B 和 C,創建僅包含這些層的接口的新 maven 項目,我們將這些項目稱為 B' 和 C'。 然后,您將只聲明對接口層的依賴關系,而不是實現層。
所以 A 將取決於 B'(僅)。 B 將依賴於 B'(因為它將實現那里聲明的接口)和 C'。 那么C將取決於C'。 這將防止“A 使用 C”問題,但您將無法獲得運行時依賴項。
從那里,您需要使用 maven 范圍標簽來獲取運行時依賴項( http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html )。 這是我真正沒有探索過的部分,但我認為您可以使用“運行時”范圍來添加依賴項。 因此,您需要添加 A 依賴於 B(具有運行時范圍),類似地,B 依賴於 C(具有運行時范圍)。 使用運行時范圍不會引入編譯時依賴關系,因此應該避免重新引入“A 使用 C”問題。 但是,我不確定這是否會提供您正在尋找的完整傳遞依賴閉包。
我很想知道你是否能提出一個可行的解決方案。
可能這不是您正在尋找的解決方案,我還沒有嘗試過,但也許您可以嘗試使用 checkstyle。
想象一下,模塊 C 中的包被稱為“ org.project.modulec... ”,模塊 B 中的包“ org.project.moduleb.... ”和模塊 A 中的包“ org.project.modulea...”。 ”。
您可以在每個模塊中配置 maven-checkstyle-plugin 並查找非法包名稱。 即在模塊 A 中將名為 org.project.modulec 的包的導入配置為非法。 查看http://checkstyle.sourceforge.net/config_imports.html (IllegalImport)
您可以配置 maven-checkstyle-plugin 並且每次編譯時檢查非法導入並使編譯失敗。
也許你可以在 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 文檔給出了一個“依賴約束測試”的例子。 兩個主要的警告是
我所知道的最好的解決方案是Structure101 軟件。 它允許您定義有關代碼依賴項的規則,並在 IDE 中或在構建期間直接檢查它們。
有一個名為archunit的項目。
我以前從未使用過它,但您可以編寫 JUnit 測試來驗證您的架構。
只需要添加如下依賴,就可以開始寫測試了。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
您將有測試錯誤,但不是編譯時警告,而是不依賴於 IDE。
我會從模塊 B 中提取接口,即你將有 B 和 B-Impl
在這種情況下,您將獲得以下依賴項:
為了組裝部署工件,您可以創建一個單獨的模塊,而無需任何依賴於 A 和 B-Impl 的代碼
您可以為 Eclipse 中的類路徑工件定義訪問規則。 訪問規則可用於將模式(例如“com.example.*”)映射到分辨率(例如“Forbidden”)。 當定義了對受限位置的導入時,這會導致編譯器警告。
雖然這對於小型代碼集非常有效,但在大型項目中定義訪問規則可能非常乏味。 請記住,這是一個專有的 Eclipse 特性,因此訪問規則存儲在 Eclpise 特定的項目配置中。
要定義訪問規則,請遵循以下點擊路徑:項目屬性 > Java 構建路徑 > 庫 > [您的庫或 Maven 模塊] > 訪問規則 > 單擊“編輯”
訪問規則也可以在設置菜單中全局定義。
看起來你正在嘗試做一些 maven 開箱即用的事情。
如果模塊 A 依賴於帶有 exclude C 子句的 B,則 C 類在沒有顯式依賴於 C 的情況下不能在 A 中訪問。但是它們存在於 B 中,因為 B 直接依賴於它們。
然后當你打包你的解決方案時,你在模塊 R 上運行程序集或其他任何東西,它是 A、B 和 C 的父級,並毫不費力地收集它們的依賴項。
您可以通過使您的 JAR 工件成為強制執行這些層的OSGI包來實現這一點。 通過使用 OSGI 指令或使用工具支持手工制作 JAR-MANIFEST(也可以通過 Maven)。 如果您使用 Maven,您可以在各種 Maven 插件之間進行選擇來實現這一點。 對於像 Eclipse 這樣的 IDE,您可以在不同的 Eclipse 插件(如PDE或bndtools )之間進行選擇。
如果你想這樣做,你需要一個只能在A 層中定義的對象,它是B 層需要的鍵。 層 C 也是一樣:它只能通過提供一個只能從層 B創建的鍵(一個對象)來訪問。
這是我剛剛創建的代碼,它向您展示了如何使用3 個類實現這個想法:
甲類:
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();
}
}
乙類:
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");
}
}
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");
}
}
現在,如果您想將此行為擴展到特定的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();
}
}
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");
}
}
對每一層依此類推。
對於軟件結構,您需要利用最佳編碼實踐和設計模式。 我概述了以下幾點肯定會有所幫助。
- 對象的創建應該只在專門的工廠類中完成
- 您應該編碼並僅公開層之間必要的“接口”
- 您應該利用包范圍(默認范圍)類的可見性。
- 如有必要,您應該將代碼拆分為單獨的子項目,並(如果需要)創建單獨的 jar 以確保適當的層間依賴關系。
擁有良好的系統設計將完成並超越您的目標。
您可以使用Sonargraph 的新 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 文章中進行了描述。
然后,您可以在構建中通過 Maven 或 Gradle 或類似方式運行 Sonargraph,並在發生規則違規時使構建失敗。
為什么不簡單地為每一層使用不同的項目? 您將它們放入您的工作區並根據需要管理構建依賴項。
如果您經常使用 Spring 框架,則可以使用https://github.com/odrotbohm/moduliths查看強制模式,Oliver 也針對此主題提供了一些不錯的視頻演示。 使用 Java 本機訪問修飾符(公共、私有)也有很大幫助。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.