QVINTVS · SCRIBET

Ein Wort über gute Dokumentation

Es ist ja nachzuvollziehen, dass das Schreiben von Dokumentation lästig ist. Man möchte sich viel lieber darum kümmern, seine grandiosen Ideen in Code umzusetzen. Ich möchte in diesem Artikel meinen eigenen Ansatz zum Schreiben von Dokumentation vorstellen, der vielleicht dabei hilft, neben gutem Code auch gute Docs zu erstellen.

Systematisch

Eine der großen Hürden für Dokumentation in Ruby sind, so denke ich, die mangelnden Dokumentationsrichtlinien. Es gibt de facto keine Anleitungen zum Thema wie schreibe ich gute Docs in Ruby? und ich denke, dass dies einer der Hauptgründe war, warum YARD enstanden ist. Rubys Standarddokumentationstool RDoc reicht jedoch zur Dokumentation völlig aus und ich persönlich ärgere mich jedes mal über YARD-Dokumentation, weil sie sich im Quelltext einfach nicht so schön liest wie RDoc und mich außerdem zwingt, neben dem RDoc-Markup auch noch das YARD-Markup zu lernen, wenn ich nicht die Dokumentation als HTML generieren will.

Doch genug davon. Das Fehlen dieser Richtlinien jedenfalls dürfte dafür verantwortlich sein, dass so viele Ruby-Projekte keine oder oder nur schlechte Dokumentation besitzen. Nicht, dass die Entwickler sich ohne Richtlinien nicht zutrauen würden, Dokumentation zu schreiben. Es ist vielmehr so, dass Entwickler ohne gegebene best practice einfach zu faul sind und so einfach nur uninformative Dokumentation entsteht. YARD erzwingt diese best practice beim Dokumentieren, RDoc hingegen nicht (YARD forciert mit @-Direktiven die Strukturierung der Doku, RDoc erlaubt freie Form), und so kommt es, dass YARD-Dokus so oft besser aussehen als RDoc-Dokus. Mal ganz abgesehen davon, dass ich RDocs Standardausgabetemplate (Darkfish) bemerkenswert hässlich finde, aber etwa ruby-doc.org zeigt ja, dass zumindest das kein Problem darstellt.

Also: Legt euch ein System für eure Dokumentation zurecht! Es muss nicht das im folgenden Vorgestellte sein, aber nehmt euch einmal eine halbe Stunde Zeit und überlegt euch, welche Informationen eine Methoden- oder Klassendokumentation enthalten muss, um einen der Materie völlig fremden Nutzer komplett zu informieren. Überlegt euch, wie einer denkt, der keine Ahnung von eurem Code hat und der einfach nur eure Library benutzen will. Pappt euch ein Schema neben den Monitor und haltet euch beim Schreiben einer Methode daran.

Chronologisch

Bei der zeitlichen Erstellung von Dokumentation gibt es zwei Möglichkeiten: Man kann sie vor oder nach dem Code schreiben. Es gibt Leute, die sind der festen Überzeugung, dass man erstmal seine gesamte Dokumentation schreiben sollte und erst dann den Code, aber wenn man während der Entwicklungsphase plötzlich was am API ändern muss, wird die Dokumentation hinfällig und man muss sie neu schreiben, sodass man die Dokumentation dann effektiv zweimal schreibt. Um das zu vermeiden, wähle ich den anderen Ansatz und schreibe erst den Code, und dann die Dokumentation, jedoch nicht im ganzheitlichen Sinne, dass ich erst eine Library vollständig fertigstelle und danach die Dokumentation schreibe, sondern eher in einem progressiven Verfahren. Konkret bedeutet das: Wann immer ich eine (öffentliche, zum API gehörende) Methode fertiggestellt habe, schreibe ich ihre Dokumentation. Oder zumindest versuche ich es, nicht immer gelingt mir diese strikte Abfolge.

Technisch

Dokumentation sollte Grundkenntnisse des Nutzers in der jeweiligen Programmiersprache, das ist in unserem Falle Ruby, voraussetzen. Sprachtutorials haben in einer Dokumentation nichts verloren und machen nur unnötig Arbeit. Stattdessen sollte Dokumentation auf vorhandene Kenntnisse der Nutzer aufsetzen und sich entsprechenden englischen Fachvokabulars bedienen und nicht dem Nutzer beispielhaft erklären, was denn nun Klassen und Instanzen sind. Dafür gibt es bereits genug (Online-)Tutorials.

Ebenfalls technischer Aspekt der Dokumentation ist Präzision. Ich will keine Romane lesen, bevor ich weiß, wie ich eine Programmbibliothek zu bedienen habe. Sicherlich ist es interessant zu erfahren, wieso ein Methode so zu bedienen ist wie es ist, doch gehören solche Aspekte in meinen Augen eher auf die Website eines Projektes denn in ihre Dokumentation. Wenn sie unbedingt in die Dokumentation müssen, dann bitte in einer extra Datei »history.rdoc« oder so ähnlich.

Es gilt demnach: »In der Kürze liegt die Würze«. Jedoch sollten die Beschreibungen nicht derart kurz ausfallen, dass man dem roten Faden einer Erklärung nicht folgen kann. Generell gilt: Klassen- und Moduldokumentation können gern etwas länger sein, sie sollten dann aber auch alle allgemein die Klasse/das Modul betreffenden Aspekte abdecken, sodass keine Notwendigkeit besteht, in jeder Methode eine oder gar mehrere Informationen zu wiederholen. Allenfalls ein »See the class’ introductory text« kann angebracht sein.

Linguistisch

Die Hauptdokumentation eines Projekts sollte auf Englisch abgefasst sein. Es spricht jedoch nichts dagegen, Übersetzungen der Dokumentation zu erstellen, die jedoch immer darauf hinweisen sollten, dass die primäre Informationsquelle die englischsprachige Dokumentation ist. Hierbei gilt: Wird eine Dokumentation übersetzt, dann idiomatisch. Es gibt tatsächlich im Englischen Redewendungen, die es im Deutschen nicht gibt1, auch wenn viele Leute das scheinbar nicht begreifen wollen. Ein Ausnahme hiervon stellen Fachbegriffe da: Da in der IT-Branche der größte Teil des Vokabulars nun einmal aus der englischen Sprache stammt, sollten stehende Begriffe beibehalten werden. Mir drehen sich jedesmal die Fußnägel hoch, wenn die Sprache von Feldern ist. Wer kommt schon darauf, dass es sich hierbei um Arrays handeln soll?

Inhaltlich

So, jetzt aber zum interessanten Punkt. Wie gestalte ich meine Dokumentation?

Struktur

Generell folgt eine von mir geschriebene Klasse stets dieser Struktur:

class Classname
  # Extends
  # Includes

  # Konstanten

  # Attribute

  # Klassenmethoden

  # initialize

  # Instanzmethoden
end

Module werden enstprechend mit module anstatt mit class als Schlüsselwort eröffnet.

Klassen und Module

Nun, Klassen- und Moduldokumentationen sind Freitexte, die jedoch den Nutzen und die generelle Idee der jeweiligen Klasse erläutern und anhand von Beispielen verdeutlichen sollten. Auch ein Wort über die generelle Idee der Klasse/des Moduls sollte genausowenig fehlen wie eine Beschreibung des Zusammenspiels mit den anderen Klassen der Programmbibliothek oder sogar, falls sinnvoll und relevant, ihr Zusammenspiel mit Core- und Stdlib-Klassen.

Methoden

Methoden werden anhand ihres Zwecks dokumentiert. Nach einer kurzen Beschreibung der Aufgabe der Methode werden ihre Parameter gelistet, ihr Rückgabewert angegeben und eventuell vom Standard abweichende Exceptions benannt (so ist es etwa unnötig, ArgumentError für falsche Argumentanzahl zu dokumentieren, weil das ohnehin normal ist). Anschließend sollte ein kurzes Anwendungsbeispiel folgen, sofern der Methodengebrauch nicht eindeutig ist (so braucht die Methode #<=> als Basismethode für Comparable wohl kein Beispiel). In summa ergibt sich das folgende Gerüst für eine Methode:

Ein paar allgemeine Sätze.
#== Parameters
#[bar]    Beschreibung für Parameter 1.
#[foobar] (7) Beschreibung für Parameter 2.
#== Raises
#[FooBarBazException] Beschreibung, wann diese Exception geworfen
#                     wird.
#== Return value
#Beschreibung des Rückgabewerts in allen Fällen, wenn nötig,
#abhängig von Parametern.
#== Examples
#  foo(1, 2)
#  foo("a", "b")
def foo(bar, foobar = 7)
  # Code...
end

Jeder der Abschnitte außer dem allgemeinen Abschnitt kann weggelassen werden, wenn er für die Funktionsweise der Methode unwichtig ist (beim Examples-Abschnitt sollte man sich das jedoch genau überlegen).

Allgemein benutze ich zur Dokumentation nur das Gatterzeichen # direkt gefolgt vom Text, weil es für lange Texte weniger Tipparbeit darstellt. Es ist jedoch ebenenso möglich, jede Zeile mit “# “, also Gatterzeichen gefolgt von einem Abstand, zu beginnen, RDoc versteht üblicherweise beides. Von der Verwendung mehrzeiliger Kommentare mittels =begin rdoc und =end rate ich ab, weil sie sich nicht vernünftig einrücken lassen.

Die einzelnen Abschnitte der Methodendokumentation werden durch Level-2-Überschriften voneinander getrennt und Volltexte sind nur für die Allgemein- und Rückgabewertbeschreibung erforderlich. Parameter und Exceptions lassen sich wesentlich übersichtlicher in Form von RDocs Definitionslisten dokumentieren — hierbei sollte man auf eine korrekte Einrückung der einzelnen Beschreibungstexte achten, andernfalls ist die Lesbarkeit im Quelltext nicht mehr gewährleistet und man ist wieder gezwungen, die HTML-Dokumentation zu erzeugen, was ja gerade vermieden werden soll. Bei langen Parameternamen ist jedoch die zweite Form von RDocs Definitionslisten vorzuziehen, da bei einer empfohlenen Zeilenlänge von maximal 80 Zeichen (länger wird für das Auge anstrengend zu lesen, da man in den Zeilen verrutscht) sonst zu wenig Platz für den Textkörper hat:

#== Parameters
#[thisisaquitelongparametername]
#  Beschreibung dieses Parameters, auch gerne und problemlos
#  mehrzeilig.
#[thisisanotherlongparametername]
#  Beschreibung dieses Parameters

Die beiden Listentypen sollten nicht innerhalb der Dokumentation einer einzelnen Methode vermischt werden.

Hinsichtlich optionalen Parametern mit Standardwerten sollte man den Wert in Klammern zu Beginn der Beschreibung des jeweiligen Parameters angeben. Typschrift für den Standardwert ist nur erforderlich, wenn Missverständnisse entstehen könnten oder ein längerer Ausdruck folgt.

#== Parameters
#[foobar] (3) Beschreibung für den Parameter.
#[barbaz] (<tt>sprintf("%30s", MyModule.foobar)</tt>) Beschreibung
#         für den Parameter[/code]

:yields: und :call-seq:

Gern vernachlässigt werden auch Blöcke—dabei gehört es zu den essentiellen Nutzungsinformationen, welche Parameter ein Block erhält. Ganz zu schweigen davon, dass man überhaupt dokumentieren muss, dass eine Methode einen Block erhalten kann, was ja auch der Fall sein kann, wenn der Methodenkörper kein yield enthält und RDoc die Methode nicht automatisch als Blockmethode kennzeichnet. In solchen Fällen ist die RDoc-Direktive :yields: oder gar ein komplettes :call-seq: anzubringen. Ist ein Block gar optional, ist :call-seq: nicht mehr zu vermeiden, weil es mehr als eine Aufrufmöglichkeit gibt.

#call-seq:
#  foo(par){|blockpar|...} → an_object
#  foo(par)                → nil
#
#==Parameters
#[par]      ...
#[blockpar] (Blockargument) Beschreibung des Blockparameters.
#.
#.
#.
def foo(par)
  yield(rand(10)) if block_given?
end

Im Bezug auf die Syntax von :call-seq: sei jedem der Blick in RDocs Markup-Referenz empfohlen. Dort sind auch all die anderen Direktiven aufgelistet, die Dokumentation viel verständlicher machen. Darum: Benutzt sie!

:call-seq: erlaubt es, die Aufrufszeile der HTML-Dokumentation beliebig zu verändern. Um das Gesamtbild der Dokumentation jedoch nicht zu beschädigen, sollte man sich weitestgehend an RDocs Standardaussehen halten, Beispielaufrufe etwa gehören hier nicht hinein (dafür gibt es den Examples-Abschnitt). Im Laufe meiner Programmierung sind mir viele Arten von call-seqs begegnet, aber die folgend beschriebene erscheint mir als die gängiste und auch übersichtlichste:

  1. Immer Klammern angeben. Auch dann, wenn die Methode keine Parameter entgegennimmt. Grund: RDoc benutzt die Klammern als Begrenzer des Methodennamens, d.h. alles vom Anfang der ersten Zeile bis zur öffnenden Klammer wird in die Methodenübersicht aufgenommen!
  2. Optionale Parameter in eckigen Klammern einschließen, Standardwerte dabei mit Gleichheitszeichen angeben (außer, die call-seq würde dadurch übermäßig lang): foo(x [, y = 7 [, z = 9 ] ]). Das Komma wird ebenfalls in die eckigen Klammern eingeschlossen, da es ja ebenfalls optional ist. Eckige Klammern dürfen daher beliebig tief geschachtelt werden.
  3. Restargumente als solche kennzeichnen, falls notwendig, ebenfalls in eckige Klammern einschließen. foo(x, *args); foo(x [, *args]).
  4. Blockargumente als Block dokumentieren, außer es gäbe einen definitiven Grund dafür, sie als &block zu dokumentieren. foo(){...}, selten foo(&block).
  5. Ein als Liste benannter Parameter getarntes Hash als hsh oder opts dokumentieren. Anonsten gelten die Bestimmungen zu eckigen Klammern und Restargumenten. Jedoch sollten die möglichen Parameter in einer Unterliste der Beschreibung des Hashparameters benannt werden.
  6. Den Rückgabewert mit einem Unicode-Pfeil → oder einem ASCII-Pfeil ==> angeben (außer der Rückgabewert der Methode ist unwichtig). Dies hat keine Bedeutung für RDoc, erscheint mir aber sinnvoll.
  7. Jede Aufrufmöglichkeit eine Zeile. Nicht mehr, und nicht weniger. Methoden mit mehr als vier Parametern gehören ohnehin verbannt.

Konstanten

Konstanten werden kurz und prägnant in einem einzelnen Satz dokumentiert. Der Wert der Konstanten hat nichts in der Dokumentation verloren, er kann vom RDoc-Template angezeigt werden (Hanna etwa tut dies).

#My cool constant.
COOL = 4

Attribute

Attribute werden im Quelltext getrennt voneinander mithilfe einzelner attr-Anweisungen definiert und dort einzeln dokumentiert.

#Attributbeschreibung hier.
attr_reader :foo

#Noch eine Attributbeschreibung.
attr_accessor :bar

Ob Readers, Writers oder Accessors zuerst und in welcher Reihenfolge kommen, ist ziemlich egal, nur sollten sie gruppiert werden, also nicht abwechselnd Readers und Accessors oder noch seltsamere Kombinationen. Attributbeschreibungen sollten genau wie Konstantenbeschreibungen kurz gehalten werden.

Pseudo-Attribute und -methoden

Rubys dynamische Fertigkeiten erlauben die Definition von Methoden on-the-fly. Da in Ruby auch Attribute nur Methoden sind, behandle ich beide in einem Abschnitt, es gelten ähnliche Prinzipien.

Fall 1: Einfaches define_method

Damit kommt RDoc inzwischen selbst ganz gut klar, man muss es nur anweisen, das Konstrukt zu dokumentieren:

# Methodendokumentation
define_method(:foo) do
  # ...
end

Man beachte das doppelte Gatter am Beginn der Dokumentation. Dieses weist RDoc an, den nächsten erkannten Codeblock zu dokumentieren. Und obwohl die Zuordnung des Quelltexts problemlos läuft, sollte man eine solche Dokumentation immer mit einem :call-seq: einleiten, da das mit dem Parametern nicht immer ganz so gut läuft.

Fall 2: Mehrfaches define_method

Hier versagen RDocs Fähigkeiten auf eine ganz üble Art und Weise. Im folgenden Code wird nur eine einzige der vielen definierten Methoden dokumentiert und auch nur einen einzigen Dokumentationsblock von allen dreien erhalten:

# Methode 1

##
# Methode 2

##
# Methode 3

3.times do |i|
  define_method(:"foo#{i}") do
    #...
  end
end

Diesem Problem lässt sich nur durch zwei kleine Tricks Abhilfe schaffen. Der erste ist, die logische Verknüpfung zwischen dem Dokumentationsblock und define_method aufzuheben, indem man mindestens eine andere reguläre (und dokumentierte!) Methode dazwischenschiebt. Ich löse das in der Regel so, dass ich meine Pseudo-Methoden nach den Attributen am Klassenanfang dokumentiere.

Der zweite Trick ist die Verwendung der RDoc-Direktive :method: (oder :singleton-method: für Klassenmethoden, :attr_reader:, :attr_writer: und :attr_accessor: für Attribute), die RDoc anweist, den folgenden Dokumentationsblock als Methodenbeschreibung aufzufassen. Im Gegensatz zu den »handelsüblichen« Direktiven sind diese nicht zusammen mit den üblichen Formatieranweisungen dokumentiert, sondern sind Teil der Dokumentation des Ruby-Parsers. Etwas versteckt, nichtsdestotrotz überaus nützlich.

Die Direktiven erwarten als Parameter einen Methoden/Attributsnamen, welchen sie dann in die HTML-Dokumentation einpflegen, als wäre es eine reguläre Methode/ein reguläres Attribut.

# :method: foo0
#First foo.

##
# :method: foo1
#Second foo.

##
# :method: foo2
#Third foo.

#X method.
def x
  #...
end

3.times do |i|
  define_method(:"foo#{i}") do
    # ...
  end
end

In manchen Fällen, insbesondere in denen, in denen man eine Attributmethode wie z.B. foo= händisch definiert, ist eine Kombination mit der :nodoc:-Direktive sinnvoll.

# :attr_accessor: foo
#Foo foo foo.

#See attribute docs.
def foo # :nodoc:
  @foo
end

#See attribute docs.
def foo=(val) # :nodoc:
  raise(ArgumentError, "val < 0!") if val < 0
  @foo = val
end

Damit sind denke ich alle wichtigen Themen abgehakt. Also, liebe Leute: Schreibt brauchbare Dokumentationen!

Valete.

1 »Ich hab’ gebellt und gebellt, aber niemand machte auf! Und das, obwohl es Katzen und Hunde regnete!«