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

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 ?


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 ?


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:


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:


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:

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

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:
Export Javadoc
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.
Export Javadoc
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:
Export Javadoc (Package-Kommentar)

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:

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