Strukturieren und verifizieren

Packageabhängigkeiten managen mit Degraph

Jens Schauder

(c) Shutterstock / ktsdesign

Packages sind ein oft vernachlässigtes Mittel, eine Anwendung zu strukturieren. Mit ein paar einfachen Regeln und dem Open-Source-Tool Degraph können Packages genutzt werden, um das Verständnis von Anwendungen zu vereinfachen, Codeduplizierung zu vermeiden und Wiederverwendung zu ermöglichen.

Nur extrem kleine Projekte kann ein Entwickler vollständig überblicken. Alles was einem realistischen Zweck dient, ist schnell so umfangreich, dass niemand mehr alle Codezeilen im Kopf hat. Zum Glück ist das auch nicht notwendig, denn in der Softwareentwicklung hat sich schon lange das Prinzip der Datenkapselung [1] etabliert. Wir lagern Funktionalität in eigene Komponenten aus und müssen uns dann nicht mehr um die Details kümmern, wie diese Komponenten intern funktionieren.

Für solche Komponenten stehen Java-Entwicklern unterschiedliche Konzepte zur Verfügung. Auf unterster Ebene ist dies die Methode. Methoden werden in Klassen versammelt und Klassen in JAR-Dateien. Das klingt alles ganz wunderbar, bis man einmal schaut, wie groß diese Bündel sind. Die Anzahl Zeilen in einer Methode liegt in der Größenordnung von zehn – hoffentlich zumindest –, ebenso die Anzahl Methoden in einer Klasse. Die Anzahl Klassen in einer JAR-Datei ist aber oft beliebig groß. In diesem Bereich fehlt ein standardisiertes Strukturierungselement. Viele Leser des Eclipse Magazins nutzen sicher OSGi-Module. Andere brechen ihr Projekt in verschiedene Maven-Module auf. Dies ist zwar ein valider Ansatz, bringt aber oft zu viel Overhead mit sich, wenn diese Module nicht auch aus anderen Gründen genutzt werden, z. B. um sie separat auszuliefern.

Es gibt in Java jedoch ein Strukturierungsmittel zwischen Klassen und deploybaren Artefakten: Packages. Leider sind Packages in Java etwas „anämisch“. Außer, dass sie separate Namensräume definieren, bieten sie kaum Funktionalität. Dies wiederum führt dazu, dass sie in vielen Projekten stiefmütterlich behandelt werden. Während viele Entwickler sich Gedanken machen, was in eine Klasse gehört und was besser extrahiert werden sollte, machen das nur wenige, wenn es um Packages geht. Dies halte ich für einen Fehler. Ich setze mich in meinen Projekten deshalb dafür ein, dass für Packages die im Folgenden beschriebenen Regeln gelten.

Packages dürfen keine zyklischen Abhängigkeiten haben

Dass Packages keine zyklischen Abhängigkeiten haben sollen, ist ein Klassiker und wird von vielen statischen Codeanalysetools überprüft. Aber warum ist das eigentlich so wichtig? Packages sind eigenständige Komponenten, d. h. Dinge, die wir zumindest potenziell aus unserer Codebasis heraustrennen können. Dies geht aber nicht mehr, wenn es zyklische Abhängigkeiten gibt. Wenn zwei Packages A und B wechselseitig voneinander abhängen, sind es in Wirklichkeit nicht zwei Komponenten, sondern eine, die sich nur unscharf auf zwei Namensräume verteilt.

Der eine oder andere hat es sicher schon erlebt: Das Nachbarprojekt hat genau das gebaut, was man gerade benötigt, aber leider kann man es nur benutzen, wenn man den ganzen Rest der Anwendung mit verwendet, oder das Ganze heftig refaktorisiert. Genau diese Situationen entstehen, wenn Packages nicht frei von Zyklen sind. Bei zyklenfreien Packagestrukturen findet sich die allgemein hilfreich Funktionalität, die für andere Projekte nützlich ist, weit am Ende des Abhängigkeitsgraphen, mit wenigen oder ganz ohne Abhängigkeiten. Allgemein wachsen durch eine zyklenfreie Struktur die Anteile der Anwendung, die unabhängig voneinander bearbeitet und verstanden werden können. Es können also zwei Personen oder Teams an diesen Bereichen arbeiten, ohne sich gegenseitig zu beeinflussen. Auch kann der eine Teil ohne den anderen verstanden werden. Dies erleichtert das Durchdringen großer Codebasen erheblich.

Namen müssen einem klaren Prinzip folgen

Ein weiteres wichtiges, einfaches, aber oft vernachlässigtes Prinzip ist es, eine klare Struktur für Packagenamen zu verwenden. Nur zu oft findet man auf einer Ebene Subpackages wie: customer, purchasing, persistence, uti. Mit einer solchen Struktur (beziehungsweise mit der Abwesenheit einer klaren Struktur) wird es schwierig, den richtigen Ort für eine Klasse zu finden. Was gehört in das util-Package? Und gehört eine Klasse zum Laden eines Kunden aus der Datenbank in persistence oder in customer?

Dabei können wir einfach Abhilfe schaffen: Auf jeder Ebene muss es genau ein Kriterium geben, anhand dessen entschieden wird, in welches Subpackage eine Klasse gehört. Typische Kriterien sind:

  • Deploybares Artefakt: In welchem JAR oder WAR landen die Artefakte in den verschiedenen Subprojekten? Es ergibt wenig Sinn, Dinge, die nicht gemeinsam ausgeliefert werden, in ein Package zu stecken. Die einzige Ausnahme von dieser Regel sind Tests, die üblicherweise im gleichen Package wie der zu testende Code liegen, aber normalerweise nicht gemeinsam mit ausgeliefert werden.
  • Fachliches Modul: Dies ist das vielleicht am häufigsten vernachlässigte Unterscheidungskriterium, dabei ist es eins der wichtigsten. Fachliche Module, auch wenn sie nur als Packages existieren, kommunizieren jemandem, der den Code liest, sehr viel über die Anwendung: Was sind die wesentlichen fachlichen Begriffe? Worum geht es eigentlich in der Anwendung? Dies kann eine saubere fachliche Struktur dokumentieren.
  • Technische Schicht: Dies ist wohl das Kriterium, das den meisten als Erstes einfällt, wenn sie über Packagestrukturen nachdenken. Dabei ist es vielleicht das Kriterium, auf das man am ehesten verzichten kann. Natürlich sollte die UI-Schicht nicht direkt auf die Persistenzschicht zugreifen. Aber wenn die fachlichen Module klein sind, sind in der technischen Schicht eines fachlichen Moduls oft nur wenige oder sogar nur eine einzige Klasse vorhanden. Da kann man mit ein wenig Disziplin auch so darauf achten, dass nur die Abhängigkeiten existieren, die wirklich sinnvoll sind. Dennoch ist dies ebenfalls ein valides Kriterium, um eine Codebasis zu unterteilen.

Wichtig ist, dass auf jeder Ebene exakt ein solches Kriterium verwendet wird und nicht mehrere verschiedene. Insgesamt ergibt sich dann für Enterprise-Anwendungen immer wieder eine Package-Struktur wie die folgende:

<domainprefix>.<produkt>.<deployable>.<fachlichesmodul>.<schicht>.<subpackage>

Dabei ist das subpackage optional und kann eingeführt werden, wenn es auf unterster Ebene noch immer zu viele Klassen gibt – obwohl dies vermutlich besser behoben werden kann, wenn man die fachlichen Module weiter unterteilt.

Keine Zyklen auf Slice-Ebene

Die unterschiedlichen Kriterien, nach denen einen Anwendung unterteilt werden kann, nenne ich „Slicings“, basierend auf einem Blogartikel von Oliver Gierke [2]. Die Elemente eines Unterteilungskriteriums sind Slices. Also ist zum Beispiel das Unterteilen in fachliche Module ein Slicing, und das Auftragsmodul ist ein Slice. Für jedes dieser Slicings gilt: Seine Elemente dürfen keine zyklischen Abhängigkeiten haben. Die Gründe für diese Regel sind die gleichen wie bei Packages: Es sind Unterteilungen der Anwendung, die dazu dienen, eventuell einmal ausgetauscht oder von separaten Teams weiterentwickelt zu werden. Und dies funktioniert nicht, wenn Abhängigkeiten kreuz und quer in alle Richtungen gehen.

Abhängigkeiten überprüfen

Vor vielen Jahren habe ich einen Test geschrieben, der diese Art von Struktur prüfte und fehlschlug, wenn sie nicht eingehalten wurde. Prinzipiell eine gute Idee, nur leider gehen Menschen tendenziell den Weg des geringsten Widerstands. Und was ist die einfachste Methode, um Packagezyklen zu vermeiden? Richtig: alle Klassen in ein Package verschieben.

Für jemanden, für den Packages und ihre Struktur wichtig sind, ist ein Package mit 150 Klassen kein schöner Anblick. Ich habe viele getroffen, die an dieser Stelle versucht sind, auf das Team zu schimpfen. Aber das ist meiner Meinung nach ein schwerer Fehler. Wer als Leadentwickler, Architekt, oder wie auch immer man sich gerade nennt, Regeln aufstellt, hat die Verantwortung, dass das Team einerseits diese Regeln sowie den Grund für die Regeln versteht, und andererseits auch das nötige Wissen sowie die nötigen Werkzeuge zum Einhalten der Regeln hat. Bei den letzten beiden Punkten hatte ich kläglich versagt.

Wie aber bricht man zyklische Abhängigkeiten zwischen Packages auf? In vielen Fällen kann man Klassen oder Teile von Klassen in ein anderes Package verschieben und dadurch einen Zyklus zwischen Packages auflösen. Ich nenne so etwas einen „unechten Zyklus“, da eigentlich kein Zyklus da wäre, wenn alle Klassen in dem Package wären, in das sie gehören. Aber was tun, wenn ein echter Zyklus zwischen zwei oder mehreren Klassen besteht, die in zwei unterschiedliche Packages gehören? Sozusagen der Worst Case? Betrachten wir ein Beispiel (Listing 1).

public class Person {
  public String name;
  public Adress homeAdress = new Adress();
  public List<Order> orderHistory = new ArrayList<>();
 
  public Person(){
    homeAdress.tenant = this;
  }
}

public class Adress {
  public Person tenant;
  // more code down here. 
}


Eine Person hat eine Adresse, und die Adresse verfügt über eine Rückreferenz auf die Person –offensichtlich ein Zyklus. Um Missverständnisse zu vermeiden: Wenn beide Klassen im gleichen Package sind, ist ein solcher Zyklus meist völlig in Ordnung. Wir sprechen hier von dem Fall, dass das Handling von Personen und Adressen in zwei separate Packages getrennt werden soll. Das heißt im Umkehrschluss, dass es eine ordentliche Portion Funktionalität gibt, die sich mit Personen beschäftigt, und ebenso Funktionalität, die sich mit Adressen beschäftigt.

Wie löst man also den Zyklus auf? Einfach: Man führt für eine der beiden Klassen ein Interface ein. In diesem Fall entscheide ich mich für die Person. Wie alle guten Interfaces (siehe Interface Segregation Principle [3]) ist das Interface aus Sicht des Nutzers (in diesem Fall der Adresse) formuliert. Dies bedeutet, dass das Interface nur die Funktionen enthält, die eine Adresse wirklich benötigt. In dem Beispiel ist es sicherlich plausibel, dass die Adresse den Namen benötigt, aber vermutlich wird sie den Namen nicht verändern. Mein Name hat sich durch einen Umzug jedenfalls noch nie verändert. Ein Interface und die angepassten Klassen könnten also aussehen wie in Listing 2.

public interface Named { 
  String getName();
}

public class Person implements Named{
  private String name;

  public Adress homeAdress = new Adress();
  public List<Order> orderHistory = new ArrayList<>();
 
  public Person(){
    homeAdress.tenant = this;
  }
  
  @Overwrite
  public String getName(){
    return name;
  }
 
  public void setName(String name){
    this.name = name;
  }
}

public class Adress {
  public Named tenant;
  // more code down here.
}

In vielen Fällen benötigt man noch eine zusätzliche Klasse, die die Instanzen der beiden beteiligten Klassen einander bekannt macht, denn zu Laufzeit soll bzw. muss sogar eine zirkuläre Abhängigkeit bestehen. Diese Klasse lässt sich natürlich durch einen Dependency-Injection-Mechanismus wie Spring, CDI oder dergleichen ersetzen.

Dieses Vorgehen macht den Code zunächst komplexer. Es gibt mehr Teile, d. h. Klassen und Interfaces, die miteinander interagieren. Allerdings muss man, um einen Teil des Codes zu verstehen, weniger über den Rest des Codes wissen. Damit wird die meist relevante Aufgabe, einen Teil des Codes zu verstehen, einfacher. Darüber hinaus enthält der Code auch Information in expliziter Form, die vorher nicht vorhanden war: Was genau erwartet eine Adresse von einer Person? In sehr vielen Fällen, in denen ich dieses Prinzip verwendet habe, haben sich diese Schnittstellen weiterentwickelt und wurden zu relevanten Abstraktionen in dem Projekt, obwohl sie vorher niemand so recht wahrgenommen hat. Umgekehrt wirken diese Abstraktionen auch als Warnung für Entwickler, die versucht sind, ein solches Interface um Methoden zu erweitern, die einfach nicht zu dem (hoffentlich gut gewählten) Namen des Interface passen. Ein solcher Fall ist eine starke Indikation dafür, dass das Design noch einmal überdacht werden sollte.

Degraph stellt Abhängigkeiten grafisch dar

In dem trivialen Beispiel ist es einfach, den Zyklus aufzulösen, weil der Zyklus nur aus zwei Klassen besteht und auf eine Bildschirmseite passt. In der Praxis liegt der Fall oft anders. A priori ist noch nicht einmal klar, welche Klassen an einem Zyklus beteiligt sind. Es gibt einige Tools, die Zyklen wie die beschriebenen entdecken können. Aber in vielen Fällen bieten die Tools nur wenig mehr als eine Structure-Dependency-Matrix an, das heißt eine Matrix, in der entlang beider Achsen die Packages aufgelistet sind und in den Feldern die Anzahl der Abhängigkeiten vom Package der Zeile zum Package der Spalte steht. Die Tools versuchen, dies in die untere Diagonalform zu bringen, sodass alle Abhängigkeiten unter der Diagonale stehen. Ist dies nicht möglich, liegen Zyklen vor. Aber welche Packages sind an dem Zyklus beteiligt? Und welche Klassen? Und wie kann man eingreifen, um den Zyklus aufzulösen. Ich habe noch niemanden gefunden, der derartige Informationen aus einer solchen Matrix ablesen könnte.

Dies ist der Grund dafür, warum ich Degraph [4] entwickelt habe. Es stellt Klassen mit ihren Abhängigkeiten in ihren Packages grafisch dar, sodass man vergleichsweise übersichtlich erkennen kann, welche Klassen an einem Zyklus beteiligt sind, und was man dagegen tun kann.

Alternativen zu Degraph
Natürlich ist Degraph nicht das einzige Tool, das sich mit diesem Thema beschäftigt. Aber freien Tools, die ich gefunden habe, mangelte es immer an der einen oder anderen Stelle. Kommerzielle Werkzeuge können in vielen Umfeldern hingegen nicht eingesetzt werden, da Organisationen oft nicht in der Lage sind, ein paar hundert Euro für ein Tool auszugeben. Ein besonders vielversprechendes Werkzeug scheint Structure101 [7] zu sein. Es ist vermutlich wesentlich leistungsfähiger als Degraph, aber kostenpflichtig.

 

Degraph lässt sich auf zwei Arten nutzen: entweder als Kommandozeilentool oder als Testbibliothek, die mit JUnit oder einem anderen Testframework verwendet werden kann. Als Kommandozeilentool generiert es aus Bytecode, d. h. aus *.class-Dateien oder *.jar-Dateien, eine *.graphml-Datei. GraphML ist eine XML-Variante für die Darstellung von Graphen. Mit yED [5], einem freien Editor für Graphen, kann man eine solche Datei öffnen, layouten und sich so seine Packageabhängigkeiten in aller Ruhe anschauen. Wichtiger ist aber die Verwendung in Tests. Ein einfacher Test mit Degraph ist in Listing 3 zu sehen.

package de.schauderhaft.degraph.meta;

import de.schauderhaft.degraph.configuration.NamedPattern;
import org.junit.Test;

import static de.schauderhaft.degraph.check.JCheck.classpath;
import static de.schauderhaft.degraph.check.JCheck.violationFree;
import static org.junit.Assert.assertThat;

public class DemoDependencyTest {
  @Test
  public void test() {
    assertThat(classpath() // (1)
      .noJars() // (2)
      .printTo("violations.graphml") // (3)
      .including("de.schauderhaft.**") // (4)
      .withSlicing("part", "de.schauderhaft.degraph.(*).**") // (5)
      .withSlicing("layer",
        "de.schauderhaft.degraph.*.*.(*).**",
        new NamedPattern("**", "other") // (6)
      ).withSlicing("module", "de.schauderhaft.degraph.*.(*).**"), 
    violationFree());
  }
}

Es werden die Klassen aus dem aktuellen Klassenpfad analysiert (1), wobei .jar-Dateien ignoriert werden (2). Dies ist meist ein sinnvolles Vorgehen, da man die eigenen Klassen analysieren möchte und nicht die von irgendwelchen Fremdbibliotheken. Der Aufruf von printTo (3) sorgt dafür, dass im Fehlerfall eine GraphML-Datei erzeugt wird, die zur Analyse von Problemen verwendet werden kann. Möchte man nur bestimmte Klassen analysieren, kann man dies mit including (4) und excluding erreichen, wobei ANT-artige Patterns verwendet werden, bei denen * für beliebige Zeichen außer dem Punkt steht und ** für beliebige Zeichen inklusive Punkt. withSlicing (5) definiert, wie der Code unterteilt werden kann. Der erste Parameter ist der Name des Slicings. Dann folgt eine beliebige Anzahl von Mustern, die ein oder mehrere Slices definieren. Dafür gibt es zwei Varianten: Entweder wird ein Muster als String angegeben, wobei ein Teil des Musters in Klammern eingeschlossen ist. Der Teil des Musters in den Klammern wird zum Namen des Slices. Beispiel: Das Muster de.schauderhaft.degraph.(*).** sorgt dafür, dass eine Klasse namens de.schauderhaft.degraph.demo.order.persistence.OrderDao in dem Slice demo landet. Alternativ kann man auch ein benanntes Pattern angeben (6), d. h. Klassen, die dem Muster entsprechen, werden dem Slice mit dem angegebenen Namen zugeordnet. In dem Beispiel werden also drei verschiedene Slicings definiert. Schließlich werden alle diese Slicings, so wie die Packages, auf Zyklenfreiheit geprüft.

Abhängigkeiten visualisieren

Wenn ein Fehler aufgetreten ist, d. h. der Test Zyklen gefunden hat, oder man das Kommandozeilentool verwendet hat, um eine GraphML-Datei zu erzeugen, möchte man sich das Ergebnis vermutlich grafisch anschauen. Dazu öffnet man die GraphML-Datei mit yED. Dort sieht man zunächst nur einen Kasten und keinen Graphen. Dies liegt daran, dass Degraph alle Knoten in den Koordinatenursprung legt. Sie müssen erst noch mit yED sinnvoll verteilt werden. Dazu öffnet man aus dem Menü Layout das Hierarchic Layout. Alle Angaben beziehen sich hier auf die englische Version von yED. Durch den Button Dock heftet man den Layoutdialog an den Rand der Anwendung. Dies ist sinnvoll, da man ihn öfters benötigt. Ich empfehle, die folgenden Einstellungen vorzunehmen:

General

Orientation: Left to Right

 

Edges

Routing Style: Polyline

Dann das Layout ausführen, und yED zieht die Knoten auseinander in einen Graphen, bei dem, wenn möglich, alle Abhängigkeiten von links nach rechts zeigen. Zyklen, die Degraph entdeckt hat, werden rot markiert. Knoten von Packages und Slices lassen sich aufklappen, sodass man die inneren Knoten sehen kann. Jedes Mal, wenn man einen Knoten auf- oder zuklappt, muss man das Layout neu starten, damit alles ordentlich aussieht.

Jedes noch so ordentlich strukturierte System sieht unordentlich aus, wenn es eine gewisse Größe hat. Daher hat es sich bewährt, Knoten, die uninteressant für die aktuelle Fragestellung sind, einfach zu löschen.

Packages nicht strukturieren!

Bei der Arbeit an Degraph habe ich mich naturgemäß viel mit Packagestrukturen beschäftigt. Ich bin auch über einige weit verbreitete Anti-Pattern gestolpert. Den unechten Zyklus (Abb. 1) haben wir schon kennengelernt. Hier entsteht ein Zyklus zwischen Packages dadurch, dass eine oder mehrere Klassen im falschen Package gelandet sind. Derartige Zyklen lassen sich einfach durch Verschieben der entsprechenden Klassen wieder beheben.

schauder_degraph_1

Abb. 1: Ein unechter Zyklus lässt sich einfach durch das Verschieben der entsprechenden Klassen beheben

 

Den echten Zyklus (Abb. 2) haben wir ebenfalls schon betrachtet, mit der Lösungsstrategie, durch Einführen von Interfaces den Zyklus aufzulösen (Abb. 3).

schauder_degraph_2

Abb. 2: Bei einem echten Zyklus sind es in Wirklichkeit nicht zwei Komponenten, sondern eine, die sich nur unscharf auf zwei Namensräume verteilt

schauder_degraph_3

Abb. 3: Ein echter Zyklus lässt sich durch das Einführen von Interfaces aufbrechen

 

Ein Anti-Pattern, das in Degraph sehr schön sichtbar wird, ist die Umleitung (Abb. 4). Sie sieht zunächst unverdächtig aus, da alle Pfeile bei einem entsprechenden Layout von links nach rechts verlaufen. Dennoch liegt ein Zyklus vor.

schauder_degraph_4

Abb. 4: Eine Umleitung sieht unverdächtig aus, ist aber auch ein Zyklus

 

Bei genauerer Betrachtung erkennt man, dass verschiedene Abhängigkeitspfade bei der Klasse bzw. den Klassen ganz rechts landen, einige dabei jedoch durch ein anderes Package laufen. Dies entsteht, wenn nicht zwischen Klassen unterschieden wird, die unabhängig von anderen Packages sind, und solchen, die übergreifend arbeiten. Ein Beispiel ist ein Plug-in-Mechanismus. Es gibt ein Interface, das von verschiedenen Plug-ins implementiert werden soll, und es gibt für die konkrete Anwendung eine Klasse, die die benötigten Plug-ins lädt. Das Interface ist unabhängig von anderen Packages. Es kann existieren, egal ob andere Packages vorhanden sind oder nicht. Die Klasse jedoch, die das Laden übernimmt, hängt ganz klar von den verwendeten Implementierungen des Interface ab. Da aber Interfaces und Plug-in-ladende-Klasse beide als zu dem Plug-in-Mechanismus gehörig wahrgenommen werden, landen sie gerne in demselben Package. Spätestens nach der Analyse dieses Anti-Patterns sollte klar sein, wie man es auflöst: Man trennt die Package-übergreifende Verwendung von der Package-unabhängigen Infrastruktur in zwei separate Packages, und der Zyklus löst sich in Wohlgefallen auf (Abb. 5).

 

schauder_degraph_5

Abb. 5: Man trennt die Package-übergreifende Verwendung von der Package-unabhängigen Infrastruktur in zwei separate Packages, und der Zyklus löst sich auf

 

Ähnlich wie es Gottklassen gibt, die zu viel tun und Abhängigkeiten in alle Richtungen haben, gibt es das Äquivalent auch für Packages: Packages mit einer sehr große Anzahl von Abhängigkeiten (Abb. 6).

schauder_degraph_6

Abb. 6: Ein Gottpackage hat zu viele Abhängigkeiten

 

Ähnlich wie bei ihren Entsprechungen auf Klassenebene ist die Weiterentwicklung solcher Packages sehr schwer. Abgemildert bekommt man dieses Problem analog zum Vorgehen bei Klassen, indem einerseits die Aufgaben des Packages zwischen mehreren Packages verteilt werden, und andererseits durch das Einführen von Interfacepackages, bei denen Teile, von denen andere abhängen (die Interfacepackages), und Teile, die von anderen abhängen (die Implementierungen), getrennt werden. Interfacepackages sind dabei nicht Packages, die nur aus Interfaces bestehen. Wenn ein Interface einen Typ als Parameter bekommt oder als Rückgabewert verwendet, so gehört dieser Typ ebenfalls zum Interfacepackage. Denn sowohl die Implementierung als auch die Nutzer des Interface benötigen immer neben dem Interface auch diesen Parameter oder Rückgabewert.

Das letzte Anti-Pattern sind die „Wäscheleinen“ (Abb. 7). Diese Muster sehen, wenn man sie mit Degraph darstellt, aus wie lang gespannt Seile: viele parallele Abhängigkeiten zwischen zwei oder mehreren Packages. Wäscheleinen entstehen, wenn Klassen, die nichts miteinander zu tun haben, in ein Package gepackt werden. Warum sollte jemand so etwas tun? Dieses Vorgehen ist häufiger als man denken mag und wird sogar von einigen Tools und Frameworks als Default vorgeschlagen. Es basiert darauf, nicht Klassen, die eng zusammenarbeiten, zu einem Package zusammenzufassen, sondern Klassen der gleichen Art: Exceptions in ein Package, Hibernate Entities in ein anderes, Interfaces in eins und Repositories in ein weiteres. Dieser Ansatz ist vermutlich so weit verbreitet, weil er ein einfaches klares Kriterium dafür bietet, was in welches Package gehört. Aber es verletzt massiv das Prinzip „High Cohesion/Low Coupling“ [6]. Dieses Prinzip besagt, dass Dinge zusammengepackt werden sollen, die intensiv zusammenarbeiten, und es zwischen diesen Packungen (also in diesem Fall Packages) möglichst wenige Abhängigkeiten geben soll.

schauder_degraph_7

Abb. 7: Die Wäscheleinen zeugen davon, dass falsch zusammengepackt wurde

 

Die Gegenmaßnahme für eine solche Packagestruktur ist natürlich, auf eine sauberere Packagestruktur zu wechseln. Dabei ist der Unterschied oft gar nicht so einfach, wie es zunächst klingt. Ist ein Package …interface nur eine Sammlung unabhängiger Interfaces? Oder stellt es die Schnittstelle zu einer Komponente dar – einem Package oder einer Gruppe von Packages? Allein an dem Namen lässt sich das nicht entscheiden. Dieser feine Unterschied ist aber wesentlich.

Fazit

Packages sind ein wertvolles Mittel, um eine Anwendung klarer zu strukturieren. Die Packages, wie sie von der JVM angeboten werden, sind alleine aber nicht mächtig genug. Man sollte sie ergänzen um ein Tool, das die Abhängigkeiten prüft und visualisieren kann. Degraph ist ein freies Open-Source-Werkzeug für diesen Zweck.

Verwandte Themen:

Geschrieben von
Jens Schauder
Jens Schauder
Jens Schauder ist leidenschaftlicher Softwareentwickler. Er arbeitet bei der T-Systems on site services GmbH als Senior Consultant. Besonders interessiert ihn das Thema Qualität in der Softwareentwicklung. In seiner Freizeit entwickelt er Degraph.
Kommentare

Hinterlasse einen Kommentar

2 Kommentare auf "Packageabhängigkeiten managen mit Degraph"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Jörg Rade
Gast

Moin,

der link [4] zeigt auf http://blog.schauderhaft.de/degraph/

Viele Grüße
Jörg

Redaktion JAXenter
Gast

Hallo Jörg,

der Link wurde korrigiert. Vielen Dank für den Hinweis!

Redaktion JAXenter