Читать книгу Prinzipien des Softwaredesigns - John Ousterhout - Страница 24

Tiefe Module

Оглавление

Die besten Module sind die, die leistungsfähige Funktionalität bieten, dabei aber trotzdem eine einfache Schnittstelle haben. Ich nutze den Begriff tief (deep), um solche Module zu beschreiben. Um sich die Tiefe zu verdeutlichen, stellen Sie sich vor, dass jedes Modul durch ein Rechteck dargestellt wird (siehe Abbildung 4-1). Die Fläche jedes Rechtecks ist proportional zur durch das Modul implementierten Funktionalität. Die obere Kante eines Rechtecks repräsentiert die Schnittstelle des Moduls – die Länge dieser Kante steht für deren Komplexität. Die besten Module sind tief: Sie besitzen viel Funktionalität, die hinter einer einfachen Schnittstelle verborgen ist. Ein tiefes Modul ist eine gute Abstraktion, weil nur ein kleiner Teil seiner internen Komplexität für seine Nutzerinnen und Nutzer sichtbar ist.

Abbildung 4-1: Tiefe und flache Module. Die besten Module sind tief: Sie erlauben, viel Funktionalität über eine einfache Schnittstelle anzusprechen. Ein flaches Modul ist eines mit einer recht komplexen Schnittstelle, aber nicht viel Funktionalität – so wird nicht viel Komplexität verborgen.

Die Modultiefe ist ein Ansatzpunkt, um über den Konflikt zwischen Kosten und Vorteilen nachzudenken. Die Vorteile eines Moduls bestehen in dessen Funktionalität, die Kosten eines Moduls (in Bezug auf die Systemkomplexität) in dessen Schnittstelle. Die Schnittstelle eines Moduls repräsentiert die Komplexität, die das Modul in den Rest des Systems trägt: Je kleiner und einfacher die Schnittstelle ist, desto weniger Komplexität bringt sie mit sich. Die besten Module sind solche mit den größten Vorteilen und den geringsten Kosten. Schnittstellen sind gut, aber mehr oder größere Schnittstellen sind nicht unbedingt besser!

Der Mechanismus für Datei-I/O von Unix und seinen Nachfolgern wie Linux ist ein wunderschönes Beispiel für eine tiefe Schnittstelle. Es gibt nur fünf grundlegende Systemaufrufe mit einfachen Signaturen:

int open(const char* path, int flags, mode_t permissions);

ssize_t read(int fd, void* buffer, size_t count);

ssize_t write(int fd, const void* buffer, size_t count);

off_t lseek(int fd, off_t offset, int referencePosition);

int close(int fd);

Der Systemaufruf open übernimmt einen hierarchischen Dateinamen wie /a/b/c und gibt einen Dateideskriptor als Integer-Wert zurück, der dann als Referenz für die geöffnete Datei dient. Die anderen Argumente beim Öffnen stellen zusätzliche Informationen bereit, wie zum Beispiel, ob die Datei zum Lesen oder Schreiben geöffnet wird, ob eine neue Datei erzeugt werden soll, wenn es sie noch nicht gibt, und wie die Zugriffsberechtigungen für die Datei aussehen, wenn eine neue Datei angelegt wird. Die Systemaufrufe read und write übertragen Informationen zwischen Pufferbereichen im Speicher der Anwendung und der Datei, close beendet den Zugriff auf die Datei. Auf die meisten Dateien wird sequenziell zugegriffen, daher ist das die Standardvorgehensweise – ein wahlfreier Zugriff wird über den Systemaufruf lseek erreicht, durch den man die aktuelle Zugriffsposition anpasst.

Für eine moderne Implementierung der I/O-Schnittstelle von Unix sind Hunderttausende Zeilen Code erforderlich, die komplexe Themen wie die folgenden angehen:

 Wie werden Dateien auf der Festplatte repräsentiert, um einen effizienten Zugriff zu ermöglichen?

 Wie werden Verzeichnisse abgelegt, und wie werden hierarchische Pfadnamen verarbeitet, um die Datei zu finden, die dadurch angesprochen wird?

 Wie werden die Berechtigungen sichergestellt, sodass der eine Anwender nicht die Dateien einer anderen Anwenderin verändern oder löschen kann?

 Wie wird der Dateizugriff implementiert? Wie wird beispielsweise die Funktionalität zwischen Interrupt-Handlern und Code im Hintergrund aufgeteilt, und wie kommunizieren diese beiden Elemente zuverlässig miteinander?

 Welche Scheduling-Richtlinien werden genutzt, wenn es konkurrierende Zugriffe auf mehrere Dateien gibt?

 Wie können die Daten von kürzlich angesprochenen Dateien im Speicher gehalten werden, um die Anzahl der Festplattenzugriffe zu verringern?

 Wie kann eine Vielzahl von unterschiedlichen Secondary-Storage-Devices – wie beispielsweise Festplatten oder Flash Drives – in ein einzelnes Dateisystem integriert werden?

Alle diese Probleme – und viele mehr – werden von der Implementierung des Unix-Dateisystems behandelt. Sie sind für Programmiererinnen und Programmierer, die die Systemaufrufe nutzen, unsichtbar. Die Implementierungen der Unix-I/O-Schnittstelle haben sich im Laufe der Jahre massiv weiterentwickelt, aber die fünf grundlegenden Kernelaufrufe haben sich nicht geändert.

Ein weiteres Beispiel eines tiefen Moduls ist der Garbage Collector in einer Sprache wie Go oder Java. Dieses Modul besitzt überhaupt keine Schnittstelle – es arbeitet unsichtbar hinter den Kulissen, um ungenutzten Speicher aufzuräumen. Indem ein System um eine Garbage Collection erweitert wird, schrumpft tatsächlich die Gesamtschnittstelle, da die Schnittstelle zum Freigeben von Objekten wegfällt. Das Implementieren eines Garbage Collector ist ziemlich komplex, aber diese Komplexität wird gegenüber den Programmierern verborgen.

Tiefe Module wie Unix-I/O und Garbage Collectors liefern leistungsfähige Abstraktionen, weil sie sich leicht einsetzen lassen, dabei aber signifikante Implementierungskomplexität verbergen.

Prinzipien des Softwaredesigns

Подняться наверх