Kommentierung
Inhalt:
Package-Kommentare
Klassen-Kommentare
Methoden-Kommentare
JavaDoc erzeugen
Kommentare innerhalb Methoden
Allgemein
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
Real programmers don't comment their code - it was hard to write, it should be hard to understand
JavaDoc-Kommentare finden zwei Verwendungen:
a) Lesbarkeit des Quellcodes verbessern (z.B. wenn ein neuer Programmierer das Projekt übernimmt)
b) Dokumentation für Closed-Source-Bibliotheken, die ohne Quellcode vertrieben werden, oder auch Open-Source-Bibliotheken,
bei denen man den Quellcode nicht direkt zur Hand hat.
In letzterem Fall ist eine gute Dokumentation besonders wichtig.
Die Kommentierung erfolgt auf drei Ebenen:
- Klassen-Kommentare: beschreiben die Klassen im Überblick.
- Methoden-Kommentare: beschreiben die einzelnen Methoden im Detail
- Kommentare innerhalb des Code: diese sind für den Nutzer einer Library unwichtig, aber sinnvoll für den, der sich mit dem Quellcode befassen muss.
Gute Kommentare sollten alle Fragen beantworten, die sich bei einen Blick auf Klasse oder Methode stellen.
Package-Kommentare
Die Package-Kommentare geben einen kurzen Überblick über das jeweilige Package (bzw. in unserem Fall wohl die gesamte Anwendung).
Siehe auch http://java.sun.com/j2se/javadoc/writingdoccomments/#packagecomments.
Man legt sie nicht im Quellcode an, sondern legt eine eigene HTML-Datei ins Projekt (oder an beliebige andere Stelle). Auf der verlinkten Sun-Seite
gibt es ein Template hierfür.
Nach dem Javadoc-Lauf (siehe unten) landet diese Datei als "overview-summary.html" in der erzeugten Doku.
Zur Verwendung in Eclipse siehe unten.
Beispiel für einen Package-Kommentar: http://java.sun.com/javase/6/docs/api/java/util/package-summary.html.
Klassen-Kommentare
Die Kommentierung der Klasse sollte einen Überblick über die Funktion der Klasse geben. Nach dem Lesen der Klassenkommentare sollte
man wissen, wofür sie überhaupt da ist, und sollte die grundlegende Funktionsweise kennen.
Dies alles gilt natürlich auch für Interfaces ! Falls eine Klasse ein Interface implementiert, sollten die Kommentare an beiden Stellen identisch sein.
Beispiel 1: org.apache.commons.lang.StringUtils
aus der Library "commons-lang" der Apache Foundation:
http://commons.apache.org/lang/api-release/org/apache/commons/lang/StringUtils.html.
Es handelt sich um eine Sammlung von static Methoden, die keine Wechselwirkungen untereinander haben. Deshalb hat die Klasse keinen Status
(in Form von Membervariablen).
Was finden wir hier an Vorteilen ?
- Guter Grobüberblick: Der Kommentar gibt hauptsächlich ein Kurzüberblick über die möglichen Methoden, wodurch man alles schnell erfassen kann.
- Grundlegende Konzepte: Im Klassenkommentar wird erklärt, was in den folgenden Methoden z.B. unter einem "Whitespace" zu verstehen ist.
Beispiel 2: java.text.SimpleDateFormat
: http://java.sun.com/javase/6/docs/api/java/text/SimpleDateFormat.html.
Hier handelt es sich eine Klasse, von der man Instanzen erzeugen muss. Die Klassen-Kommentare gehen leider wenig auf die Verwendung ein.
Was können wir hier lernen ?
- Erzeugung von Instanzen: Der Kommentar weißt darauf hin, dass man Instanzen idealerweise über static Methoden erzeugt.
- Funktionsweise: Die Datumsformate werden sehr detailliert beschrieben, einschließlich Beispielen.
Beispiel 3:java.util.Hashtable
: http://java.sun.com/javase/6/docs/api/java/util/Hashtable.html
Dies ist ebenfalls eine Klasse, von der Instanzen erzeugt werden. Der Klassenkommentar gibt einen groben Überblick über die Verwendung und geht
detailliert auf Performanz-Fragen ein.
Was bringt uns das ?
- Klassenüberblick: hier ist in Fließtext-Form die grundlegende Verwendung der Klasse beschrieben.
- Besonderheiten bei der grundlegenden Verwendung der Klasse: es wird einigermaßen detailliert auf "initial capacity" und "load factor" eingegangen.
- Besonderheiten bei einzelnen Aspekten der Klasse: es wird explizit darauf hingewiesen dass der Iterator-Aspekt des Hashtable
unter gewissen Umständen gefährlich sein kann.
Gegenbeispiel 4:
aus dem JBoss 4.0.5: http://docs.jboss.org/jbossas/javadoc/4.0.5/system/org/jboss/deployment/MainDeployer.html
Bei dieser schicken Klasse besteht der Kommentar aus "The main/primary component for deployment."
Es steht nichts dazu, was sie überhaupt macht oder wie man Instanzen erzeugt (das ist vor allem knifflig, wenn eine Client-Anwendung von außerhalb des
Servers auf sie zugreift).
Zusammenfassung:
Ein Klassenkommentar sollte:
- Einen Überblick über die Verwendung geben.
- Erklären, wie man an Instanzen der Klasse kommt (falls es nicht über einen Standard-Konstruktor geht)
- Grundkonzepte der Klasse erklären
- Sonstige wichtige Informationen über die Klasse geben (z.B. Performanz-Betrachtungen, Beziehung zu anderen Klassen)
Methoden-Kommentare
Die nächste Stufe der Kommentierung nach den Klassenkommentare sind die Methoden-Kommentare.
Die Kommentare müssen für die jeweilige Methode beschreiben, was genau sie tut, und welche Rahmenbedingungen zu beachten sind.
Beispiel 1: aus dem Stateless-Beispiel:
/**
* Berechnet ein Quader-Volumen
*
* @param a Seite a
* @param b Seite b
* @param c Seite c
* @return Volumen
* @exception InvalidParameterException
*/
public double computeCuboidVolume(double a, double b, double c) throws InvalidParameterException
Die grundlegende Funktionalität ist beschrieben.
Nicht so gelungen sind hier:
- Aus der Dokumentation geht nicht hervor, dass es Einschränkungen für die Parameter a, b und c gibt. Gemäß Doku wären hier +5, 0 und -5 zulässig.
- Es wird nirgends erklärt wie das Volumen überhaupt ermittelt wird.
- Wann und wie die Exception ausgelöst wird, wird nirgends erklärt.
Beispiel 2: das gleiche in besser:
/**
* Berechnet ein Quader-Volumen nach der Formel "a*b*c"
*
* @param a Seite a, mit Wert >= 0
* @param b Seite b, mit Wert >= 0
* @param c Seite c, mit Wert >= 0
* @return Volumen nach der Formel "V = a * b * c"
* @exception InvalidParameterException Wenn a oder b oder c < 0
*/
public double computeCuboidVolume(double a, double b, double c) throws InvalidParameterException
Hier sind die obigen Punkte behoben, die Doku läßt hoffentlich keine Fragen offen.
Beispiel 3: org.apache.commons.lang.exception.ExceptionUtils
aus dem Apache-Commons-Paket: http://commons.apache.org/lang/api-release/org/apache/commons/lang/exception/ExceptionUtils.html#getMessage(java.lang.Throwable)
Der Kommentar ist nicht allzu ausführlich, aber ich denke man hat nach einem Blick ein brauchbare Vorstellung vom Verhalten bekommen.
Auch hier:
- Besonderheiten bei Parameter (Verhalten bei Übergabe von "Null") sind beschrieben.
- Aussehen der Rückgabe ist beschrieben.
- Exceptions gibt es diesmal nicht.
Beispiel 4: Besonderheiten bei Verwendung:
Dieser Ausschnitt ist aus dem KuchenZutatNM-Beispiel. Ich gebe zu, dass er in dieser Form dort nicht so zu finden ist ;-).
/**Abrufen der Zutatenliste.
*
* Beim Entfernen einer Zutat aus dem Kuchen werden die verbundenen Zutaten NICHT gelöscht !
* Beim Laden des Kuchen werden sie NICHT mitgeladen. Ein Verwender kann die Zutatenliste eines aus der DB geladenen Kuchen also
* nur abrufen, solange die Bean unter Entity-Manager-Kontrolle ist.
*
* @return Liste der Zutaten.
*/
@ManyToMany(mappedBy="kuchen", cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, fetch=FetchType.LAZY)
public Collection<ZutatNMBean> getZutaten()
{
return this.collZutaten;
}
Da die Annotations nicht im JavaDoc erkennbar sind, muss hier einiges zur Verwendung erklärt werden, nämlich der Umgang mit den Zutaten beim
Löschen des Kuchens, sowie die Auswirkungen des FetchType.LAZY
.
Beispiel 5: Verweise:
Jetzt wollen wir Beispiel 4 noch verbessern: um auch im JavaDoc-HTML die beiden Seiten der Relationship zu "verbinden", können
wir unter Verwendung des @see
-Tags Verweise zu anderen Methoden oder Klassen erzeugen.
Das Javadoc-Tool erzeugt dabei HTML-Links, durch die man mit einem Klick zur anderen Methode gelangt.
/**Abrufen der Zutatenliste.
*
* Beim Entfernen einer Zutat aus dem Kuchen werden die verbundenen Zutaten NICHT gelöscht !
* Beim Laden des Kuchen werden sie NICHT mitgeladen. Ein Verwender kann die Zutatenliste eines aus der DB geladenen Kuchen also
* nur abrufen, solange die Bean unter Entity-Manager-Kontrolle ist.
*
* @return Liste der Zutaten.
* @see ZutatNMBean#getKuchen()
*/
@ManyToMany(mappedBy="kuchen", cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, fetch=FetchType.LAZY)
public Collection<ZutatNMBean> getZutaten()
{
return this.collZutaten;
}
Die Syntax ist: "Package.Klasse#Methode". Die Angabe des Packages ist optional, wenn wir das Ziel des Verweises innerhalb des gleichen Packages wie
die aktuelle Klasse liegt.
Gegenbeispiel 6: viele offene Fragen
Dieses Beispiel ist aus einer .NET-Library, mit der ich in der Firma arbeiten muss, und die wir angekauft haben. Die Doku einer Enumeration
für Excel-Linienstile sieht so aus:
Die Kommentare zu den einzelnen Werten sind total für den Eimer, interessanter wäre z.B. gewesen wieviele Pixel eine "Medium"-Linie breit ist.
Trivial-Operationen:
Man mag argumentieren, dass Kommentare für Trivial-Operationen wie Property-Zugriffe unnötige Tipparbeit sind.
Meiner Meinung nach bedeutet ein nicht vorhandener Kommentar allerdings nur, dass man absolut keine Aussage über die Methode
machen kann.
Dass die Methode "KuchenBean.getName()" nur die Property "name" zurückliefert, ist für den Programmierer, der nur die JavaDoc
zur Hand hat, nicht zu erkennen. Für ihn ist die Methode eine Blackbox, und hier kann alles passieren. Wer garantiert, dass "getName" nicht einen
aufwändigen Datenbankzugriff ausführt, um den Namen des aktuellen Kuchens aus einer völlig anderen Datenbank-Tabelle zu holen ?
Der Programmierer weiß allerdings, dass ihm kein Unheil droht, wenn im Kommentar etwas harmloses wie "Liefert den Namen des Kuchens" steht, und nichts davon steht
dass hier schlimmer Voodoo vonstatten geht.
Ein weiteres Beispiel: "ZutatBean.getMenge/setMenge": Was hat man sich unter "Menge" vorzustellen ? Welchen Regeln genügt dies ? Dass hier ein
Freitext ohne weitere Regeln dahintersteckt, und Eingaben wie "100g Mehl", "5 Liter Milch" etc. erlaubt sind, sieht man von außen nicht.
Eigentlich hilft auch ein Blick in den Quellcode nicht weiter, denn dies ist eine Information, die man nur aus der Anforderungsdefinition erhält,
und die existiert nur in meinem Kopf.
Doppel-Kommentare in Interface und Klasse:
In der EJB-Welt sind Local-/Remote-Interface so dicht verdrahtet, dass die Kommentare für Interface und Bean-Klasse eigentlich absolut identisch sein können.
Rein theoretisch würde es also genügen, z.B. das Interface zu dokumentieren, und in der implementierenden Methode der Bean-Klasse nur mittels @see
-Tag
auf die Interface-Methode zu verweisen. Allerdings macht es grundsätzlich Sinn, bei der Klassen-Methode einige Details über die Implementierung zu beschreiben,
falls diese Relevanz für den Verwender haben.
Beispielsweise genügt für eine allgemeine Interface-Methode "getDaten" die Beschreibung "Liest die Daten ein, wobei dies abhängig von
der Implementierung ist". Die implementierenden Klassen könnten jetzt aber Beschreibungen wie "Liest die Daten aus der Datenbank" oder "Greift auf eine XML-Datei
zu um die Daten einzulesen. Diese muss folgendes Format haben: ...".
Für den Fall, dass Kommentare in Klassenmethode und Interfacemethode absolut identisch sind, kann man folgenden Kommentar deklarieren:
/**{@inheritDoc}
*/
public KuchenNMBean findKuchenById(Integer int_Id)
{
Das Tag {@inheritDoc}
sucht den Methodenkommentar in Basisklassen oder Interfaces und übernimmt ihn von dort. Allerdings muss man die Kommentare
trotzdem in Remote- und Local-Interface duplizieren.
Zusammenfassung:
Ein Methodenkommentar sollte:
- Einen Überblick über die Funktion geben.
- Besonderheiten bei der Verwendung erklären.
- Erlaubte und unzulässige Werte für Parameter erklären
- Die Rückgabewerte erklären
- Exceptions und deren Ursachen erklären
- Bussiness-Logik erklären (vor allem, falls die nicht mal aus dem Quellcode hervorgeht)
- Und vor allem: dem Verwender das Gefühl geben, dass nichts innerhalb der Methoden passiert, was nicht in den Kommentaren steckt.
JavaDoc erzeugen
Das Erzeugen von JavaDoc geschieht leider in Eclipse nicht manuell. Man wählt das Projekt aus, für das man JavaDocs erzeugen will, und ruft
im Menü "File" -< "Export..." auf. Im Export-Dialog wählt man "Java" -< "Javadoc" aus:
Im nächsten Schritt wählt man die Projekt aus, für die man generieren will. Ich empfehle hier, alle Teilprojekte der Enterprise-Application zu wählen,
im Beispiel also EJB-, Web- und ApplicationClient-Projekt. Außerdem wird die Option "Create Javadoc for members with Visibility" auf "Protected" gesetzt.
Als Zielpfad empfehle ich ein Unterverzeichnis außerhalb des Projekts. Grund: ansonsten explodiert unsere Fehlerliste, weil das generierte
HTML ebenfalls validiert wird.
In den restlichen Schritten können wir alles auf den Defaults lassen.
Falls wir Package-Kommentare erzeugen wollen, geben wir den Pfad zur HTML-DAtei in Schritt 4 an:
Die Ausgabe des Javadoc-Laufs erscheint auf einer Konsole, und hier sollten wir ein Auge auf Fehler oder Warnungen haben.
Kommentare innerhalb Methoden
Diese Kommentare spielen für JavaDoc bzw. für Verwender der Library natürlich keine Rolle. Sie sind aber wichtig, wenn ein neuer Entwickler
ins Projekt kommt, oder wenn man selbst Workarounds einbaut und sich später noch an diese erinnern will.
Ich würde empfehlen, logisch zusammenhängende Codeblöcke per Kommentar zusammenzufassen. Also nicht vor jeder Codezeile ein Kommentar,
sondern immer vor Teilstücken.
Beispiel 1:
Dieses Beispiel stammt aus der "KuchenZutatNMWorkerBean" des KuchenZutatNM-Beispiel:
/**Die übergebene Zutat löschen.
*
* @param intZutatId ID der zu löschenden Zutat.
* @exception EntityNotFoundException Wenn Zutat nicht gefunden wurde.
*/
public void deleteZutat (Integer intZutatId)
{
logger.info ("deleteZutat " + intZutatId);
//Die Zutat im EntityManager laden.
//Hier mit "getReference" arbeiten, damit eine böse EntityNotFoundException fliegt wenn Zutat nicht gefunden wird.
ZutatNMBean zutat = this.entityManager.getReference(ZutatNMBean.class, intZutatId );
//Jetzt wird es knifflig: wir müssen die Kuchen der Zutat holen,
//und aus den Zutat-Collections der Kuchen die Zutat entfernen.
//Grund scheint zu sein dass der EntityManager ansonsten noch die
//Zutat im Kuchen hält und dabei in einen Fehlerzustand läuft.
Collection<KuchenNMBean> listeKuchen = zutat.getKuchen();
Iterator<KuchenNMBean> iteratorKuchen = listeKuchen.iterator();
while (iteratorKuchen.hasNext() == true)
{
//Zutat aus der Zutatenliste des Kuchens entfernen:
KuchenNMBean kuchenMitZutat = iteratorKuchen.next();
kuchenMitZutat.getZutaten().remove(zutat);
}
//Jetzt endlich dürfen wir die Zutat löschen.
this.entityManager.remove(zutat);
}
Hier sind einzelne Abschnitte kommentiert.
Es gibt zwei wichtige Fragen:
- Warum wird "entityManager.getReference" verwendet statt "find" ?
Erklärung: um bei ungültigen IDs eine Exception
- b) Was macht die Schleife ? Der Kommentar beschreibt zusammenfassend,
was die Schleife überhaupt macht. Er ist hier sehr detailliert und begründet, warum die folgende Aktion notwendig ist.
In diesem Detailgrad sind Kommentare nicht mehr nötig, wenn man genügend Erfahrung im Umgang mit Relationships gesammelt hat
und auf den ersten Blick sieht was zu tun ist.
Aber gerade für einen EJB3-Neuling ist der Sinn dieser Schleife ohne detaillierten Kommentar nicht erkennbar.
Beispiel 2:
Dieses Beispiel stammt aus der "KuchenZutatNMWorkerBean" des KuchenZutatNM-Beispiel:
/**Finden des Kuchens zur übergebenen ID.
*
* @param int_Id ID des gesuchten Kuchens
* @return Gefundener Kuchen oder null, wenn nicht gefunden (es gibt in diesem Fall keine Exception)!
*/
public KuchenNMBean findKuchenById(Integer int_Id)
{
logger.info ("findKuchenById " + int_Id);
//Den Kuchen im EntityManager laden.
//Hier mit "find" arbeiten, da bei ungültiger ID "null" zurückkommen soll.
KuchenNMBean kuchen = this.entityManager.find(KuchenNMBean.class, int_Id);
//Falls etwas gefunden wurde, dann die Relationship einlesen.
if (kuchen != null)
{
//Da der FetchType der ManyToMany-Relation auf LAZY gesetzt ist müssen die Zutaten
//hier explizit abgerufen werden solange die KuchenNMBean noch nicht
//detached ist. Später würde ansonsten ein Zugriff auf die Zutatenliste eine
//Exception auslösen.
Collection<ZutatNMBean> collZutaten = kuchen.getZutaten();
//Wir müssen einen Zugriff auf die Collection selbst ausführen, Abrufen der Property alleine reicht nicht !
logger.info ("Anzahl Zutaten: " + collZutaten.size());
}
else
{
logger.info ("Kuchen zur ID " + int_Id + " nicht gefunden !");
}
return kuchen;
}
Der Methoden-Kommentar macht präzise Aussagen, was bei einer ungültigen ID passiert.
Innerhalb der Methode ist der relevante Kommentar der Workaround für Probleme beim FetchType.LAZY
: Schaut ein Entwickler später in den Code, dann wird er wahrscheinlich
keinen Sinn mehr hinter dem doch scheinbar unnötigen collZutaten.size()
erkennen. Deshalb muss hier ein Kommentar hin !
Gegenbeispiel 3
Folgenden Kommentar eines Kollegen habe ich heute (26.11.2007) in einem Codeschnipsel gefunden (die von ihm geänderte Zeile führte nebenbei zu einem Crash
an anderer Stelle, es handelt sich übrigens um eine C#-Property):
public override DateTime Von
{
get
{
...
}
set
{
//Anpassen auf den NÄCHSTEN Montag:
DateTime datMontag = DatumZeitFunktionen.GetMontag(value);
// Korrektur JK 18.01.2007
if (datMontag < value) datMontag = datMontag.AddDays(7);
base.Von = datMontag;
}
Wir haben danach eine Viertelstunde lang gerätselt, was die eingefügte Zeile zu bedeuten habe, und mussten am Ende sogar beim Kunden anrufen,
um uns bestätigen zu lassen, was wir entschlüsselt hatten ;-). Ich muss meinem Kollegen allerdings zugute halten, dass er immerhin ein
"Schuldeingeständnis" in den Code gesetzt hat (in Form eines Änderungs-Kommentars). Ansonsten hätte ich die mir unsinnig erscheinende Zeile
wohl einfach auskommentiert (versehen mit einem "//Erscheint mir sinnlos und führt zu Bug in Situation xyz (WKnauf 26.11.2007)"), und
dadurch wäre die Situation wohl nicht besser geworden ;-).
Zusammenfassung
- Kommentare sollten immer Code-Blöcke zusammenfassen und damit die Lesbarkeit des Codes erhöhen.
- Sie sollen Besonderheiten und Workarounds erklären, und zwar so, dass man auch nach 5 Jahren noch weiß was man sich damals
gedacht hat !
- Wenn man in bestehendem Code etwas ändert, dann macht es Sinn, einen Bekennerbrief und eine Begründung zu hinterlassen ;-).
Kommentare der Form "Schleife über xyz" oder "IF-Abfrage", nur um meinen Kommentar-Hunger zu stillen, sind natürlich total unnötig.
Wenn es wirklich nichts zu sagen gibt, dann ist auch kein Kommentar nötig. Aber sobald eine IF-Abfrage mehrere mit UND oder ODER verknüpfte
Bestandteile hat, muss man die Bedingung einfach in Prosa-Text übersetzen, sonst kapiert man sie selbst nicht.
Meine Trivial-Beispiele enthalten leider nicht allzuviele Stellen, an denen es wirklich knackig wird. Aber die Realität besteht oft
aus fünfzeiligen IF-Bedingungen mit vielen Klammern ;-).
Stand 01.12.2007
Historie:
19.11.2007: Seite erstellt.
26.11.2007: Überarbeitung, Javadoc-Generierung und Package-Kommentare
01.12.2007: Kommentare aus KuchenZutatNM-Beispiel korrigiert, Tag {@inheritDoc}
zugefügt.