Quellcodedokumentation

Quellcode kann auf verschiedenen Ebenen dokumentiert werden, um dessen Lesbarkeit und Wiederverwendbarkeit zu erhöhen. Hierzu betrachten wir zwei Mechanismen:

  1. Inline-Kommentare: Kommenatare im Quellcode, um dessen Funktionsweise zu erklären, als Antwort auf die Frage: «Wie funktioniert das?»
  2. Dokumentationskommentare Kommentare am Quellcode, um dessen Verwendung zu erklären, als Antwort auf die Frage: «Wie kann ich das verwenden?»

Inline-Kommentare

In JavaScript/TypeScript gibt es zwei Arten von Inline-Kommentaren:

  1. Zeilenkommentare, die mit // anfangen und bis zum Ende der Zeile gehen.
  2. Blockkommentare, die mit /* anfangen und mit */ auf der gleichen oder auf einer andere Zeile enden.

Hierzu ein Beispiel:

function fizzBuzz(x: number): string {
  /* note the order of the rules:
     - the rarest case is checked first
     - the more common the case, the farther on the top it is checked
     - the most common case is the at the end */
  if (x % 15 == 0 /* i.e. divisible by 5 and 3 */) {
    return "FizzBuzz"; // no space in between words
  } else if (x % 5 == 0) {
    return "Buzz";
  } else if (x % 3 == 0) {
    return "Fizz";
  } else {
    // format the number as a string without floating point part
    return x.toFixed(0);
  }
}
Info

Die Platzierung der Kommentarbegrenzungen /* und */ auf der gleichen bzw. auf separaten Zeilen ist Gegenstand vieler sinnloser Diskussionen, an denen sich mit Vorliebe Entwickler ohne belastbare Evidenz beteiligen. Oft arten solche Diskussionen in Beschimpfungen und Handgreiflichkeiten aus – oder enden mit dem Kompromissvorschlag, dass diese Platzierung eben Geschmackssache sei. Dies ist paradox, zumal sich Entwickler mit gutem Geschmack nicht an solchen Diskussionen beteiligen.

Kommentare werden vom Compiler ignoriert und können nicht verschachtelt werden. Einige Texteditoren und Entwicklungsumgebungen heben einige häufig in Kommentaren verwendete Schlüsselwörter hervor:

  • TODO: Hier gibt es noch etwas fertigzustellen oder zu verbessern.
  • NOTE: Hier ist etwas besonderes zu beachten.
  • FIXME: Hier muss noch ein Problem (Fehler) gelöst werden.
  • HACK: Hier wurde aus einem besimmten Grund unsauber gearbeitet.

Beispiele:

// TODO: Make sure this also works for non-ASCII characters.
// NOTE: This inconvenient API is used for better performance.
// FIXME: Handle missing arguments gracefully instead of crashing.
// HACK: This only works for amounts up to roughly one million.

Diese Schlüsselwörter sind jedoch nicht standardisiert, sondern bloss eine Konvention. Bei deren Verwendung sollte man prüfen, ob man nicht besser direkt eine Verbesserung am Quellcode vornehmen soll, als dessen Unzulänglichkeit zu dokumentieren. Ist dies nicht möglich, sollten solche Kommentare durch ein Ticket (Fehlerbericht, Task, usw.) begleitet oder dadurch ersetzt werden.

Quellcode sollte so klar geschrieben sein, dass er selbsterklärend ist. Beim Schreiben von Quellcode darf man davon ausgehen, dass der Leser die Programmiersprache und die verwendeten Libraries und Frameworks kennt. Faustregel:

Kommentare sollen sagen, was der Code nicht sagen kann.

Oder:

Kommentiere nur, was du nicht codieren kannst.

Sehr guter Quellcode kann über weite Strecken ohne Kommentare auskommen. Im Umkehrschluss ist eine hohe Kommentardichte im Quellcode eher ein schlechtes Zeichen. Dies sind jedoch nur Heuristiken, d.h. Faustregeln.

Wenn man erklärende Kommentare schreibt, sollte man eher die Frage beantworten, warum man etwas auf eine bestimmte Weise tut, und nicht, was hier überhaupt passiert. Auch das ist nur eine Faustregel.

Dokumentationskommentare

Im Gegensatz zu erklärenden Inline-Kommentaren sollte man Dokumentationskommentare konsequent schreiben. Diese erklären, wie eine bestimmte Quellcode-Einheit (eine Konstante, eine Funktion, eine Klasse, eine Eigenschaft, eine Methode oder ein Modul) verwendet wird, und was dabei zu beachten ist.

Im Gegensatz zu Inline-Kommentaren werden Dokumentationskommentare auch ausserhalb vom Code gelesen. Hierzu verarbeitet man den kommentierten Quellcode mit speziellen Werkzeugen, um Dokumentation daraus zu erzeugen.

Da Dokumentationskommentare die Verwendung und nicht die interne Funktionsweise von Quellcode dokumentieren, kann man sich mit der Dokumentation von öffentlichen (public) und exportieren (export) Elementen begnügen. Einige Organisationen verlangen, dass sämtliche öffentlichen Bezeichner (Klassen, Eigenschaften, Methoden) dokumentiert sein müssen. Andere sind pragmatischer und verwenden Dokumentationskommentare nur an Orten, wo die Verwendung der Codeeinheit nicht selbsterklärend ist.

JSDoc

Verschiedene Programmiersprachen und -umgebungen stellen verschiedene Mechanismen und Werkzeuge für Dokumentationskommentare zur Verfügung. In JavaScript/TypeScript kommt JSDoc (siehe auch die für TypeScript spezifische Referenz) zum Einsatz.

Ein JSDoc-Kommentar beginnt mit der Zeichensequenz /** und endet mit */. Dazwischen stehen ein- oder mehrzeilige Kommentare.

Deno hat Unterstützung für JDSoc über den Befehl deno doc eingebaut, das u.a. Dokumentation als HTML ausgeben kann (deno doc --html).

JDSoc-Kommentare beinhalten neben erklärendem Text auch Annotationen bzw. Tags. Das sind spezielle Schlüsselwörter mit einer bestimmten Bedeutung. Im folgenden wird kurz auf einige ausgewählte Annotationen eingegangen. Eine vollständige Übersicht bietet die JSDoc Reference.

Weitere Annotationen/Tags werden im weiteren Modulverlauf bei Bedarf eingeführt.

Die Dokumentation einer TypeScript-Datei kann folgendermassen extrahiert und auf der Kommandozeile betrachtet werden:

deno doc code.ts

Parameter und Rückgaben: @param und @returns

Bei Funktionen und Methoden sollen die Funktionsparameter und Rückgabewerte dokumentiert werden. Hierzu das dokumentiere FizzBuzz-Beispiel von vorher (fizz-buzz.ts):

/**
 * Turns the given number into its proper representation for the FizzBuzz game
 * by applying the following rules:
 * 1. If the number is divisible by 3, it shall be represented as "Fizz".
 * 2. If the number is divisible by 5, it shall be represented as "Buzz".
 * 3. If the number is divisible by both 3 and 5, it shall be represented as "FizzBuzz".
 * 4. If the number is neither divisible by 3 nor 5, it shall be represented as it is.
 * @param {number} x - The number to be represtented.
 * @returns {string} The representation of the number as a string according to the rules above.
 */
function fizzBuzz(x: number): string {
  if (x % 15 == 0) {
    return "FizzBuzz";
  } else if (x % 5 == 0) {
    return "Buzz";
  } else if (x % 3 == 0) {
    return "Fizz";
  } else {
    return x.toFixed(0);
  }
}

export { fizzBuzz };

Die Dokumentation beginnt mit einer Beschreibung der Funktion und nach welchen Regeln sie operiert. Dabei steht das Verhältnis zwischen übergebenem Argument und Rückgabewert im Vordergrund.

Mit @param wird der Funktionsparameter x bezeichnet. Hier wird er zusätzlich innerhalb geschweifter Klammern mit dem Typ annotiert. Nach dem Namen des Parameters folgt (getrennt von einem Dash) dessen Erklärung.

Mit @returns wird der Rückgabetyp und dessen Bedeutung angegeben.

Info

Da die Parameter- und Rückgabetyp bereits im TypeScript-Code annotiert worden sind, kann man sich die Datentypen in den JSDoc-Annotationen auch sparen!

Die generierte Dokumentation sieht dann folgendermassen aus:

function fizzBuzz(x: number): string
  Turns the given number into its proper representation for the FizzBuzz game
  by applying the following rules:

  1. If the number is divisible by 3, it shall be represented as "Fizz".
  2. If the number is divisible by 5, it shall be represented as "Buzz".
  3. If the number is divisible by both 3 and 5, it shall be represented as "FizzBuzz".
  4. If the number is neither divisible by 3 nor 5, it shall be represented as it is.

  @param {number} x
      The number to be represtented.

  @return {string}
      The representation of the number as a string according to the rules above.

Möchte man die Dokumentation als HTML betrachten, kann man dies mit Deno folgendermassen bewerkstelligen:

deno doc --html fizz-buzz.ts

Dadurch wird ein Ordner docs/ erzeugt, die u.a. eine Datei index.html enthält, die nun im Browser geöffnet werden kann.

Info

Die Funktion fizzBuzz wurde exportiert, damit sie in der HTML-Dokumentation unter Symbols erscheint!

Missbilligter Code: @deprecated

Bestehender Code, der nicht mehr verwendet werden sollte, den man aus Gründen der Rückwärtskompatibilität aber noch nicht löschen kann, kann man mit @deprecated markieren. Damit kündigt man an, dass Verwendungen dieses Codes ersetzt werden sollten:

/** represents a person with first-, and optional middle- and a last name */
export class Person {
  constructor(
    private firstName: string,
    private lastName: string,
    private middleName?: string,
  ) {}

  /** @returns the full name of the person; with or without middleName */
  get fullName(): string {
    if (this.middleName) {
      return `${this.firstName} ${this.middleName} ${this.lastName}`;
    }
    return `${this.firstName} ${this.lastName}`;
  }

  /** @deprecated use the fullName accessor instead */
  buildName(): string {
    return `${this.firstName} ${this.middleName ?? ""} ${this.lastName}`;
  }
}

Die Implementierung buildName hat den Nachteil, dass sie bei fehlendem middleName einen Abstand von zwei Zeichen zwischen Vor- und Nachnamen anzeigt. Da sie öffentlich und Teil einer exportierten Klasse ist, wird sie möglicherweise an vielen Orten verwendet. Deshalb kann sie nicht ohne weiteres gelöscht werden.

Symbole, die als @deprecated markiert sind, werden im Code oft durchstrichten dargestellt (z.B. als buildName). Dadurch erhält man beim Bearbeiten von Client-Code einen Anreiz, den Aufruf zu ersetzen.

Markiert man ein Symbol als @deprecated, sollte man eine entsprechende Alternative bereitstellen und diese erwähnen. Hierzu kann man die Annotationen @see oder @link verwenden:

/** represents a person with first-, and optional middle- and a last name */
export class Person {
  // ...

  /** @deprecated use the @see fullName accessor instead */
  buildName(): string {
    return `${this.firstName} ${this.middleName ?? ""} ${this.lastName}`;
  }

  /** @deprecated use the {@link swissName} accessor instead */
  buildSwissName(): string {
    return `${this.firstName} ${this.middleName ?? ""} ${this.lastName}`;
  }
}

Urheber: @author

Möchte man sich als Urheber eines bestimmten Symbols auszeichnen, kann man hierzu die @author-Annotation verwenden:

/**
 * Rounds a value to the given granularity.
 * @author Patrick Bucher <patrick.bucher@sluz.ch>
 * @param value - the value to be rounded
 * @param granularity - the granularity to be used
 * @returns the rounded value
 */
export function roundTo(value: number, granularity: number): number {
  const factor = 1 / granularity;
  const scaled = value * factor;
  const rounded = Math.round(scaled);
  return rounded / factor;
}

Gibt man neben seinem Namen auch eine E-Mail-Adresse an, sollte diese in spitzen Klammern angegeben sein, da das @sluz im obigen Beispiel sonst als Annotation interpretiert wird.


Viele JSDoc-Annotationen wie @class, @type oder @private die in JavaScript sehr hilfreich sein können, sind in TypeScript nicht nötig, da diese Informationen bereits aus dem Code extrahiert werden können.