Diese RUP-Richtlinien für die Programmierung mit Ada sind eine Vorlage, aus der Sie einen Codierungsstandard für Ihre eigene Organisation ableiten können. Das vorliegende Dokument gibt an, wie Ada-Programme geschrieben werden müssen. Es richtet sich an alle Designer und Entwickler von Anwendungssoftware, die Ada als Implementierungssprache oder als Designsprache, beispielsweise für die Angabe von Schnittstellen oder Datenstrukturen, verwenden.
Die in diesem Dokument beschriebenen Regeln decken die meisten Aspekte der Codierung ab. Für das Programmlayout, Namenskonventionen und die Verwendung von Kommentaren gelten allgemeine Regeln. Für ausgewählte Ada-Features gelten spezifische Regeln, die verbotene Konstrukte angeben, Verwendungsmuster empfehlen und generelle Hinweise zur Verbesserung der Programmqualität enthalten.
Diese Richtlinien für die Programmierung überschneiden sich teilweise mit den Designrichtlinien für ein Projekt. Diese Überschneidungen sind jedoch gewollt. Viele Codierungsregeln wurden aufgestellt, um einen objektorientierten Ansatz für das Softwaredesign aktiv zu unterstützen und zu fördern. Dies gilt insbesondere für Regeln im Bereich der Namenskonventionen.
Ursprünglich wurden die Richtlinien für Ada 83 geschrieben. Es gibt Regeln für die Kompatibilität mit Ada 95, jedoch keine spezifischen Richtlinien für die Verwendung der neuen Features der Sprache nach überarbeitetem Sprachstandard, z. B. für die Verwendung von erweiterbaren Typen, untergeordneten Einheiten oder von DECIMAL-Typen.
Der Aufbau des Dokument ist an die Struktur des Ada-Referenzhandbuchs angelehnt [ISO 8052].
In Kapitel 2, Einführung, sind die grundlegenden Prinzipien erläutert, auf denen die Richtlinien basieren. Dieses Kapitel stellt auch eine Klassifizierung der Richtlinien vor.
Kapitel 3, Codelayout, beschäftigt sich mit der generellen visuellen Organisation von Programmtexten.
Kapitel 4, Kommentare, enthält eine Anleitung für die Verwendung von Kommentaren zur strukturierten, sinnvollen und verwaltbaren Dokumentation des Codes.
In Kapitel 5, Namenskonventionen, finden Sie einige allgemeine Regeln für die Benennung von Sprachentitäten und Beispiele. Dieses Kapitel müssen Sie an die Anforderungen Ihres spezifischen Projekts bzw. Ihrer Organisation anpassen.
Kapitel 6, Deklarationen, und Kapitel 7, Ausdrücke und Anweisungen, enthalten weitere Ratschläge zu den einzelnen Sprachkonstrukten.
In Kapitel 8, Transparenzprobleme, und Kapitel 9, Programmstruktur und Kompilierungsprobleme, finden Sie Anleitungen zur globalen Strukturierung und Organisation von Programmen.
Kapitel 10, Parallelität, beschäftigt sich mit dem speziellen Thema der Verwendung von Features zur Task-Verarbeitung und von zeitbezogenen Features.
In Kapitel 11, Fehlerbehandlung und Ausnahmen finden Sie eine Anleitung für die Verwendung bzw. Nichtverwendung von Ausnahmen für eine einfache und systematische Fehlerbehandlung.
Kapitel 12, Low-Level-Programmierung, beschäftigt sich mit Fragen zu den Darstellungsklauseln.
In Kapitel 13, Zusammenfassung, sind noch einmal die wichtigsten Richtlinien aufgeführt.
Dieses Dokument ersetzt die Veröffentlichung Ada Guidelines: Recommendations for Designers and programmers, Application Note 15, Rational, Santa Clara, CA, 1990.
Ada wurde explizit entworfen, um die Entwicklung qualitativ hochwertiger, zuverlässiger, wiederverwendbarer und portierbarer Software zu unterstützen [ISO 87, Abschnitt 1.3]. Dies kann jedoch nicht allein mit einer Programmiersprache erreicht werden. Die Programmierung ist als Teil eines Prozesses zu verstehen, der viel Disziplin erfordert.
Die meisten hier angegebenen Richtlinien haben klaren und verständlichen Ada-Quellcode zum Ziel, der einen wichtigen Beitrag zur Zuverlässigkeit und Wartungsfreundlichkeit leistet. Was mit klarem und verständlichem Code gemeint ist, geht aus den drei folgenden grundlegenden Prinzipien hervor.
Quellcode wird während seiner Lebensdauer häufiger gelesen als geschrieben. Dies gilt insbesondere für Spezifikationen. Code lässt sich im Idealfall wie eine englische Beschreibung der erforderlichen Schritte lesen, hat jedoch den Vorteil, dass er diese Schritte auch gleich ausführt. Programme werden eher für Menschen als für Computer geschrieben. Das Lesen von Code ist ein komplexer mentaler Prozess, der durch Uniformität unterstützt werden kann. Dieses Prinzip der Uniformität sorgt für möglichst wenig Überraschungen. Ein einheitlicher Stil für ein Projekt ist ein wichtiger Grund, aus dem sich ein Team von Softwareentwicklern auf Programmierungsstandards einigt. Solche Standards sind nicht als Strafe oder als Kreativitäts- und Produktivitätshemmnis zu verstehen.
Ein weiteres wichtiges Prinzip, das diesem Leitfaden zugrunde liegt, ist das Prinzip der zentralen Wartung. Eine Designentscheidung sollte nach Möglichkeit nur an einem Punkt der Ada-Quelle ausgedrückt werden. Die Konsequenzen dieser Entscheidung sollten größtenteils programmatisch von diesem Punkt abgeleitet werden. Eine Verletzung dieses Prinzips kann sich sehr nachteilig auf die Wartungsfreundlichkeit und Zuverlässigkeit, aber auch auf die Verständlichkeit des Codes auswirken.
Zur Verbesserung der Lesbarkeit gilt schließlich noch das Prinzip, möglichst wenig Schnickschnack aufzunehmen. Dieses Prinzip soll helfen, eine Überhäufung des Quellcodes mit visuellem Schnickschnack zu vermeiden. Dazu gehören Balken, Fenster und Texte mit geringem Informationsgehalt oder mit Informationen, die nicht zum Verständnis des Verwendungszwecks der Software beitragen.
Die Portierbarkeit und die Wiederverwendbarkeit gaben Anlass für viele der Richtlinien. Der Code muss für verschiedene Zielcomputer auf mehrere verschiedene Compiler und schließlich auf eine verbesserte Version von Ada, "Ada 95", portiert werden [PLO92, TAY92].
Die hier vorgestellten Richtlinien gehen von einigen wenigen Grundvoraussetzungen aus:
Programmierer werden ermuntert, die innovativen Ada-Features zu verwenden, wo immer dies sinnvoll erscheint. Dass einige Programmierer sich nicht mit diesen Features auskennen, sollte kein hinreichender Grund sein, auf die Verwendung der Features zu verzichten. Nur so kann das Projekt tatsächlich von dem Einsatz von Ada profitieren. Verwenden Sie Ada nicht wie Pascal oder FORTRAN. Es wird davon abgeraten, den Code in Kommentaren zu umschreiben. Ganz im Gegenteil, Sie sollten überall, wo es machbar ist, Ada anstelle von Kommentaren verwenden.
Viele der Namenskonventionen basieren auf dem Vokabular und der Syntax der englischen Sprache. Ada-Schlüsselwörter sind sogar allgemein gebräuchliche englische Wörter, die nicht mit anderen Sprachen kombiniert werden sollten, um die Lesbarkeit nicht zu beeinträchtigen.
Die Namenskonventionen und einige andere Regeln gehen davon aus, dass keine USE-Klauseln verwendet werden.
Die meisten Regeln kommen in großen Ada-Systemen am besten zum Tragen. Sie können natürlich auch auf kleine Systeme angewendet werden, um sich besser mit Ada vertraut zu machen und Uniformität auf Projekt- oder Unternehmensebene zu erreichen.
Wenn Sie in der Rational-Umgebung arbeiten, können Sie Probleme wie das Codelayout oder Bezeichner in schließenden Konstrukten dem Ada-Editor und dem Ada-Formatierungstool überlassen. Die Layoutempfehlungen in diesem Dokument können Sie jedoch auf jeder Entwicklungsplattform anwenden.
Viele Regeln unterstützen eine systematische Abbildung objektorientierter Konzepte in Ada-Features und spezifischen Namenskonventionen.
Nicht alle hier genannten Richtlinien sind von gleicher Wichtigkeit. Sie können vielmehr grob den folgenden Kategorien zugeordnet werden:
Diese Richtlinien sind lediglich Vorschläge. Ob Sie sie anwenden oder nicht, bleibt Ihnen überlassen. Es entsteht kein Schaden, wenn Sie sie nicht beachten. Hinweise sind in diesem Dokument mit dem obigen Symbol gekennzeichnet.
Für diese Richtlinien gibt es eher technische Gründe. Sie können sich auf die Portierbarkeit oder Wiederverwendbarkeit, in einigen Implementierungen auch auf die Leistung auswirken. Empfehlungen sollten, abgesehen von den in diesem Dokument genannten Ausnahmen, nur aus gutem Grund außer Acht gelassen werden. Empfehlungen sind in diesem Dokument mit dem obigen Symbol gekennzeichnet.
Die Verwendung des betreffenden Features ist gefährlich, jedoch nicht grundsätzlich verboten. Die Entscheidung für oder gegen die Verwendung sollte auf Projektebene getroffen werden und in hohem Maße transparent sein. Einschränkungen sind in diesem Dokument mit dem obigen Symbol gekennzeichnet.
Die Nichtbeachtung einer Anforderung führt definitiv zu fehlerhaftem, unzuverlässigem oder nicht portierbarem Code. Anforderungen müssen erfüllt werden. Sie sind in diesem Dokument mit dem Symbol einer zeigenden Hand gekennzeichnet.
Die vorliegenden Richtlinien verbieten im Gegensatz zu vielen anderen Ada-Codierungsstandards einige wenige Ada-Features grundsätzlich. Der Schlüssel zu guter Software liegt hier in folgenden Faktoren:
Lassen Sie sich vom gesunden Menschenverstand
leiten.
Wenn Sie keine geltende Regel oder Richtlinie finden können oder eine Regel offensichtlich nicht zutrifft, halten Sie sich an die grundlegenden Prinzipien und bauen Sie auf den gesunden Menschenverstand. Diese Regel gilt vor allen anderen. Gesunder Menschenverstand ist unabdingbar.
Das Layout eines Programms wird vollständig vom Rational Environment Formatter gesteuert, so dass sich der Programmierer, abgesehen von Kommentaren und Leerstellen, nicht viele Gedanken über das Layout machen muss. Die Formatierungskonventionen des Tools sind in Anhang E des Referenzhandbuchs zur Programmiersprache Ada beschrieben [ISO87]. Diese Konventionen sehen vor, dass die Schlüsselwörter am Anfang und Ende eines strukturierten Konstrukts vertikal ausgerichtet werden und dass der Bezeichner eines Konstrukts systematisch am Ende des Konstrukts wiederholt wird.
Das konkrete Verhalten des Formatierungstools wird von mehreren Bibliotheksschaltern gesteuert, die während des Projekts ausgehend von einer gemeinsamen Modellwelt eine einheitliche Gruppe von Werten empfangen. Die relevanten Schalter sind nachfolgend mit ihrem aktuellen Wert für die hier empfohlene Modellwelt aufgelistet.
Gibt die Groß-/Kleinschreibung von Bezeichnern in Ada-Einheiten an. Der allererste Buchstabe und jeweils der erste Buchstabe nach einem Unterstreichungszeichen wird groß geschrieben. Diese Art der Großschreibung ist bei den Schriftarten, die von den meisten modernen Anzeigegeräten und Laserdruckern verwendet werden, die für Menschen am besten lesbare Form.
Gibt die Groß-/Kleinschreibung von Ada-Schlüsselwörtern an, die die Schlüsselwörter geringfügig von den Bezeichnern unterscheidet.
Gibt die Groß-/Kleinschreibung des Buchstabens "E" in Gleitkommaliteralen und der Basen ("A" bis "F") in Literalen mit Basis.
Eine Ada-Einheit wird gemäß der allgemeinen Konventionen im Anhang E des Ada-Referenzhandbuchs formatiert [ISO87]. Demnach werden die Schlüsselwörter am Anfang und Ende eines strukturierten Konstrukts untereinander ausgerichtet. Dies gilt beispielsweise für "loop" und "end loop" bzw. für "record" und "end record". Elemente innerhalb strukturierter Konstrukte werden rechts eingerückt.
Gibt die Anzahl der Spalten an, um die das Formatierungstool strukturierte (übergeordnete) Konstrukte einrückt, z. B. die Anweisungen "if", "case" und "loop".
Gibt die Anzahl der Spalten an, um die das Formatierungstool untergeordnete Konstrukte einrückt, z. B. Datensatzdeklarationen, Typendeklarationen, Ausnahmebehandlungsroutinen, Alternativen, Anweisungen zur Groß-/Kleinschreibung sowie benannte und gekennzeichnete Anweisungen.
Gibt die Anzahl der Spalten an, nach der das Formatierungstool Zeilen in Ada-Einheiten für die Ausgabe umbricht. Diese Umbrüche ermöglichen die Anzeige formatierter Einheiten mit traditionellen Terminals wie VT100.
Gibt die Anzahl der Spalten an, um die das Formatierungstool die Zeilen einer Anweisung ab der zweiten Zeile einrückt, wenn die Zeile länger als Line_Length ist und umgebrochen werden muss. Das Formatierungstool führt die Einrückung um die von Statement_Indentation angegebene Anzahl von Spalten nur aus, wenn es kein lexikalisches Konstrukt gibt, an dem der eingerückte Code ausgerichtet werden kann.
Gibt die Anzahl der Spalten an, die in jeder Zeile für die Anzeige einer Anweisung reserviert ist. Falls der aktuelle Grad der Einrückung für eine Zeile weniger als die von Statement_Length angegebene Anzahl Spalten zulässt, geht das Formatierungstool zur Einrückung um die von Wrap_Indentation angegebene Anzahl Spalten über. Auf diese Weise wird verhindert, dass die Ausgabe tief verschachtelter Anweisungen über den rechten Rand hinaus geht.
Gibt die Spalte an, bei der das Formatierungstool mit der nächsten Einrückungsebene beginnt, wenn der derzeitige Grad der Einrückung nicht die von Statement_Length angegebene Anzahl Spalten zulässt. Auf diese Weise wird verhindert, dass die Ausgabe tief verschachtelter Anweisungen über den rechten Rand hinaus geht.
Steuert die Formatierung von Listen der Form (xxx:aaa; yyy:bbb), die in formalen Abschnitten von Unterprogrammen und als Diskriminanten in Typendeklarationen vorkommen, und die Formatierung von Listen der Form (xxx=>aaa, yyy=>bbb), die in Aufrufen und Aggregaten von Unterprogrammen vorkommen. Wenn eine Liste nicht in eine Zeile passt, beginnt jeder Eintrag der Liste in einer neuen Zeile, da diese Option ungleich null (true) ist.
Gibt die Anzahl der Leerstellen ein, die das Formatierungstool einfügen kann, um lexikalische Konstrukte wie Doppelpunkte, Zuweisungen und Pfeile in benannter Schreibweise in aufeinander folgenden Anweisungen auszurichten. Falls für die Ausrichtung eines Konstrukts mehr Leerstellen erforderlich sind, wird das Konstrukt nicht ausgerichtet.
Der Programmierer kann ein bestimmtes Layout erzwingen, indem er durch
Eingabe von <Leerzeichen>
<Leerzeichen> <CR> ein Zeilenende oder einen Zeilenumbruch einfügt, das bzw. den das Formatierungstool nicht entfernt.
Mit diesem Verfahren sollten Listen mit Ada-Elementen, die mehr als drei Elemente enthalten oder nicht in eine Zeile passen,
so umgebrochen werden, dass
jedes Element in einer neuen Zeile steht. Die Listen sind so besser lesbar und können leichter gewartet werden. Dies gilt insbesondere für die folgenden
Ada-Konstrukte (siehe Anhang E des Ada-Referenzhandbuchs
[ISO87]):
Argumentzuordnung
pragma Suppress (Range_Check, On => This_Type, On => That_Type, On => That_Other_Type);
Bezeichner- und Komponentenlisten
Next_Position, Previous_Position, Current_Position : Position; type Some_Record is record A_Component, B_Component, C_Component : Component_Type; end record;
Aufzählungstypdefinition
type Navaid is (Vor, Vor_Dme, Dme, Tacan, VorTac, NDB);
Diskriminantenvorgabe
subtype Constrained is Element (Name_Length => Name'Length, Valid => True, Operation => Skip);
Reihenfolge von Anweisungen (vom Formatierungstool sortiert)
Formaler Abschnitt, formaler generischer Abschnitt, aktueller Parameterabschnitt, aktueller generischer Parameterabschnitt
procedure Just_Do_It (This : in Some_Type; For_That : in Some Other_Type; Status : out Status_Type); Just_Do_It (This => This_Value; For_That => That_Value; Status => The_Status);
Es ist ein weit verbreiteter Irrglaube, dass gute Programmierer an der Anzahl der Kommentare zu erkennen sind. Tatsächlich kann man sie an der Qualität ihrer Kommentare erkennen.
Kommentare sollten den Ada-Code ergänzen und nicht umschreiben. Ada ist an sich eine sehr gut lesbare Programmiersprache, was sich noch deutlicher zeigt, wenn gute Namenskonventionen angewendet werden. Kommentare sollten eine Ergänzung zum Ada-Code sein und das erklären, was nicht offensichtlich ist. Eine Wiederholung von Ada-Syntax oder -Semantik sollte vermieden werden. Kommentare können dem Leser etwas über die zugrunde liegenden Konzepte und die Abhängigkeiten vermitteln und ihm helfen, besonders komplexe Datencodierungen oder Algorithmen zu verstehen. In Kommentaren sollten Abweichungen von Codierungs- oder Designstandards, die Verwendung eingeschränkter Features und spezielle "Tricks" hervorgehoben werden. Kommentarrahmen oder -formulare, die systematisch für jedes größere Ada-Konstrukt (wie Unterprogramme und Pakete) erscheinen, haben den Vorteil der Einheitlichkeit und erinnern den Programmierer daran, den Code zu dokumentieren. Häufig verleiten sie jedoch zu einem umschreibenden Stil. Der Programmierer sollte zu jedem Kommentar ohne Umschweife angeben können, welchen Nutzen er bringt.
Ein falscher oder irreführender Kommentar ist schlimmer als ein weggelassener Kommentar. Kommentare werden nicht vom Compiler überprüft (es sei denn, sie sind wie bei RDF (Rational Design Facility) Teil formaler Ada Design Language (ADL) oder PDL Program Design Language (PDL)). Deshalb sollten Designentscheidungen gemäß dem Prinzip der zentralen Wartung in Ada und nicht in Kommentaren ausgedrückt werden, auch wenn dafür etwas mehr Deklarationen erforderlich sind.
Schauen Sie sich die folgende (nicht so gute) Beispieldeklaration an:
------------------------------------------------------------ -- procedure Create ------------------------------------------------------------ -- procedure Create (The_Subscriber: in out Subscriber.Handle; With_Name : in out Subscriber.Name); -- -- Zweck: Diese Prozedur erstellt einen Subskribenten mit einem bestimmten -- Namen. -- -- Parameter: - The_Subscriber :mode in out, type Subscriber.Handle - Die interne Kennung des erstellten Subskribenten. - With_Name :mode in, type Subscriber.Name - Der Name des zu erstellenden Subskribenten. - Der Name hat folgende Syntax: -- <Buchstabe> { <Buchstabe> | <Ziffer> } -- Ausnahmen: -- Subscriber.Collection_Overflow, wenn kein Platz für das Erstellen -- eines neuen Subskribenten verfügbar ist -- Subscriber.Invalid_Name, wenn der Name leer oder falsch -- formatiert ist -- -------------------------------------------- end Create ----
Zu diesem Beispiel gibt es einiges anzumerken.
Der folgenden Version, die knapper und sinnvoller ist, wäre der Vorzug zu geben:
procedure Create (The_Subscriber : in out Subscriber.Handle; With_Name : in Subscriber.Name); -- -- löst Subscriber.Collection_Overflow aus -- löst Subscriber.Invalid_Name aus, wenn der Name leer -- oder falsch formatiert ist (siehe Syntaxbeschreibung -- nach der Deklaration von type Subscriber.Name)
Kommentare sollten in der Nähe
des Codes stehen, auf den sie sich beziehen, und dieselbe Einrückung wie dieser Code haben. Der Kommentarblock sollte durch jeweils eine leere Kommentarzeile vom
Ada-Konstrukt abgehoben werden:
procedure First_One; -- -- Dieser Kommentar bezieht sich auf First_One. -- Dieser Kommentar gilt jedoch für Second_One. -- procedure Second_One (Times : Natural);
Zusammengehörige Quellcodeblöcke (Kommentare und Code) sollten durch
leere Kommentarzeilen und nicht durch Kommentarzeilen mit gestrichelten Linien oder doppelten Linien voneinander abgesetzt werden.
Trennen Sie die einzelnen Absätze eines Kommentarblocks
nicht durch Leerzeilen, sondern durch leere Kommentarzeilen.
-- Eine Erläuterung, die in einem weiteren Absatz fortgesetzt -- werden muss. -- -- Die leere Kommentarzeile an dieser Stelle verdeutlicht, -- dass es sich um einen Kommentarblock handelt.
Sie können Kommentare über oder unter dem betreffenden
Ada-Konstrukt angeben. Kommentare mit Abschnittsüberschriften oder wichtigen Informationen, die für mehrere Ada-Konstrukte gelten, sollten
Sie jedoch
über den Konstrukten angeben. Kommentare mit Anmerkungen oder zusätzlichen Informationen sollten
unter dem Ada-Konstrukt stehen, auf das sie sich beziehen.
Fassen Sie Kommentare
am Anfang des Ada-Konstrukts zusammen. Nutzen Sie die volle Seitenbreite aus. Schreiben Sie Kommentare
nicht in dieselbe Zeile wie ein Ada-Konstrukt, da die Ausrichtung solcher Kommentare oft verloren geht. Bei langen Deklarationen, z. B. für Literale des Aufzählungstyps,
können solche Kommentare jedoch in den Beschreibungen der einzelnen Elemente toleriert werden.
Verwenden Sie eine kleine
Hierarchie von Standardkommentarblöcken für Abschnittsüberschriften. Dies gilt allerdings nur für sehr große Ada-Einheiten
(>200 Deklarationen oder Anweisungen).
--=========================================================== -- -- HIER HAUPTÜBERSCHRIFT -- --=========================================================== ------------------------------------------------------------- -- Hier Untertitel ------------------------------------------------------------- -- -------------------- -- Kopfzeile des Unterabschnitts -- --------------------
Oberhalb solcher Kommentarüberschriften sollten mehr Leerzeilen als unterhalb eingefügt werden. Fügen Sie beispielsweise zwei Zeilen davor und eine Zeile danach ein. Dadurch wird die Überschrift visuell enger mit dem nachfolgenden Text verbunden.
Vermeiden Sie Kopfzeilen mit Informationen wie Telefonnummern, Erstellungs- und Änderungsdatum und Speicherposition (oder Dateiname) der Einheit, da solche Informationen schnell veralten. Stellen Sie Hinweise zum Eigentumsrecht oder den Copyrightvermerk
an das Ende der Einheit, insbesondere, wenn Sie in der Rational-Umgebung
arbeiten. Wenn der Benutzer die Quelle einer Paketspezifikation aufruft, in der Rational-Umgebung beispielsweise durch Klicken
auf [Definition], möchte er nicht erst zwei oder drei Seiten mit Text durchblättern, der für das Verständnis des Programms nicht von Bedeutung
ist und/oder überhaupt keine Programminformationen enthält, wie es beim Copyrightvermerk der Fall ist. Vermeiden Sie
vertikale Balken, geschlossene Rahmen oder Kästchen, die nur mit Mühe in Form gehalten werden können und im Grunde nichts als optischer Schnickschnack sind. Verwenden Sie
zum Protokollieren der Einheit Anmerkungen in Rational
CMVC (oder eine andere Form von Softwareentwicklungsdateien).
Replizieren Sie keine Informationen, die an anderer Stelle
verfügbar sind, sondern zeigen Sie auf diese Informationen.
Verwenden Sie anstelle von Kommentaren weitestgehend
Ada. Dies lässt sich durch bessere Namen, zusätzliche temporäre Variablen, Qualifikation, Umbenennung, Subtypen, statische
Ausdrücke und durch Attribute erreichen, die (zumindest bei Verwendung eines guten Compilers) allesamt keinen Einfluss auf den generierten Code haben. Sie können auch
kleine Inline-Prädikatfunktionen verwenden und den Code in mehrere Prozeduren ohne Parameter unterteilen, deren Namen gleichzeitig die Überschriften für mehrere separate Codeabschnitte
sind.
Beispiele:
Dieser Code sollte ersetzt werden:
exit when Su.Locate (Ch, Str) /= 0; -- Suchschleife nach erfolgreicher Suche verlassen
Search_Loop : loop
Found_It := Su.Locate (Ch, Str) /= 0;
exit Search_Loop when Found_It
end Search_Loop;
Dieser Code sollte ersetzt werden:
if Value < 'A' or else Value > 'Z' then -- Sofern keine Großbuchstaben angegeben sind
subtype Uppercase_Letters is Character range 'A' .. 'Z'; if Value not in Uppercase_Letters then ...
Dieser Code sollte ersetzt werden:
X := Green; -- Dies ist das Green für den -- Status, nicht für die Farbe raise Fatal_Error; -- Vom Paket Outer_Scope delay 384.0; -- Entspricht 6 Minuten und -- 24 Sekunden
The_Status := Green;
X := Status'(Green); raise Outer_Scope.Fatal_Error; delay 6.0 * Minute + 24.0 * Second;
Dieser Code sollte ersetzt werden:
if Is_Valid (The_Table (Index).Descriptor(Rank).all) then -- Dies ist der aktuelle Wert für die Iteration. Ist er gültig, -- werden Elemente an die enthaltene Liste angefügt. Append (Item, To_List => The_Table (Index).Descriptor(Rank).Ptr);|
declare Current_Rank : Lists.List renames The_Table (Index).Descriptor (Rank); begin if Is_Valid (Current_Rank.all) then Append (Item, To_List => Current_Rank.Ptr); end if; end;
Achten Sie in Kommentaren auf
Stil, Syntax und Rechtschreibung. Verwenden Sie keinen kryptischen Telegrammstil. Nutzen Sie die Rechtschreibprüfung. (In der Rational-Umgebung können Sie
Speller.Check_Image aufrufen.)
Verwenden Sie keine Buchstaben mit Akzenten oder Sonderzeichen
aus anderen Sprachen. Möglicherweise unterstützen einige Entwicklungssysteme und Ada-Compiler in Kommentaren Zeichen, die nicht zum englischen Alphabet gehören
(Ada Issue AI-339). Dies ist jedoch nicht auf alle Systeme übertragbar und könnte zu Fehlern führen.
Für Unterprogramme
sollten Sie mindestens Folgendes dokumentieren:
Dokumentieren Sie für Typen und Objekte alle
Invarianten oder zusätzlichen Einschränkungen, die nicht in Ada ausgedrückt werden können.
Vermeiden Sie Wiederholungen in Kommentaren. Der Abschnitt zum Zweck sollte beispielsweise kurz
angeben, was ausgeführt wird, nicht aber wie es ausgeführt wird.
In der Übersicht sollte das Design kurz vorgestellt werden. Die Beschreibung sollte sich nicht mit den verwendeten Algorithmen beschäftigen, sondern
die Verwendung des Pakets erläutern.
Der Abschnitt Data_Structure und der Abschnitt mit den Algorithmen sollten genug Informationen enthalten, um die Hauptimplementierungsstrategie verständlich zu machen (damit das Paket ordnungsgemäß verwendet werden kann). Die Abschnitte sollten jedoch nicht alle Details der Implementierung und keine Informationen enthalten, die für die korrekte Verwendung des Pakets nicht von Relevanz sind.
Ordnen Sie Ada-Entitäten (Programmeinheiten, Typen, Subtypen, Objekten, Literalen, Ausnahmen) gute Namen zu. Dies ist in allen Softwareanwendungen einer der heikelsten Punkte. In mittleren und großen Anwendungen taucht darüber hinaus das Problem von Namenskonflikten auf bzw. die Schwierigkeit, genug Synonyme für ähnliche, aber dennoch verschiedene Beschreibungen desselben realen Konzepts (oder Namen für einen Typ, einen Subtyp, ein Objekt und einen Parameter) zu finden. Hier können Sie sich die Regel, keine (oder nur sehr stark eingeschränkt) USE-Klauseln zu verwenden, zunutze machen. In vielen Situationen können Sie dann einen Namen abkürzen und ohne das Risiko von Unklarheiten dieselben beschreibenden Worte wiederverwenden.
Wählen sie verständliche, lesbare und aussagekräftige
Namen.
Im Gegensatz zu anderen Programmiersprachen beschränkt Ada die Länge von Bezeichnern nicht auf 6, 8 oder 15 Zeichen. Dass kurze Namen schneller geschrieben werden können, ist keine akzeptable Begründung für die Verwendung kurzer Namen. Bezeichner, die nur aus einem Buchstaben bestehen, sind in der Regel keine gute Wahl und ein Zeichen von Faulheit. Es kann einige wenige Ausnahmen geben, z. B. den Buchstaben E für die Basis der natürlichen Logarithmen, Pi und eine Handvoll weiterer allgemein bekannter Fälle.
Trennen Sie mehrere Wörter eines Namens durch ein
Unterstreichungszeichen:
Is_Name_Valid
ist besser als IsNameValid
.
Verwenden Sie vollständige Namen und keine
Abkürzungen.
Verwenden Sie nur Abkürzungen, die für das Projekt genehmigt wurden.
Wenn Abkürzungen verwendet werden, muss es sich um hinlänglich bekannte Abkürzungen des jeweiligen Anwendungsbereichs handeln (z. B. SFT für Schnelle Fourier-Transformation) oder um eine Abkürzung aus einer für das Projekt bestätigten Liste. Andernfalls werden sehr wahrscheinlich an der einen oder anderen Stelle ähnliche, aber nicht ganz identische Abkürzungen auftauchen, die für Unklarheiten sorgen und zu Fehlern führen können (z. B. die Abkürzungen Tr_Id, Trck_Id, Tr_Iden, Trid, Tid, Tr_Ident usw. für Track_Identification).
Gehen Sie sparsam mit Suffixen zur Angabe der Kategorie von Ada-Konstrukten
um. Sie tragen nicht zur Verbesserung der Lesbarkeit bei.
Suffixe für die Kategorie von Ada-Entitäten wie _Package für Pakete, _Error für Ausnahmen, _Type für Typ und _Param für Unterprogrammparameter sind in der Regel nicht geeignet, wenn es um die Lesbarkeit und Verständlichkeit des Codes geht. Suffixe wie _Array, _Record und _Function sind noch schlimmer. Sowohl der Ada-Compiler als auch der Leser des Codes kann eine Ausnahme anhand des Kontextes von einem Unterprogramm unterscheiden. In einer Anweisung raise oder in einer Ausnahmebehandlungsroutine kann ganz offensichtlich nur der Name einer Ausnahme erscheinen. Derartige Suffixe sind daher nur in den folgenden Situationen sinnvoll:
Generischer formaler Typ mit dem Suffix _Constrained
Zugriffstyp mit dem Suffix _Pointer oder einer anderen Form des indirekten Verweises wie _Handle oder _Reference
Ein Unterprogramm, das einen potenziell blockierenden Eingangsaufruf verdeckt, mit dem Suffix _Or_Wait
Drücken Sie Namen so aus,
dass sie gut zum Verwendungszweck passen.
Denken Sie über den Kontext nach, in dem deine exportierte Entität verwendet werden wird, und wählen Sie den Namen unter diesem Gesichtspunkt aus. Eine Entität wird nur einmal deklariert, jedoch häufig verwendet. Dies gilt insbesondere für die Namen von Unterprogrammen und deren Parameter. Die Namensassoziationen für die resultierenden Aufrufe sollten sich möglichst nah an natürlicher Sprache orientieren. Vergessen Sie nicht, dass die Abwesenheit von USE-Klauseln für die meisten deklarierten Entitäten den qualifizierten Namen zwingend erforderlich macht. Für formale generische Parameter, die eher in der generischen Einheit als auf der Clientseite verwendet werden, gilt es einen guten Kompromiss zu finden. Bei formalen Parametern von Unterprogrammen sollten Sie das Hauptaugenmerk jedoch definitiv darauf legen, dass der Name auf dem Client gut aussieht.
Verwenden Sie englische Wörter ohne Rechtschreibfehler.
Ein Mix aus mehreren Sprachen (z. B. Französisch und Englisch) erschwert das Lesen des Codes und führt manchmal zur Mehrdeutigkeit von Bezeichnern. Da die Ada-Schlüsselwörter bereits englisch sind, sollte der Rest auch englisch sein. Die US-Schreibweise sollte bevorzugt werden, damit die integrierte Rechtschreibprüfung der Rational-Umgebung genutzt werden kann.
Erstellen Sie keine neuen Definitionen für
Entitäten aus dem Paket 'Standard'. Dies ist absolut untersagt.
Die Missachtung dieser Regel kann Unklarheiten und dramatische Fehler nach sich ziehen. Diese Regel ließe sich auch auf andere vordefinierte Bibliothekseinheiten wie Calendar und System ausdehnen und schließt natürlich den Bezeichner Standard selbst mit ein.
Vermeiden Sie, Bezeichner anderer vordefinierter Pakete wie System oder
Calendar neu zu definieren.
Verwenden Sie
Wide_Character und Wide_String nicht als Bezeichner. Beide werden im Paket 'Standard' von
Ada 95 eingeführt. Verwenden Sie keine Kompiliereinheit mit dem Namen
Ada.
Verwenden Sie die Wörter
abstract, aliased,protected, requeue,
tagged und until nicht als Bezeichner. Sie werden in Ada 95 als Schlüsselwörter verwendet.
Nachfolgend sind einige Namensvorschläge für verschiedene Ada-Entitäten angegeben. Vorausgesetzt wird ein generell am objektorientierten Stil angelehntes Design. Weitere Erläuterungen hierzu finden Sie in Anhang A.
Wenn mit einem Paket eine Objektklasse eingeführt wird, benennen Sie das Paket nach der Objektklasse.
Der Name einer Objektklasse ist in der Regel ein normales Substantiv im Singular, an das ggf. (wenn eine parametrisierte Klasse definiert wird) das Suffix _Generic angefügt werden kann. Die Pluralform sollten Sie nur verwenden,
wenn die Objekte immer in Gruppen vorkommen. Beispiele:
package Text is package Line is package Mailbox is package Message is package Attributes is package Subscriber is package List_Generic is
Wenn ein Paket eine Schnittstelle oder eine Funktionsgruppe angibt und keinen Bezug zu einem Objekt hat, sollten Sie dies
im Namen ausdrücken:
package Low_Layer_Interface is package Math_Definitions is
Wenn ein
"logisches" Paket durch unstrukturierte Dekomposition als mehrere verschiedene Pakete ausgedrückt werden muss, verwenden Sie Suffixe aus einer für das Projekt vereinbarten
Liste. Ein logisches Paket Mailbox könnte beispielsweise wie folgt implementiert werden:
package Mailbox_Definitions is package Mailbox_Exceptions is package Mailbox_Io is package Mailbox_Utilities is package Mailbox_Implementation is package Mailbox_Main is
Weitere annehmbare Suffixe:
_Test_Support _Test_Main _Log _Hidden_Definitions _Maintenance _Debug
Verwenden Sie in einem Paket, das eine Objektklasse definiert, Folgendes, sofern es ein Paket mit impliziter Kopiersemantik ist (d. h. der Typ instanziierbar und eine Form der
Zuweisung möglich ist):
type Object is ...
Beachten Sie, dass der Name der Klasse im Bezeichner nicht wiederholt werden sollte, da er immer in seiner vollständig qualifizierten Form verwendet wird:
Mailbox.Object Line.Object
Wenn referenzielle Semantik impliziert ist, d. h. der Typ mit Zugriffswerten (oder einer anderen Form der Dereferenzierung) implementiert wird und eine Zuweisung, sofern verfügbar, das Objekt nicht kopiert, verwenden Sie
Folgendes:
type Handle is
für einen indirekten Verweis type Reference is
als mögliche Alternative
Die Elemente werden als Suffixe verwendet, wenn ihre Verwendung ohne Suffix und nur mit dem Paketnamen als Präfix unklar oder mehrdeutig ist.
Wenn mehrere Objekte impliziert sind, können Sie eine der folgenden Angaben verwenden:
type Set
type List
type Collection
type Iterator
Wenn ein Objekt mit einer Zeichenfolge bezeichnet werden soll, verwenden Sie Folgendes:
type Name
Aus Gründen der besseren Lesbarkeit sollte im gesamten definierenden Paket der qualifizierte Typname verwendet werden. In der
Rational-Umgebung führt dies auch zu einem besseren Verhalten, wenn die Funktion
[Complete] in einem Unterprogrammaufruf verwendet wird.
Schauen Sie sich dazu Subscriber.Object (vollständiger Name) im folgenden Beispiel an:
package Subscriber is type Object is private; type Handle is access Subscriber.Object; subtype Name is String; package List is new List_Generic (Subscriber.Handle); Master_List : Subscriber.List.Handle; procedure Create (The_Handle : out Subscriber.Handle; With_Name : in Subscriber.Name); procedure Append (The_Subscriber : in Subscriber.Handle; To_List : in out Subscriber.List.Handle); function Name_Of (The_Subscriber : Subscriber.Handle) return Subscriber.Name; ... private type Object is record The_Name : Subscriber.Name (1..20); ... end Subscriber;
Unter anderen Bedingungen müssen Sie für Substantive oder Qualifier+Substantiv für Typnamen
verwenden. Sie können für Typen die Pluralform verwenden, damit die Singularform Objekten (Variablen) vorbehalten bleibt:
type Point is record ... type Hidden_Attributes is ( ... type Boxes is array ...
Verwenden Sie Mode, Kind, Code usw. für Aufzählungstypen, ggf. auch als Suffix.
Für Array-Typen können Sie das Suffix _Table verwenden, wenn der einfache Name bereits für den Komponententyp verwendet wird. Verwenden Sie Namen oder Suffixe wie wie _Set und _List nur, wenn das Array mit der implizierten Semantik gewartet wird. Reservieren Sie _Vector und _Matrix für die entsprechenden mathematischen Konzepte.
Da Einzel-Task-Objekte (aus später erklärten Gründen) zu vermeiden sind, sollten
Sie auch dann einen Task-Typ einführen, wenn es nur ein Objekt dieses Typs gibt. Im folgenden Fall ist eine einfache Suffixstrategie wie die Verwendung von
_Type ausreichend:
task type Listener_Type is ... for Listener_Type'Storage_Size use ... Listener : Listener_Type;
Ähnlich verhält es sich, wenn die Verwendung eines Substantivs oder eines (substantivischen Ausdrucks)
für einen Typnamen und einen Objekt- oder Parameternamen zu Mehrdeutigkeit führt. Hängen Sie in diesem Fall an den Typnamen das Suffix
_Kind an und verwenden Sie den Objektnamen ohne Suffix.
type Status_Kind is (None, Normal, Urgent, Red); Status : Status_Kind := None;
Für alle Elemente, die immer mehrfach vorkommen, können Sie die Pluralform für den Typ
verwenden.
Da Zugriffstypen nicht ohne Risiken sind, sollte der Benutzer auf die Gefahren aufmerksam
gemacht werden. In der Regel enthalten Sie 'Pointer' in ihrem Namen. Verwenden Sie das Suffix _pointer, wenn der Name ohne Suffix mehrdeutig ist. Als Alternative kommt _Access in Frage. ;
Manchmal wird die Benennung durch ein verschachteltes Unterpaket zur Einführung einer
zweiten Abstraktion vereinfacht:
package Subscriber is ... package Status is type Kind is (Ok, Deleted, Incomplete, Suspended, Privileged); function Set (The_Status : Subscriber.Status.Kind; To_Subscriber : Subscriber.Handle); end Status; ...
Da Ausnahmen nur für Fehlersituationen gedacht sind, sollten Sie ein Substantiv oder einen substantivischen Ausdruck mit negativer Wertung verwenden:
Overflow, Threshold_Exceeded, Bad_Initial_Value
Wenn der Bezeichner in einem Klassenpaket definiert wird, muss er nicht den Namen der Klasse enthalten (z. B. Bad_Initial_Subscriber_Value), da als Ausnahme immer Subscriber.Bad_Initial_Value verwendet wird.
Nutzen Sie die Wörter
Bad, Incomplete, Invalid, Wrong, Missing oder Illegal, anstatt systematisch auf das Wort Error zurückzugreifen, das keinen spezifischen
Informationsgehalt hat.
Illegal_Data, Incomplete_Data
Verwenden Sie für Prozeduren (und Task-Einträge) Verben. Für Funktionen wollten Sie Substantive mit den Attributen oder Merkmalen der Objektklasse
verwenden. Adjektive (oder Vergangenheitspartizipien) sollten für Funktionen verwendet werden, die Boolesche Werte zurückgeben.
Subscriber.Create Subscriber.Destroy Subscriber.List.Append Subscriber.First_Name -- Gibt eine Zeichenfolge zurück Subscriber.Creation_Date -- Gibt ein Datum zurück Subscriber.List.Next Subscriber.Deleted -- Gibt einen Booleschen Wert zurück Subscriber.Unavailable -- Gibt einen Booleschen Wert zurück Subscriber.Remote
In manchen Fällen kann es sinnvoll sein, für Prädikate vor einem Substantiv das Präfix
Is_ oder Has_ anzugeben. Achten Sie bei der Angabe auf Genauigkeit, Konsistenz und auf die Zeitform.
function Has_First_Name ... function Is_Administrator ... function Is_First... function Was_Deleted ...
Dies ist sinnvoll, wenn der einfache Name bereits als Typname oder Aufzählungsliteral verwendet wird.
Verwenden Sie Prädikate in der bejahenden Form, d. h. geben Sie die Namen nicht mit "Not_" an.
Für allgemeine Operationen sollten Sie Verben aus einer Auswahlliste für das Projekt verwenden (die erweitert werden kann, wenn sich neue Erkenntnisse zum System ergeben).
Create Delete Destroy Initialize Append Revert Commit Show, Display
Verwenden Sie für Prädikatfunktionen und Boolesche Parameter bejahende Namen. Verneinende Namen können zu
doppelter Verneinung führen (z. B. Not Is_Not_Found) und beeinträchtigen die Lesbarkeit des Codes.
function Is_Not_Valid (...) return Boolean procedure Find_Client (With_The_Name : in Name; Not_Found : out Boolean)
Dieser Code sollte wie folgt definiert werden:
function Is_Valid (...) return Boolean; procedure Find_Client (With_The_Name: in Name; Found: out Boolean)
Der Client kann wie erforderlich die Verneinung des Ausdrucks testen. (Dies hat keine negativen Auswirkungen auf die Laufzeit.)
if not Is_Valid (...) then ....
In einigen Fällen lässt sich mit einem Antonym aus einem negativen Prädikat ein positives machen, ohne die Semantik zu ändern. Sie könnten beispielsweise "Is_Invalid" anstatt "Is_Not_Valid" verwenden. Bejahende Namen sind jedoch besser lesbar. "Is_Valid" ist leichter zu verstehen als "not Is_Invalid".
Verwenden Sie dasselbe Wort, wenn dieselbe allgemeine Bedeutung impliziert ist. Versuchen Sie nicht,
Synonyme oder Varianten zu finden. Die Überladung von Namen wird im Interesse einer größeren Uniformität und unter Beachtung des Prinzips 'Möglichst wenig Überraschungen' empfohlen.
Falls Unterprogramme als
"Oberflächen" oder "Wrapper" für Eingangsaufrufe verwendet werden, kann es sinnvoll sein, dies im Namen widerzuspiegeln. Zu diesem Zweck kann das Verb mit dem Suffix
_Or_Wait versehen werden oder ein Ausdruck wie Wait_For_, gefolgt von einem Substantiv, verwendet werden.
Subscriber.Get_Reply_Or_Wait Subscriber.Wait_For_Reply
Einige Operationen sollten immer konsistent (mit denselben Namen) definiert werden.
Für Typkonvertierungen von und in Zeichenfolgen, die folgenden symmetrischen Funktionen:
function Image und function Value
Für Typkonvertierungen von maschinennaher Sprache und in maschinennahe Sprache (wie beispielsweise
Byte_String für den Datenaustausch):
procedure Read und Write
Für
zugeordnete Daten:
function Allocate (besser als Create) function Destroy (oder Release, um auszudrücken, dass das Objekt verschwindet)
Wenn Sie diesen Ansatz systematisch verfolgen und Namen konsistent verwenden, wird die Typkomposition viel einfacher.
Für aktive Iteratoren müssen die folgenden Basiselemente (Primitives) immer definiert werden:
Initialize Next Is_Done Value_Of ResetWerden in einem Geltungsbereich mehrere Iteratortypen eingeführt, sollten diese Basiselemente überladen werden, anstatt für jeden Iterator eine gesonderte Gruppe von Bezeichnern einzuführen (siehe [BOO87]).
Wenn Sie vordefinierte Ada-Attribute als Funktionsnamen verwenden, achten Sie auf eine einheitliche allgemeine Semantik:
'First, 'Last, 'Length, 'Image, 'Value usw.
Beachten Sie, dass mehrere Attribute (z. B. 'Range und 'Delta) reservierte Wörter sind und daher nicht als Funktionsnamen verwendet werden können.
Stellen Sie einem Objektnamen oder Parameternamen das Präfix The_ oder This_ voran, um
Eindeutigkeit zu erzielen oder zu zeigen, dass diese Entität den Schwerpunkt der Aktion bildet. Ein Nebenobjekt, temporäres Objekt oder Hilfsobjekt können Sie mit dem Präfix
A_ oder Current_ versehen.
procedure Change_Name (The_Subscriber : in Subscriber.Handle; The_Name : in Subscriber.Name ); declare A_Subscriber : Subscriber.Handle := Subscriber.First; begin ... A_Subscriber := Subscriber.Next (The_Subscriber); end;
Verwenden Sie für Boolesche Objekte eine bejahende Prädikatklausel.
Found_It Is_Available
Is_Not_Available
sollten Sie vermeiden.
Verwenden Sie für Task-Objekte ein Substantiv oder einen substantivischen Ausdruck, das bzw. der eine aktive Entität impliziert.
Listener Resource_Manager Terminal_Driver
Bei Parametern können Sie die Lesbarkeit verbessern, wenn Sie dem Klassennamen oder einem charakteristischen Substantiv eine Präposition als Präfix voranstellen.
Dies gilt insbesondere für den Aufrufenden, wenn benannte Zuordnungen verwendet werden. Weitere hilfreiche Präfixe für Hilfsparameter haben die Form
Using_ oder, im Fall eines Parameters in out, der durch einen Nebeneffekt beteiligt ist,
Modifying_.
procedure Update (The_List : in out Subscriber.List.Handle; With_Id : in Subscriber.Identification; On_Structure : in out Structure; For_Value : in Value); procedure Change (The_Object : in out Object; Using_Object : in Object);
Aus Sicht des Aufrufenden ist die Reihenfolge
wichtig, in der Parameter deklariert sind:
Auf diese Weise können Sie die Vorteile von Standardwerten nutzen, ohne für die wichtigsten Parameter benannte Zuordnungen verwenden zu müssen.
Der Modus
"in" muss explizit angegeben werden. Dies gilt auch für Funktionen.
Wählen Sie den besten Namen aus, den Sie für eine nicht generische
Version verwenden würden, den Klassennamen für ein Paket oder ein transitives Verb (bzw. einen verbalen Ausdruck) für eine Prozedur (siehe oben), und versehen Sie den Namen mit dem Suffix
_Generic.
Wenn ein generisches Paket eine abstrakte Datenstruktur definiert, verwenden Sie für formale generische Typen
Item
oder Element
und für die exportierte Abstraktion Structure
oder ein anderes geeignetes Substantiv.
Verwenden Sie bei passiven Iteratoren im Bezeichner
ein Verb wie
Apply
, Scan
, Traverse
,
Process
oder Iterate
.
generic with procedure Act (Upon : in out Element); procedure Iterate_Generic (Upon : in out Structure);
Die Namen formaler generischer Parameter dürfen keine
Homographen sein.
generic type Foo is private; type Bar is private; with function Image (X : Foo) return String; with function Image (X : Bar) return String; package Some_Generic is ...
Dieser Code sollte ersetzt werden durch:
generic type Foo is private; type Bar is private; with function Foo_Image (X : Foo) return String; with function Bar_Image (X : Bar) return String; package Some_Generic is ...
Sofern erforderlich, können die formalen generischen Parameter in der generischen Einheit umbenannt werden:
function Image (Item : Foo) return String Renames Foo_Image; function Image (Item : Bar) return String Renames Bar_Image;
Wenn ein großes System in Rational-Subsysteme (oder eine andere Form verbundener Programmbibliotheken) partitioniert wird, sollte eine Benennungsstrategie mit folgenden Zielen definiert werden:
In einem System mit mehreren hundert Objekten und Teilobjekten sind Namensunverträglichkeiten auf der Ebene der Bibliothekseinheiten wahrscheinlich. Die Programmierer werden es schwer haben, genug Synonyme für so sinnvolle Namen wie Utilities, Support, Definitions usw. zu finden.
Auf einem Rational-Host sind Entitäten mit Suchfunktionen leicht zu finden. Wenn Code jedoch auf ein Ziel mit eigenen Tools (Debugger, Testtools usw.) portiert wird, kann das Auffinden einer Prozedur Utilities.Get in 2.000 Einheiten, die auf 100 Subsysteme verteilt sind, für einen Projektneuling eine Herausforderung darstellen.
Versehen Sie die Namen von
Einheiten auf Bibliotheksebene mit einem Präfix für das übergeordnete Subsystem (vier Buchstaben umfassende Abkürzung für das Subsystem).
Das Dokument zur Softwarearchitektur (SAD) enthält eine Liste der Subsysteme. Diese Regel gilt nicht für Bibliotheken mit Komponenten, die sehr häufig (und somit wahrscheinlich in einer Vielzahl von Projekten, für im Handel erhältliche Standardprodukte und für Standardeinheiten) wiederverwendet werden.
Beispiel:
Comm - Communication
Dbms - Database management
Disp - Displays
Math - Mathematical packages
Drive - Drivers
Allen Bibliothekseinheiten aus dem Subsystem Disp wird beispielsweise das Präfix Disp_ vorangestellt. Das Team oder Unternehmen, das für Disp zuständig ist, hat ansonsten völlige Freiheit bei der Auswahl von Namen. Falls sowohl Dbms als auch Disp eine Objektklasse mit dem Namen Subscriber einführen müssen, ergeben sich Paketnamen wie die folgenden:
Disp_Subscriber Disp_Subscriber_Utilities Disp_Subscriber_Defs Dbms_Subscriber Dbms_Subscriber_Interface Dbms_Subscriber_Defs
Die strikte Verwendung festgelegter Datentypen in Ada verhindert die Vermischung verschiedener Datentypen. Konzeptionell unterschiedliche Typen müssen
als verschiedene benutzerdefinierte Typen realisiert werden. Durch Subtypen können Sie die Lesbarkeit des Programms verbessern und die Effektivität der vom Compiler generierten Laufzeitüberprüfungen
erhöhen.
Führen Sie in Aufzählungen, wo immer dies möglich ist, einen gesonderten Literalwert für einen nicht initialisierten Wert, einen ungültigen Wert
oder keine vorhandenen Werte ein.
type Mode is (Undefined, Circular, Sector, Impulse); type Error is (None, Overflow, Invalid_Input_Value,Ill-formed_Name);
Damit werden die Regeln für die systematische Initialisierung von Objekten unterstützt. Stellen Sie dieses Literal an den Anfang und nicht an das Ende der Liste, um die Wartung zu erleichtern und angrenzende Teilbereiche gültiger Werte zu ermöglichen:
subtype Actual_Error is Error range Overflow .. Error'Last;
Vermeiden Sie die Verwendung vordefinierter
numerischer Typen.
Wenn Sie sich ein hohes Maß an Portierbarkeit und Wiederverwendbarkeit zum Ziel gesetzt haben oder eine Kontrolle des von numerischen Objekten belegten Speicherbereichs erforderlich ist, dürfen keine vordefinierten numerischen Typen (aus dem Paket 'Standard') verwendet werden. Diese Forderung ergibt sich daraus, dass die Merkmale der vordefinierten Typen Integer und Float im Referenzhandbuch zur Programmiersprache Ada [ISO87] (absichtlich) nicht angegeben sind.
Eine erste systematische Strategie sieht die Einführung
projektspezifischer numerischer Typen vor, z. B. in einem Paket
System_Types, deren Namen auf die Genauigkeit oder Speichergröße hinweisen:
package System_Types is type Byte is range -128 .. 127; type Integer16 is range -32568 .. 32567; type Integer32 is range ... type Float6 is digits 6; type Float13 is digits 13; ... end System_Types;
Erstellen Sie keine neuen Definitionen für
Standardtypen (aus dem Paket 'Standard').
Geben Sie nicht an, von welchem Basistyp die numerischen Typen
abgeleitet werden sollen. Überlassen Sie die Auswahl des Typs dem Compiler.
Hier sehen Sie ein negatives Beispiel:
type Byte is new Integer range -128 .. 127;
Der Name Float6 ist besser
als Float32, selbst wenn 32-Bit-Floats auf den meisten Maschinen eine Genauigkeit von sechs Stellen erreichen.
Leiten Sie in den verschiedenen Abschnitten des Projekts Typen ab, deren Namen
aussagekräftiger als die in
Baty_System_Types sind. Einige der exaktesten Typen können als nicht öffentliche Typen definiert werden, um eine eventuelle Portierung auf ein Ziel mit beschränkter Genauigkeitsunterstützung zu ermöglichen.
Diese Strategie ist in folgenden Fällen anzuwenden:
In allen anderen Fällen kommt eine andere, einfachere Strategie zum Tragen: Definieren Sie immer neue Typen, indem Sie den erforderlichen Bereich und die erforderliche Genauigkeit angeben. Definieren Sie jedoch nie den Basistyp, von dem diese abgeleitet werden sollen. Deklarieren Sie beispielsweise Folgendes:
type Counter is range 0 .. 100; type Length is digits 5;
Diese Deklaration ist der folgenden vorzuziehen.
type Counter is new Integer range 1..100; -- Ein 64-Bit-Integer wäre möglich. type Length is new Float digits 5; -- 13 Stellen wären möglich.
Bei dieser zweiten Strategie ist der Programmierer gezwungen, über die für jeden Typ erforderlichen exakten Grenzen und die erforderliche Genauigkeit jedes Typs nachzudenken. Es reicht nicht aus, willkürlich eine bestimmte Anzahl von Bits zu wählen. Beachten Sie jedoch, dass der Compiler bei einer Abweichung des Bereichs von dem des Basistyps systemtische Bereichsprüfungen durchführt. Im obigen Beispiel würde eine solche Prüfung für den Typ Counter durchgeführt werden, wenn der Basistyp ein 32-Bit-Integer ist.
Falls die Bereichsprüfungen zu einem Problem werden,
können Sie sie mit folgender Deklaration vermeiden:
type Counter_Min_Range is range 0 .. 10_000; type Counter is range Counter_Min_Range'Base'First .. Counter_Min_Range'Base'Last;
Vermeiden Sie, dass Standardtypen durch Konstrukte wie Schleifen, Indexbereiche usw. in den Code
gelangen.
Subtypen der vordefinierten numerischen Typen werden nur unter folgenden Umständen verwendet:
Beispiel:
for I in 1 .. 100 loop ... -- I ist vom Typ Standard.Integer. type A is array (0 .. 15) of Boolean; -- Der Index ist Standard.Integer.
Verwenden Sie stattdessen dieses Format:
Some_Integer range L .. H for I in Counter range 1 .. 100 loop ... type A is array (Byte range 0 .. 15) of Boolean;
Versuchen Sie nicht, Typen ohne Vorzeichen zu implementieren.
In Ada gibt es keine ganzzahligen Typen mit vorzeichenloser Arithmetik. Gemäß Sprachdefinition werden alle ganzzahligen Typen nicht oder nur indirekt von den vordefinierten Typen abgeleitet, die ihrerseits symmetrisch zu null sein müssen.
Aus Gründen der Portierbarkeit sollten Sie nur auf reale Typen
zurückgreifen, deren Werte in den folgenden Bereichen liegen:
[-F'Large .. -F'Small] [0.0] [F'Small .. F'Large]
Beachten Sie, dass F'Last und F'First unter Umständen keine Modellzahlen sind und nicht einmal in einem Modellintervall enthalten sein müssen. Die relative Position von F'Last und F'Large hängt von der Typdefinition und der zugrunde liegenden Hardware ab. Ein besonders tückisches Beispiel ist der Fall, bei dem 'Last eines Festkommatyps nicht zum Typ gehört:
type FF is delta 1.0 range -8.0 .. 8.0;
Streng genommen besagt das Ada-Referenzhandbuch [3.5.9(6)], dass FF'Last = 8.0 nicht zum Typ gehören kann.
Verwenden Sie für die Darstellung großer oder kleiner realer Zahlen die Attribute 'Large oder 'Small (und deren negative Entsprechungen), nicht jedoch 'First und 'Last wie bei ganzzahligen Typen.
Verwenden Sie für
Gleitkommatypen nur <= und >=, in keinem Fall aber =, <, >, /=.
Die Semantik absoluter Vergleiche ist schlecht definiert (Gleichheit der Darstellung und Ungleichheit beim erforderlichen Genauigkeitsgrad). X < Y muss beispielsweise nicht zwingend zu demselben Ergebnis führen wie not (X >= Y). Tests auf Gleichheit (A = B) sollten wie folgt ausgedrückt werden:
abs (A - B) <= abs(A)*F'Epsilon
Zur Verbesserung der Lesbarkeit und der Wartungsfreundlichkeit wäre es ratsam, einen Ist-gleich-Operator zu verwenden, um den obigen Ausdruck zu kapseln.
Beachten Sie, dass der folgende einfachere Ausdruck nur gültig ist, wenn der Wert von A und B klein ist:
abs (A - B) <= F'Small
Dieser einfache Ausdruck kann daher nicht allgemein empfohlen werden.
Vermeiden Sie jeden Verweis auf die vordefinierte
Ausnahme Numeric_Error. Gemäß einer als bindend geltenden Interpretation des
Ada-Board wird jetzt in allen Fällen, die bisher die Ausnahme Numeric_Error ausgelöst haben,
Constraint_Error ausgelöst. In Ada 95 wird die Ausnahme Numeric_Error nicht mehr verwendet.
Sollte die Implementierung noch immer die Ausnahme
Numeric_Error auslösen (wie es beim nativen Rational-Compiler der Fall ist),
sollten Sie in derselben Alternative einer Ausnahmebehandlungsroutine eine Überprüfung auf Constraint_Error und
Numeric_Error vorsehen:
when Numeric_Error | Constraint_Error => ...
Vermeiden Sie
Unterlauf.
Unterlauf wird in Ada nicht erkannt. Das Ergebnis ist 0.0, und es wird keine Ausnahme ausgelöst. Sie können eine explizite Überprüfung auf Unterlauf ausführen, indem Sie testen, ob das Ergebnis einer Multiplikation oder Division 0.0 ist, wenn keiner der Operanden 0.0 ist. Sie können auch eigene Operatoren implementieren, um diese Überprüfung automatisch durchzuführen, was jedoch zu Lasten der Effizienz geht.
Festkommatypen können nur eingeschränkt verwendet werden.
Verwenden Sie weitestgehend Gleitkommatypen. Eine unangemessene Verwendung von Festkommatypen in einer Ada-Implementierung kann zu Problemen bei der Portierbarkeit führen.
Bei Festkommatypen
sollte 'Small gleich 'Delta sein.
Dies muss aus dem Code hervorgehen. Die Tatsache, dass die Standardauswahl für 'Small eine Zweierpotenz ist, führt zu den verschiedensten Problemen. Die folgende Schreibweise ist eine Möglichkeit, die Auswahl für 'Small zu verdeutlichen:
Fx_Delta : constant := 0.01; type FX is delta Fx_Delta range L .. H; for FX'Small use Fx_Delta;
Falls keine Längenklauseln für Festkommatypen unterstützt werden, lässt sich diese Regel nur befolgen, indem explizit ein 'Delta angegeben wird, das eine Zweierpotenz ist. Bei Subtypen kann 'Small von 'Delta verschieden sein. (Die Regel gilt nur für die Typdefinition bzw., laut Terminologie des Ada-Referenzhandbuchs, für den "ersten benannten Subtyp".)
Geben Sie für die Komponenten eines Satztyps nach Möglichkeit statische Anfangswerte an. (Häufig
können Werte wie
'First oder 'Last verwendet werden.)
Wenden Sie diese Regel jedoch nicht auf Diskriminanten an. Nach den Regeln der Sprache haben Diskriminanten immer Werte. Veränderliche Datensätze (d. h. Datensätze mit Standardwerten für Diskriminanten) sollten nur eingeführt werden, wenn Veränderlichkeit als Merkmal erwünscht ist. In allen anderen Fällen erfordern veränderliche Datensätze zusätzliche Hauptspeicherkapazität (häufig wird die umfangreichste Variante zugeordnet) und mehr Zeit (komplexere Variantenprüfungen).
Vermeiden Sie Funktionsaufrufe in den Standardanfangswerten aller Komponenten, da diese zu einem
Fehler
"Access before Elaboration" (Zugriff vor Ausarbeitung) führen (siehe Abschnitt "Programmstruktur und
Kompilierungsprobleme").
Wenn bei veränderlichen Datensätzen (deren Diskriminanten Standardwerte haben) eine Diskriminante
für die Dimensionierung einer anderen Komponente verwendet wird, sollte die Diskriminante als Subtyp mit einem angemessen kleinen Bereich angegeben werden.
Beispiel:
type Record_Type (D : Integer := 0) is record S : String (1 .. D); end record; A_Record : Record_Type;
Dieses Beispiel wird in den meisten Implementierung Storage_Error auslösen. Geben Sie für den Subtyp der Diskriminanten D einen angemessenen Bereich an.
Gehen Sie bezüglich des physischen Layouts von Datensätzen von keinen
Annahmen aus.
Komponenten müssen (im Gegensatz zu anderen Programmiersprachen) in der Reihenfolge dargestellt werden, die in der Definition festgelegt ist.
Verwenden Sie Zugriffstypen nur eingeschränkt.
Dies gilt insbesondere für Anwendungen, die permanent auf kleinen Maschinen ohne virtuellen Speicher ausgeführt werden sollen. Zugriffstypen sind nicht ungefährlich, denn kleine Programmierungsfehler können dazu führen, dass kein Speicher verfügbar ist. Aber selbst bei guter Programmierung ist eine Fragmentierung des Speichers möglich. Zugriffstypen sind außerdem langsamer. Die Verwendung von Zugriffstypen muss im Rahmen einer Strategie für das gesamte Projekt erfolgen. Diese Strategie sollte die Überwachung von Objektgruppen und deren Größe sowie von Zuordnungs- und Freigabepunkten vorsehen. Der für einen Zugriffstyp gewählte Name sollte dessen Natur deutlich machen. Wählen Sie Pointer oder einen Namen mit dem Suffix _Pointer.
Ordnen Sie Objektgruppen während der Ausarbeitung des Programms zu und geben Sie systematisch die Größe
der einzelnen Objektgruppen an.
Der (in Speichereinheiten angegebene) Wert kann statisch sein oder dynamisch berechnet werden (z. B. aus einer Datei gelesen werden). Der Grund für diese Regel ist, dass ein Programm gleich beim Start scheitern sollte und nicht erst ungewisse Zeit später. Für generische Pakete kann zu diesem Zweck eine zusätzliche generische Formel verwendet werden, die die Größe angibt.
Beachten Sie, dass für jedes zugeordnete Objekt oft ein gewisser Mehrbedarf an Speicher besteht. Manchmal ordnen die Laufzeiten auf dem Zielsystem jedem Speicherblock Zusatzinformationen zur internen Verwaltung zu. Für das Speichern einer Objektgruppe mit N Objekten, die jeweils eine Größe von M Speichereinheiten haben, müssen also gegebenenfalls mehr als N * M Speichereinheiten zugeordnet werden, beispielsweise N * (M + K). Den Wert K für diesen zusätzlichen Bedarf können Sie Anhang F der Veröffentlichung [ISO87] entnehmen oder durch Experimente ermitteln.
Kapseln Sie die Verwendung von Zuordnungen
(Ada-Basiselement new) und Freigaben. Sie sollten sich nicht auf
Unchecked_Deallocation verlassen, sondern eine interne Liste der freien Blöcke führen, sofern dies machbar ist.
Falls eine rekursive Datenstruktur mit einem Zugriffstyp implementiert wird, wird dieser sehr wahrscheinlich auf einen Satztyp zugreifen, der (als eine Komponente) denselben Zugriffstyp hat. Auf diese Weise können freie Zellen zu einer Liste freier Blöcke zusammengeführt und erneut genutzt werden. Dafür ist kein zusätzlicher Speicherplatz erforderlich (außer für den Zeiger auf den Kopfsatz der Liste).
Von new ausgelöste Storage_Error-Ausnahmen sollten Sie explizit behandeln und eine aussagefähigere Ausnahme exportieren, die angibt, dass die maximale Speichergröße für die Objektgruppe nicht verfügbar ist.
Bei nur einem Zuordnungs- und Freigabepunkt ist im Falle eines Fehlers eine leichtere Trace- und Debug-Verarbeitung möglich.
Geben Sie nur zugeordnete Zellen derselben Größe (und also mit derselben Diskriminante)
frei.
Dies ist wichtig, um die Fragmentierung des Speichers zu vermeiden. Unchecked_Deallocation wird höchstwahrscheinlich keinen Dienst für die Speicherverdichtung bereitstellen. Sie sollten überprüfen, ob das Laufzeitsystem in der Lage ist, freigegebene benachbarte Blöcke zu verschmelzen.
Stellen Sie systematisch eine primitive Funktion
Destroy (oder Free oder Release) für Zugriffstypen bereit.
Dies ist besonders für abstrakte Datentypen wichtig, die mit Zugriffstypen implementiert werden. Sie sollten sich systematisch daran halten, wenn Sie mehrere dieser Typen kombinieren möchten.
Geben Sie systematisch Objekte frei.
Versuchen Sie, die Aufrufe für Zuordnungen und Freigaben einander zuzuordnen, um sicherzustellen, dass alle zugeordneten Daten wieder freigegeben werden. Versuchen Sie, Daten in dem Bereich freizugeben, in dem Sie zugeordnet wurden. Vergessen Sie nicht die Freigabe beim Auftreten von Ausnahmen. Dies ist einer der Fälle, in denen ein alternatives Konstrukt when others verwendet werden sollte, das mit einer Anweisung raise endet.
Die bevorzugte Strategie lautet "Abrufen - Verwenden - Freigeben". Das Programm ruft die Objekte ab (wobei eine dynamische Struktur erstellt wird), verwendet die Objekte dann und muss sie schließlich wieder freigeben. Stellen Sie sicher, dass diese drei Operationen im Code klar bezeichnet sind und dass an allen möglichen Rahmen-Exits (einschließlich Ausnahmen) eine Freigabe erfolgt.
Achten Sie darauf, die temporären zusammengesetzten Datenstrukturen freizugeben, die in Datensätzen enthalten
sein können.
Beispiel:
type Object is record Field1: Some_Numeric; Field2: Some_String; Field3: Some_Unbounded_List; end record;
'Some_Unbounded_List' ist hier eine zusammengesetzte verlinkte Struktur (eine Struktur, die sich aus einer Reihe verlinkter Objekte zusammensetzt). Schauen wir uns nun eine typische Attributfunktion an:
function Some_Attribute_Of(The_Object: Object_Handle) return Boolean is Temp_Object: The_Object; begin Temp_Object := Read(The_Object); return Temp_Object.Field1 < Some_Value; end Some_Attribute_Of;
Die zusammengesetzte Struktur, die beim Einlesen des Objekts in Temp_Object implizit im Heap-Speicher erstellt wird, wird nie freigegeben, ist jetzt jedoch unerreichbar. Dies ist ein Speicherleck. Für so aufwendige Strukturen wäre die Implementierung des Paradigmas 'Anfordern - Verwenden - Freigeben' eine saubere Lösung. Ihr Client sollte das Objekt also zunächst anfordern, es dann wie erforderlich verwenden und anschließend freigeben:
procedure Get (The_Object : out Object; With_Handle : in Object_Handle); function Some_Attribute_Of(The_Object : Object) return Some_Value; function Other_Attribute_Of(The_Object : Object) return Some_Value; ... procedure Release(The_Object: in out Object);
Der Clientcode könnte wie folgt aussehen:
declare My_Object: Object; begin Get (My_Object, With_Handle => My_Handle); ... Do_Something (The_Value => Some_Attribute_Of(My_Object)); ... Release(My_Object); end;
Deklarieren Sie Typen immer dann als
privat, wenn es zum Verbergen von Implementierungsdetails erforderlich ist.
In folgenden Fällen müssen Implementierungsdetails mit einem privaten Typ verdeckt werden:
In der Rational-Umgebung können private Typen in Verbindung mit geschlossenen privaten Komponenten und Subsystemen dazu betragen, die Auswirkungen einer eventuellen Änderung des Schnittstellendesigns so gering wie möglich zu halten.
Verwenden Sie private Typen nicht, wenn der entsprechende
vollständige Typ die bestmögliche Abstraktion ist, wie es bei der so genannten "reinen" objektorientierten Programmierung üblich ist. Seien Sie pragmatisch. Lassen Sie sich von der Frage leiten, ob
die Deklaration eines Typs als private einen Vorteil bringt.
Ein mathematischer Vektor sollte beispielsweise als ein Array oder ein Punkt auf einer Ebene als ein Datensatz dargestellt werden und nicht als privater Typ.
type Vector is array (Positive range <>) of Float; Type Point is record X, Y : Float := Float'Large; end record;
Die Array-Indizierung, die Auswahl der Satzkomponenten und die Aggregatschreibweise sind weit besser lesbar (und unter Umständen sogar effizienter) als eine Reihe von Unterprogrammaufrufen, die bei unnötigerweise privaten Typen erforderlich wären.
Deklarieren Sie private Typen
als limited, wenn die Standardzuordnung oder der Vergleich der eigentlichen Objekte und Werte ohne Bedeutung, nicht intuitiv oder unmöglich ist.
Dies trifft in folgenden Situationen zu:
Ein begrenzter privater Typ sollte sich selbst
initialisieren.
Eine Objektdeklaration eines solchen Typs muss einen angemessenen Anfangswert empfangen, da die spätere Zuordnung eines Wertes im Allgemeinen nicht möglich ist, ohne das Auslösen einer Ausnahme während eines Unterprogrammaufrufs zu riskieren.
Geben Sie für begrenzte Typen eine Copy-Prozedur (oder Assign-Prozedur) und eine
Destroy-Prozedur an, wo immer dies machbar und sinnvoll ist.
Sie können den Bedienungskomfort einer generischen Einheit verbessern, indem Sie
die formalen Typen der Einheit als begrenzte private Typen angeben, solange intern keine Gleichheit oder Zuordnung erforderlich ist.
In Verbindung mit der vorherigen Regel können Sie dann eine formale generische Copy- und Destroy-Prozedur importieren und, falls sinnvoll, ein Prädikat Are_Equal.
Geben Sie in der Spezifikation für formale generische private
Typen an, ob der entsprechende tatsächliche Typ beschränkt sein muss oder nicht.
Hierzu können Sie eine Namenskonvention und/oder einen Kommentar verwenden:
generic -- muss beschränkt sein type Constrained_Element is limited private; package ...
Alternativ dazu können Sie das von Rational definierte Pragma Must_Be_Constrained
verwenden:
generic type Element is limited private; pragma Must_Be_Constrained (Element); package ...
Wenn Sie einen Typ ableiten, werden auch alle (ableitbaren) Unterprogramme abgeleitet, die in demselben Abschnitt wie der übergeordnete
Typ deklariert sind. Es ist daher nicht sinnvoll, sie alle im Deklarationsabschnitt des abgeleiteten Typs
neu als Oberflächen zu definieren. Generische Unterprogramme können jedoch nicht abgeleitet werden und müssen daher möglicherweise neu als Oberflächen definiert werden.
Beispiel:
package Base is type Foo is record ... end record; procedure Put(Item: Foo); function Value(Of_The_Image: String) return Foo; end Base; with Base; package Client is type Bar is new Foo; -- An dieser Stelle sind die folgenden Deklarationen -- implizit: -- -- function "="(L,R: Bar) return Boolean; -- -- procedure Put(Item: bar); -- function Value(Of_The_Image: String) return Bar; -- end Client;
Es ist daher nicht notwendig, diese Operationen als Oberflächen neu zu definieren. Beachten Sie jedoch, dass generische Unterprogramme (wie passive Iteratoren) nicht zusammen mit anderen Operationen abgeleitet werden und daher als Oberflächen neu exportiert werden müssen. Unterprogramme, die nicht zusammen mit der Spezifikation, die die Deklaration des Basistyps enthält, definiert sind, können gleichfalls nicht abgeleitet werden und müssen als Oberflächen neu exportiert werden.
Geben Sie in Objektdeklarationen Anfangswerte an, sofern das Objekt kein selbstinitialisierendes
Objekt ist und es keinen impliziten Standardanfangswert gibt (wie es beispielsweise bei Zugriffstypen, Task-Typen oder Datensätzen mit Standardwerten für Nichtdiskriminantenfelder der Fall ist).
Der zugeordnete Wert muss ein realer und sinnvoller Wert sein, nicht ein beliebiger Wert für den Typ. Wenn der tatsächliche Anfangswert verfügbar ist, z. B. einer der Eingabeparameter, ordnen Sie diesen zu. Falls es nicht möglich ist, einen sinnvollen Wert zu berechnen, sollten Sie das Objekt später deklarieren oder, sofern verfügbar, einen NIL-Wert zuordnen.
Ein NIL-Wert bedeutet, dass das Objekt
"nicht initialisiert" ist. Er wird für die Deklaration von Konstanten verwendet, die als ein
"nicht verwendbarer, aber bekannter Wert" genutzt werden können, der von Algorithmen kontrolliert zurückgewiesen werden kann.
Der NIL-Wert sollte, soweit möglich, ausschließlich für die Initialisierung verwendet werden, so dass sein Auftauchen immer ein Hinweis auf einen Fehler durch eine nicht initialisierte Variable ist.
Es ist nicht immer möglich, für alle Typen einen NIL-Wert zu deklarieren. Dies gilt insbesondere für modulare Typen, z. B. für eine Winkel. Wählen Sie in diesem Fall den am wenigsten wahrscheinlichen Wert.
Beachten Sie, dass Code zur Initialisierung großer Datensätze aufwendig sein kann, besonders, wenn
es Varianten des Datensatzes gibt und ein Anfangswert nicht statisch ist (oder noch präziser, wenn der Wert in der Kompilierzeit nicht berechnet werden kann). Manchmal ist es effizienter, einen immer geltenden Anfangswert festzulegen (z. B. in dem Paket, das
den Typ definiert), und diesen explizit zuzuordnen.
R : Some_Record := Initial_Value_For_Some_Record;
Anmerkung:
Die Erfahrung zeigt, dass nicht initialisierte Variablen eine der Hauptfehlerquellen beim Portieren von Code sind und eine der Hauptursachen für Programmierfehler. Diese Situation verschärft sich, wenn der Entwicklungshost dem Programmierer "entgegenkommt" und zumindest für einige der Objekte Standardwerte bereitstellt (z. B. für den Typ Integer im nativen Rational-Compiler), oder wenn das Zielsystem vor dem Programmladen Nullen in den Speicher schreibt (wie es beispielsweise bei einer DEC VAX der Fall ist). Wenn es Ihnen um Portierbarkeit geht, sollten Sie stets das Schlimmste annehmen.
Auf die Zuordnung eines Anfangswertes in der
Deklaration kann verzichtet werden, wenn sie aufwendig ist und dem Objekt vor dessen Verwendung offensichtlich ein Wert zugeordnet wird.
Beispiel:
procedure Schmoldu is Temp : Some_Very_Complex_Record_Type; -- später initialisiert begin loop Temp := Some_Expression ... ...
Vermeiden Sie die Verwendung von Literalwerten
im Code.
Verwenden Sie Konstanten (mit einem Typ), wenn der definierte Wert an einen Typ gebunden ist. Andernfalls sollte Sie benannte Zahlen verwenden, insbesondere für alle reinen (dimensionslosen) Werte.
Earth_Radius : constant Meter := 6366190.7; -- in Metern Pi : constant := 3.141592653; -- keine Einheit
Definieren Sie zusammengehörige Konstanten
mit universellen statischen Ausdrücken.
Bytes_Per_Page : constant := 512; Pages_Per_Buffer : constant := 10; Buffer_Size : constant := Bytes_Per_Page * Pages_Per_Buffer; Pi_Over_2 : constant := Pi / 2.0;
Hier wird der Fakt genutzt, dass diese Ausdrücke in der Kompilierzeit exakt berechnet werden müssen.
Deklarieren Sie keine Objekte mit anonymen Typen. Weitere Informationen hierzu finden Sie im Abschnitt 3.3.1 des Ada-Referenzhandbuchs.
Solche Deklarationen senken die Wartungsfreundlichkeit, bewirken, dass Objekte nicht als Parameter übergeben werden können, und führen oft zu Fehlern durch Typkonflikte.
Unterprogramme können als Prozeduren oder Funktionen deklariert werden. Anhand der folgenden allgemeinen Kriterien
können Sie entscheiden, welche Form der Deklaration angemessen ist.
Funktionsdeklaration:
Prozedurdeklaration:
Vermeiden Sie Standardwerte für formale generische Parameter, die für Dimensionierungsstrukturen (Tabellen, Objektsammlungen usw.)
verwendet werden.
Schreiben Sie lokale Prozeduren mit möglichst wenig Nebeneffekten und Funktionen ganz ohne
Nebeneffekte. Dokumentieren Sie die Nebeneffekte.
Nebeneffekte sind in der Regel Modifikationen globaler Variablen und werden manchmal nur beim Lesen des Hauptteils des Unterprogramms bemerkt. Der Programmierer ist sich unter Umständen nicht darüber im Klaren, dass es an der Aufrufstelle zu Nebeneffekten kommt.
Der Code ist stabiler, besser verständlich und weniger von seinem Inhalt abhängig, wenn die erforderlichen Objekte als Parameter übergeben werden.
Diese Regel gilt vor allem für lokale Unterprogramme. Exportierte Unterprogramme benötigen häufig legitimen Zugriff auf globale Variablen im Pakethauptteil.
Verbundausdrücke werden durch paarige runde Klammern
deutlicher.
Die Verschachtelungsebene eines Ausdrucks definiert die Anzahl verschachtelter paariger Klammern, die für die Auswertung eines Ausdrucks von links nach rechts erforderlich sind, wenn die Vorrangregeln für Operatoren ignoriert werden.
Beschränken Sie Ausdrücke auf vier Verschachtelungsebenen.
Satzaggregate sollten benannte Zuordnungen verwenden und qualifiziert sein.
Subscriber.Descriptor'(Name => Subscriber.Null_Name, Mailbox => Mailbox.Nil, Status => Subscriber.Unknown, ...);
Die Verwendung von
when others ist für Satzaggregate untersagt.
Dies liegt daran, dass Datensätze im Gegensatz zu Arrays von Natur als heterogene Strukturen sind. Eine uniforme Zuordnung ist daher unvernünftig.
Verwenden Sie für einfache Prädikate an Stelle der Anweisungen
"if...then...else" Boolesche Ausdrücke.
function Is_In_Range(The_Value: Value; The_Range: Range) return Boolean is begin if The_Value >= The_Range.Min and The_Value <= The_Range.Max then return True; end if; end Is_In_Range;
Es wäre besser, folgende Schreibweise zu verwenden:
function Is_In_Range(The_Value: Value; The_Range: Range) return Boolean is begin return The_Value >= The_Range.Min and The_Value <= The_Range.Max; end Is_In_Range;
Komplexe Ausdrücke mit zwei oder mehr if-Anweisungen, sollten nicht auf diese Weise geändert werden, wenn dadurch die Lesbarkeit beeinträchtigt wird.
Schleifenanweisungen sollten in folgenden Fällen
Namen haben:
Forever: loop ... end loop Forever;
Wenn eine Schleife einen Namen hat, sollte eine enthaltene exit-Anweisung diesen Namen angeben.
Schleifen, für die am Anfang eine Überprüfung des Beendigungsstatus erforderlich ist,
sollten die Form einer "while"-Schleife haben. Schleifen, bei denen der Beendigungsstatus an anderer Stelle überprüft werden muss, sollten die allgemeine Form und eine exit-Anweisung haben.
Minimieren Sie die Zahl der exit-Anweisungen in einer Schleife.
Wenden Sie in einer
"for"-Schleife, die über einem Array iteriert, das Attribut 'Range für das Array-Objekt und nicht einen expliziten Bereich oder einen anderen
Subtyp.
Von der Schleife unabhängiger Code sollte nicht in der Schleife stehen. Obwohl das "Code Hoisting" (Verschieben von Ausdrücken an den
spätestmöglichen Ausführungszeitpunkt) eine allgemeine Art der Compileroptimierung ist, kann es nicht angewendet werden,
wenn der invariante Code andere Kompiliereinheiten aufruft.
Beispiel:
World_Search: while not World.Is_At_End(World_Iterator) loop ... Country_Search: while not Nation.Is_At_End(Country_Iterator) loop declare City_Map: constant City.Map := City.Map_Of (The_City => Nation.City_Of(Country_Iterator), In_Atlas => World.Country_Of(World_Iterator).Atlas); begin ...
Der Aufruf von "World.Country_Of" im obigen Code ist von der Schleife unabhängig (d. h., das Land - country - wird in der inneren Schleife nicht geändert). In den meisten Fällen kann/darf der Compiler den Aufruf nicht aus der Schleife an eine andere Stelle verschieben, weil der Aufruf Nebeneffekte haben könnte, die die Programmausführung beeinflussen. Der Code wird daher durch die Schleife jedes Mal unnötigerweise ausgeführt.
Wenn Sie die Schleife wie folgt neu schreiben, ist sie effizienter und leichter zu verstehen und zu warten:
Country_Search: while not World.Is_At_End(World_Iterator) loop declare This_Country_Atlas: constant Nation.Atlas := World.Country_Of (World_Iterator).Atlas; begin ... City_Search: while not Nation.Is_At_End (The_City_Iterator) loop declare City.Map_Of ( The_City => Nation.City_Of (Country_Iterator), In_Atlas => This_Country_Atlas ); begin ...
Unterprogramm- und Eingangsaufrufe sollten benannte Zuordnungen
verwenden.
Wenn jedoch klar ist, dass der Schwerpunkt der Operation beim ersten (oder einzigen) Parameter liegt (z. B. dem direkten Objekt eines transitiven Verbs), kann der Name für diesen (und nur diesen) Parameter weggelassen werden.
Subscriber.Delete (The_Subscriber => Old_Subscriber);
Hier ist Subscriber.Delete das transitive Verb und Old_Subscriber das direkte Objekt. Die folgenden Ausdrücke ohne die benannte Zuordnung The_Subscriber => Old_Subscriber sind akzeptabel:
Subscriber.Delete (Old_Subscriber); Subscriber.Delete (Old_Subscriber, Update_Database => True, Expunge_Name_Set => False); if Is_Administrator (Old_Subscriber) then ...
Es gibt auch Fälle, in denen die Bedeutung von Parametern so offensichtlich ist, das benannte Zuordnungen lediglich die Lesbarkeit beeinträchtigen würden. Dies trifft beispielsweise zu, wenn alle Parameter denselben Typ und Modus und keine Standardwerte haben.
if Is_Equal (X, Y) then ... Swap (U, B);
In case-Anweisungen oder in Satztypdefinitionen (für Varianten) sollte kein when
others verwendet werden.
Der Verzicht auf when others hilft in der Wartungsphase, denn diese Konstrukte werden ungültig, sobald die diskrete Typdefinition modifiziert wird. Der Programmierer muss sich dann überlegen, wie die Modifizierung gehandhabt werden soll. Das Konstrukt when others wird jedoch toleriert, wenn der Selektor ein großer Integer-Bereich ist.
Verwenden Sie an Stelle mehrerer
"elsif" eine case-Anweisung, wenn die Verzweigungsbedingung ein diskreter Wert ist.
Unterprogramme sollten nur einen Rückkehrpunkt
haben.
Versuchen Sie, Unterprogramme am Ende des Anweisungsabschnitts zu verlassen. Funktionen sollten nicht mehr als eine Rückkehranweisung haben. Frei über den Hauptteil der Funktion verteilte Rückkehranweisungen sind wie goto-Anweisungen und machen den Code schwer lesbar und schwer zu warten.
Prozeduren sollten gar keine Rückkehranweisung haben.
Mehrere return
können nur in sehr kleinen Funktionen toleriert werden, wenn alle return gleichzeitig erkennbar sind und der Code eine sehr regelmäßige
Struktur hat.
function Get_Some_Attribute return Some_Type is begin if Some_Condition then return This_Value; else return That_Other_Value; end if; end Get_Some_Attribute;
Die Anweisung goto kann nur eingeschränkt verwendet werden.
Die Anweisung "goto" ist trotz der Syntax von goto-Kennsätzen und der eingeschränkten Verwendungsbedingungen in Ada nicht so nachteilig wie man meinen könnte. In vielen Fällen ist goto besser lesbar und aussagekräftiger als manche äquivalenten Konstrukte (ein mit einer Ausnahme erstelltes imitiertes goto beispielsweise).
Wenn Sie Arrays manipulieren, gehen Sie nicht davon aus, dass der Index der Arrays mit
1 beginnt. Verwenden Sie die Attribute
'Last, 'First und 'Range.
Definieren Sie die gebräuchlichsten eingeschränkten Subtypen Ihres uneingeschränkten Typs (es werden größtenteils Datensätze sein)
und verwenden Sie diese Subtypen für Parameter und Rückgabewerte, um die Selbstüberprüfung im Clientcode zu
verstärken.
type Style is (Regular, Bold, Italic, Condensed); type Font (Variety: Style) is ... subtype Regular_Font is Font (Variety => Regular); subtype Bold_Font is Font (Variety => Bold); function Plain_Version (Of_The_Font: Font) return Regular_Font; procedure Oblique (The_Text : in out Text; Using_Font : in Italic_Font); ...
Folgende Richtlinien sollten angewendet werden:
Überladen Sie Unterprogramme.
Stellen Sie jedoch bei Verwendung desselben Bezeichners sicher, dass er tatsächlich dieselbe Art von Operation impliziert.
Vermeiden Sie ein Verdecken von Homographbezeichnern in verschachtelten Geltungsbereichen.
Dies führt zur Irritation des Lesers und zu potenziellen Risiken bei der Wartung. Achten Sie auch auf das Vorhandensein und den Geltungsbereich von Steuervariablen von "for"-Schleifen.
Überladen Sie nicht Operationen für Subtypen, sondern nur Operationen für den
Typ.
Diese Überladung gilt für den Basistyp und alle seine Subtypen.
Beispiel:
subtype Table_Page is Syst.Natural16 range 0..10; function "+"(Left, Right: Table_Page) return Table_Page;
Beim Abgleich von Unterprogrammen sucht der Compiler nach dem Basistyp und nicht nach dem Subtyp eines Parameters. Im obigen Beispiel wird "+" daher tatsächlich für alle Natural16-Werte des aktuellen Pakets und nicht nur für Table_Page neu definiert. Demzufolge würde jeder Ausdruck "Natural16 + Natural16" jetzt einem Aufruf von "+"(Table_Page, Table_Page) zugeordnet werden, der wahrscheinlich das falsche Ergebnis zurückgibt oder eine Ausnahme auslöst.
Minimieren Sie die Anzahl der von
"with"-Klauseln eingeführten Abhängigkeiten.
Wo die Transparenz durch eine "with"-Klausel verbessert werden kann, sollte die Klausel einen möglichst kleinen Codebereich abdecken. Verwenden Sie eine "with"-Klausel nur, wenn es erforderlich ist. Im Idealfall werden Sie sie nur in einem Hauptteil oder auch im Stub eines großen Hauptteils verwenden.
Verwenden Sie für den erneuten Export von Entitäten der tieferen Ebenen Schnittstellenpakete, um den sichtbaren Import einer Vielzahl von Paketen der tieferen Ebenen mit "with" zu vermeiden. Nutzen Sie abgeleitete Typen, Umbenennung, Oberflächenunterprogramme und eventuell vordefinierten Typen (z. B. Strings), wie sie bei Paketen mit Umgebungsbefehlen verwendet werden.
Verwenden Sie weiche (schwache) Einheitenverbindungen (mit formalen generischen Parameter) und keine harten (starken) Verbindungen (mit "with"-Klauseln).
"Use"-Klauseln sollten nicht verwendet werden.
Wenn Sie die Verwendung von "use"-Klauseln weitestgehend vermeiden, verbessern Sie die Lesbarkeit, sofern diese Regel durch Namenskonventionen unterstützt wird, die vom Kontext und einer geeigneten Umbenennung Gebrauch machen (siehe obigen Abschnitt "Namenskonventionen"). Auf diese Weise können Sie auch einige Überraschungen hinsichtlich der Transparenz vermeiden, insbesondere in der Wartungsphase.
Für ein Paket, das einen Zeichentyp definiert, ist eine "use"-Klausel in jeder Kompiliereinheit erforderlich, die auf der Basis dieses Zeichentyps Zeichenfolgenliterale definieren muss.
package Internationalization is type Latin_1_Char is (..., 'A', 'B', 'C', ..., U_Umlaut, ...); type Latin_1_String is array (Positive range <>) of Latin_1_Char; end Internationalization ; use Internationalization; Hello : constant Latin_1_String := "Baba"
Das Fehlen einer "use"-Klausel verhindert die Verwendung von Operatoren in Infix-Form. Diese können in der Clienteinheit umbenannt werden.
function "=" (X, Y : Subscriber.Id) return Boolean renames Subscriber."="; function "+" (X, Y :Base_Types.Angle) return Base_Types.Angle renames Base_Types."+";
Da das Fehlen einer
"use"-Klausel häufig dazu führt, dass dieselbe Gruppe von Umbenennungen in zahlreiche Clienteinheiten eingeschlossen wird, können
alle diese Umbenennungen durch ein im definierenden Paket verschachteltes Paket 'Operations' innerhalb des definierenden Pakets aufgeschlüsselt werden. In der Clienteinheit wird dann eine "use"-Klausel für das Paket 'Operations' empfohlen.
package Pack is type Foo is range 1 .. 10; type Bar is private; ... package Operations is function "+" (X, Y : Pack.Foo) return Pack.Foo renames Pack."+"; function "=" (X, Y : Pack.Foo) return Boolean renames Pack."="; function "=" (X, Y : Pack.Bar) return Boolean renames Pack."="; ... end Operations; private ... end Pack; with Pack; package body Client is use Pack.Operations; -- Macht NUR Operationen direkt sichtbar ... A, B : Pack.Foo; -- Präfix Pack noch immer erforderlich ... A := A + B ; -- Beachten Sie, dass "+" direkt -- sichtbar ist.
Das Paket 'Operations' sollte immer diesen Namen haben und immer an das Ende des sichtbaren Abschnitts des definierenden Pakets gestellt werden. Die "use"-Klausel sollte nur verwendet werden, wo es notwendig ist, d. h. im Hauptteil des Clients, sofern in der Spezifikation keine Operation verwendet wird, was oft der Fall ist.
with Defs; package Client is ... package Inner is use Defs; ... end Inner; -- Der Geltungsbereich der USE-Klausel endet hier. ... end Client; declare use Special_Utilities; begin ... end; -- Der Geltungsbereich der USE-Klausel endet hier.
Verwenden Sie Umbenennungsdeklarationen.
In Verbindung mit den Einschränkungen für "use"-Klauseln werden Umbenennungen empfohlen, um die Lesbarkeit des Codes zu verbessern. Wenn mehrfach auf eine Einheit mit einem sehr langen Namen verwiesen wird, kann die Lesbarkeit durch Angabe eines sehr kurzen Namens für diese Einheit verbessert werden.
with Directory_Tools; with String_Utilities; with Text_Io; package Example is package Dt renames Directory_Tools; package Su renames String_Utilities; package Tio renames Text_Io; package Dtn renames Directory_Tools.Naming; package Dto renames Directory_Tools.Object; ...
Die Auswahl von Kurznamen sollte innerhalb des gesamten Projekts konsistent sein, um dem Prinzip 'Möglichst wenig
Überraschungen' gerecht zu werden. Verwenden Sie daher den Kurznamen direkt im Paket.
package With_A_Very_Long_Name is package Vln renames With_A_Very_Long_Name; ... end with With_A_Very_Long_Name; package Example is package Vln renames With_A_Very_Long_Name; -- Ab hier ist Vln eine Abkürzung.
Beachten Sie, dass die Umbenennung von Paketen nur den sichtbaren Abschnitt des umbenannten Pakets transparent macht.
Importierte Paketumbenennungen müssen am Anfang des Deklarationsabschnitts gruppiert und
alphabetisch sortiert werden.
Umbenennungen können lokal überall dort verwendet werden, wo sie die Lesbarkeit verbessern.
(Dies hat keine negativen Auswirkungen auf die Laufzeit.) Typen können ohne Einschränkungen als Subtypen umbenennt werden.
Wie bereits im Abschnitt zu Kommentaren angegeben, sind Umbenennungen oft eine elegante und einfach zu verwaltende Möglichkeit, den Code zu dokumentieren, wenn Sie beispielsweise einem komplexen Objekt einen einfachen Namen geben oder die Bedeutung eines Typs lokal präzisieren möchten. Wählen Sie den Geltungsbereich des Umbenennungsbezeichners aus, um Unklarheiten zu vermeiden.
Durch das Umbenennen von Ausnahmen können Ausnahmen in mehreren Einheiten aufgeschlüsselt werden, z. B.
in allen Instanziierungen eines generischen Pakets. In einem Paket, das einen Typ ableitet, sollten Ausnahmen, die potenziell von den abgeleiteten Unterprogrammen ausgelöst werden, zusammen mit dem abgeleiteten Typ
erneut exportiert werden, damit die Clients
nicht das ursprüngliche Paket mit
"with" importieren müssen.
with Inner_Defs; package Exporter is ... procedure May_Raise_Exception; -- Raises exception Inner_Defs.Bad_Schmoldu when ... ... Bad_Schmoldu : exception renames Inner_Defs.Bad_Schmoldu; ...
Das Umbenennen von Unterprogrammen mit verschiedenen Standardwerten für
"in"-Parameter kann eine einfache Codeaufschlüsselung ermöglichen und die Lesbarkeit verbessern.
procedure Alert (Message : String; Beeps : Natural); procedure Bip (Message : String := ""; Beeps : Natural := 1) renames Alert; procedure Bip_Bip (Message : String := ""; Beeps : Natural := 2) renames Alert; procedure Message (Message : String; Beeps : Natural := 0) renames Alert; procedure Warning (Message : String; Beeps : Natural := 1) renames Alert;
Vermeiden Sie, den (alten) Namen der umbenannten Entität
im unmittelbaren Geltungsbereich der Umbenennungsdeklaration zu verwenden. Verwenden Sie nur den von der Umbenennungsdeklaration eingeführten Bezeichner oder das Operatorsymbol (neuer Name).
Seit vielen Jahren gibt es in der Ada-Community eine Kontroverse über "use"-Klauseln, die streckenweise an einem Glaubenskrieg gleichkommt. Beide Parteien haben verschiedene Argumente ins Feld geführt, die sich oft nicht auf große Projekte übertragen lassen, und Beispiele vorgebracht, die unrealistisch oder kalkuliert unfair sind.
Verfechter der "use"-Klausel stehen auf dem Standpunkt, dass die Klausel die Lesbarkeit verbessert. Sie bringen Beispiele für besonders schwer lesbare, lange und redundante Namen, die bei mehrfacher Verwendung von einer Umbenennung profitieren würden. Sie vertreten außerdem die Meinung, dass ein Ada-Compiler die Überladung auflösen kann. Das stimmt zwar, aber eine Person, die mit einem umfangreichen Ada-Programm beschäftigt ist, kann die Überladung nicht zu zuverlässig und mit Sicherheit nicht so schnell wie ein Compiler auflösen. Weiterhin gehen sie davon aus, dass hoch entwickelte Unterstützungsumgebungen für die Ada-Programmierung wie die Rational-Umgebung die explizit vollständig qualifizierten Namen überflüssig machen. Das ist jedoch nicht wahr. Der Benutzer sollte nicht für jeden Bezeichner, den er nicht sicher kennt, die Taste [Definition] drücken müssen. Der Benutzer sollte nicht raten müssen, sondern sofort sehen können, welche Objekte und Abstraktionen verwendet werden. Die Rosen-Anhänger leugnen die potenziellen Gefahren der "use"-Klausel bei der Programmwartung. Sie unterstellen, dass die Entstehung solcher Risiken auf die Unfähigkeit der Programmierer zurückzuführen ist. Wir denken, dass die Risiken durch vollständig qualifizierte Namen vermieden werden.
Falls Sie der Meinung sind, dass die Methoden, die oben vorgeschlagen wurden, um die Folgen der Einschränkungen für "use"-Klauseln zu entschärfen, einen zu hohen Tippaufwand erfordern, sollten Sie sich die Bemerkung von Norman H. Cohen zu eigen machen: "Die Zeit, die beim Tippen eines Programms eingespart wird, geht mehrfach verloren, wenn das Programm überprüft, mit Debuggern getestet und gewartet wird.".
Und schließlich hat sich gezeigt, dass das Fehlen von "use"-Klauseln bei großen Systemen die verbessern verkürzt, da der Suchaufwand in Symboltabellen reduziert wird.
Lesern, die sich detaillierter über die Kontroverse zur use-Klausel informieren möchten, empfehlen wir folgende Literatur:
D. Bryan, "Dear Ada", Ada Letters 7, 1, Januar/Februar 1987, Seiten 25-28
J. P. Rosen, "In Defense of the Use Clause", Ada Letters 7, 7, November/Dezember 1987, Seiten 77-81
G. O. Mendal, "Three Reasons to Avoid the Use Clause", Ada Letters 8, 1, Januar/Februar 1988, Seiten 52-57
R. Racine, "Why the Use Clause Is Beneficial", Ada Letters 8, 3, Mai/Juni 1988, Seiten 123-127
N. H. Cohen, Ada as a Second Language, McGraw-Hill (1986), Seiten 361-362
M. Gauthier, Ada-Un Apprentissage, Dunod-Informatique, Paris (1989), Seiten 368-370
Es gibt zwei grundsätzliche Wege, ein großes "logisches" Paket einer ersten Designphase in mehrere kleinere Ada-Bibliothekseinheiten zu zerlegen, die leichter zu verwalten, zu kompilieren, zu warten und zu verstehen sind.
a) Die verschachtelte Dekomposition
Bei diesem Ansatz liegt die Betonung auf der Verwendung von Ada-Untereinheiten und/oder -Unterpaketen. Die wichtigen Unterprogramme, Task-Hauptteile und Hauptteile innerer Pakete werden systematisch voneinander getrennt. Dieser Prozess wird innerhalb dieser Untereinheiten/Unterpakete rekursiv wiederholt.
b) Die unstrukturierte Dekomposition
Das logische Paket wird in ein Netz kleinerer Pakete zerlegt, die durch "with"-Klauseln miteinander verbunden werden. Das ursprüngliche logische Paket ist meistens eine Oberfläche (oder ein Designartefakt, das es es gar nicht mehr gibt).
Jeder der beiden Ansätze hat seine Vor- und Nachteile. Die verschachtelte Dekomposition erfordert weniger Code und führt zu einer einfacheren Benennung (da viele Bezeichner kein Präfix benötigen). In der Rational-Umgebung ist die Struktur im Bibliotheksimage sehr transparent und leichter umzusetzen (Befehle Ada.Make_Separate, Ada.Make_Inline). Bei der unstrukturierten Dekomposition führt oft zu weniger Neukompilierungen und zu einer besseren oder saubereren Struktur (insbesondere an Subsystemgrenzen). Diese Art der Dekomposition fördert außerdem die Wiederverwendung. Sie ist auch leichter mit automatischen Neukompilierungstools und Konfigurationsmanagement zu verwalten. Bei der flachen Struktur besteht jedoch ein größeres Risiko, vom ursprünglichen Design abzuweichen, indem einige der Pakete der tieferen Ebenen, die durch die Dekomposition erstellt wurden, mit "with" importiert werden.
Die Verschachtelungstiefe sollte bei Unterprogrammen auf drei Ebenen und bei Paketen auf zwei Ebenen beschränkt werden.
Verschachteln Sie keine Pakete innerhalb von Unterprogrammen.
package Level_1 is package Level_2 is package body Level_1 is procedure Level_2 is procedure Level_3 is
Verwenden Sie für verschachtelte Einheiten
Hauptteil-Stubs ("separate Hauptteile"), wenn eine der folgenden Situationen zutrifft:
Der Deklarationsabschnitt einer Paketspezifikation sollte Deklarationen in dieser
Reihenfolge enthalten:
1) Umbenennungsdeklaration für das eigentliche Paket
2) Umbenennungsdeklarationen für importierte Entitäten
3) "Use"-Klauseln
4) benannte Zahlen
5) Deklarationen von Typen und Subtypen
6) Konstanten
7) Deklarationen von Ausnahmen
8) Spezifikationen exportierter Unterprogramme
9) ggf. verschachtelte Pakete
10) privater Abschnitt
In einem Paket, das mehrere Haupttypen einführt, sind verschiedene Gruppen
zusammengehöriger Deklarationen vorteilhafter:
5) Typ- und Subtypdeklarationen für A
6) Konstanten
7) Deklarationen von Ausnahmen
8) Spezifikationen exportierter Unterprogramme für A
5) Typ- und Subtypdeklarationen für B
6) Konstanten
7) Deklarationen von Ausnahmen
8) Spezifikationen exportierter Unterprogramme für B
usw.
Wenn der Deklarationsabschnitt groß
ist (> 100 Zeilen), begrenzen Sie die verschiedenen Teilabschnitte durch Kommentarblöcke.
Der Deklarationsabschnitt eines Pakethauptteils sollte Deklarationen in dieser
Reihenfolge enthalten:
1) Umbenennungsdeklarationen (für importierte Entitäten)
2) "Use"-Klauseln
3) benannte Zahlen
4) Deklarationen von Typen und Subtypen
5) Konstanten
6) Deklarationen von Ausnahmen
7) Spezifikationen lokaler Unterprogramme
8) Hauptteile lokaler Unterprogramme
9) Hauptteile exportierter Unterprogramme
10) ggf. Hauptteile verschachtelter Pakethauptteile
Andere Deklarationsabschnitte, z. B. in Hauptteilen von Unterprogrammen, Task-Hauptteilen und
Blockanweisungen, folgen demselben allgemeinen Muster.
Verwenden Sie pro importierter Bibliothekseinheit eine
"with"-Klausel. Sortieren Sie die with-Klauseln in alphabetischer Reihenfolge. Falls in einer Einheit mit "with"-Klausel eine "use"-Klausel zu verwenden ist, sollte sie unmittelbar
auf die entsprechende
"with"-Klausel folgen. Informationen zum Pragma 'Elaborate' finden Sie im folgenden Abschnitt.
Versuchen Sie nicht, mit der Reihenfolge der Ausarbeitung von Bibliothekseinheiten
einen bestimmten Effekt zu erzielen.
Jede Ada-Implementierung ist frei, ihre Strategie für die Berechnung der Ausarbeitungsreihenfolge zu wählen, sofern sie die einfachsten Regeln aus dem Ada-Referenzhandbuch [ISO87] erfüllt. Einige Implementierungen nutzen intelligentere Strategien als andere (z. B. die Ausarbeitung der Hauptteile, sobald dies nach der entsprechenden Spezifikation machbar ist). Es gibt auch Implementierungen, bei denen kein Wert auf eine intelligente Strategie gelegt wird (insbesondere für generische Instanziierungen), was zu sehr schweren Portierbarkeitsproblemen führt.
Für den berüchtigten Fehler "Zugriff vor Ausarbeitung" während der Programmausarbeitung (der normalerweise die Ausnahme Program_Error auslösen sollte) gibt es drei Hauptursachen:
task type T; type T_Ptr is access T; SomeT : T_Ptr := new T; -- Zugriff vor der Ausarbeitung
Um Probleme beim Portieren von Anwendungen von einem Ada-Compiler auf einen anderen
zu vermeiden, sollte der Programmierer den Code neu strukturieren (was nicht immer möglich ist) oder die Ausarbeitungsreihenfolge mit folgender Strategie durch das Pragma 'Elaborate' explizit steuern.
In der Kontextklausel einer Einheit Q sollte ein Pragma 'Elaborate' auf jede Einheit P, die in einer "with"-Klausel erscheint, angewendet werden:
Falls nun P einen Typ T exportiert, so dass die Ausarbeitung von Objekten des Typs T eine Funktion im Paket R aufruft, sollte die Kontextklausel von Q Folgendes enthalten:
with R; pragma Elaborate (R);
Dies gilt auch dann, wenn Q keine direkten Verweise auf R enthält!
In der Praxis kann es einfacher (aber nicht immer möglich) sein, die Regel aufzustellen, dass das Paket P Folgendes enthalten sollte:
with R; pragma Elaborate (R);
Das Paket Q muss einfach Folgendes enthalten:
with P; pragma Elaborate (P);
Hier wird die richtige Ausarbeitungsreihenfolge durch die Transitivität erreicht.
Verwenden Sie Tasks nur eingeschränkt.
Tasks sind ein sehr mächtiges Feature, das jedoch heikel in der Verwendung ist. Eine nicht gerechtfertigte Verwendung von Tasks kann einen großen Zeitaufwand und einen hohen Platzbedarf mit sich bringen. Kleine Änderungen an einem Abschnitt des Systems können die Lebendigkeit einer Gruppe von Tasks komplett gefährden und so zu Blockierungen und/oder gegenseitigen Sperren führen. Es ist schwierig, Programme mit Tasks (mit Debuggern) zu testen. Über die Verwendung von Tasks, ihre Platzierung und Interaktion sollte daher auf Projektebene entschieden werden. Tasks können nicht verdeckt verwendet oder von unerfahrenen Programmierern geschrieben werden. Das Task-Verarbeitungsmodell eines Ada-Programms muss transparent und verständlich sein.
Solange es keine effektive Unterstützung durch parallele Hardware gibt, sollten Tasks nur eingeführt werden, wenn Parallelität tatsächlich erforderlich ist. Dies ist der Fall, wenn zeitabhängige Aktionen ausgedrückt werden (periodische Aktivitäten oder die Einführung von Zeitlimits) oder Aktionen, die von einem externen Ereignis abhängen, z. B. einer Unterbrechung oder dem Eintreffen einer externen Nachricht. Tasks müssen ebenfalls für die Entkopplung anderer Aktivitäten eingeführt werden, z. B. das Puffern, die Warteschlangensteuerung, die Zuteilung und die Synchronisation des Zugriffs auf allgemeine Ressourcen.
Geben Sie die Größe des Task-Stacks mit einer Klausel
'Storage_Size length an.
Aus denselben Gründen und unter denselben Umständen, die length-Klauseln für Objektgruppen erforderlich machen (siehe obigen Abschnitt "Zugriffstypen"), muss die Größe einer Task immer dann angegeben werden, wenn Speicher eine knappe Ressource ist. Deklarieren Sie zu diesem Zweck immer Tasks eines explizit deklarierten Typs (da die length-Klausel nur auf einen Typ angewendet werden kann). Zur dynamischen Größenanpassung des Stacks können Sie einen Funktionsaufruf verwenden.
Anmerkung: Es kann schwer einzuschätzen sein, wie groß der Stack für die einzelnen Tasks sein muss. Zur Vereinfachung dieser Einschätzung kann das Laufzeitsystem mit einem Hochwassermarkenmechanismus zu instrumentieren.
Verwenden Sie im Hauptteil einer Task eine Ausnahmebehandlungsroutine, um ein nicht erklärtes Sterben einer Task
zu vermeiden oder wenigstens zu protokollieren.
Tasks, die keine Ausnahmen behandeln, sterben in der Regel unbemerkt. Versuchen Sie nach Möglichkeit, die Art des Sterbens, insbesondere Storage_Error, zu dokumentieren. Dies ermöglicht eine Optimierung der Stack-Größe. Beachten Sie, dass dies eine Kapselung der Zuordnung (Basiselement new) in einem Unterprogramm erfordert, das wiederum eine andere Ausnahme als Storage_Error exportiert.
Erstellen Sie Tasks während der Ausarbeitung des Programms.
Aus denselben Gründen und unter denselben Umständen, die eine Zuordnung von Objektgruppen während der Programmausarbeitung erforderlich machen (siehe obigen Abschnitt "Zugriffstypen"), sollte die gesamte Task-Verarbeitungsstruktur der Anwendung schon sehr früh beim Programmstart erstellt werden. Es ist besser, wenn ein Programm durch Speichermangel gar nicht starten kann, als wenn es ein paar Tage später stirbt.
In den folgenden Regeln wird zwischen Service-Tasks und Anwendungs-Tasks unterschieden. Service-Tasks sind kleine und algorithmisch einfache Tasks, die eine Art "Nahtstelle" zwischen anwendungsbezogenen Tasks bilden. Beispiele für Service-Tasks (oder Zwischen-Tasks) sind Puffer, Transporter, Relais, Agenten, Monitore usw., die in der Regel Synchronisations-, Entkopplungs-, Zwischenspeicher- und Anklopfdienste bereitstellen. Anwendungs-Tasks haben, wie der Name schon sagt, einen direkteren Bezug zu den den Primärfunktionen der Anwendung.
Vermeiden Sie Hybrid-Tasks. Anwendungs-Tasks sollten reine Aufruf-Tasks sein und Service-Tasks reine
aufgerufene Tasks.
Eine reine aufgerufene Tasks enthält nur accept-Anweisungen oder Anweisungen für selektives Warten und keine Eingangsaufrufe.
Vermeiden Sie Kreisstrukturen im Diagramm der Eingangsaufrufe.
Sie verringern damit die Gefahr gegenseitiger Sperren ganz erheblich. Wenn sich Kreisstrukturen nicht gänzlich vermeiden lassen, sollten Sie sie wenigstens im stationären Zustand des Systems vermeiden. Mit diesen beiden Regeln wird die Struktur auch leichter verständlich.
Schränken Sie die Verwendung gemeinsamer Variablen ein.
Achten Sie ganz besonders auf verdeckte gemeinsame Variablen, d. h. auf Variablen, die beispielsweise in Pakethauptteilen verborgen sind und auf die Basiselemente zugreifen, die für verschiedene Tasks sichtbar sind. Gemeinsame Variablen können in Extremfällen für die Synchronisation des Zugriffs auf allgemeine Datenstrukturen genutzt werden, wenn die Kosten für Rendezvous zu hoch sind. Überprüfen Sie, ob das Pragma 'Shared' effektiv unterstützt wird.
Verwenden Sie abort-Anweisungen nur eingeschränkt.
Die Anweisung abort wird allgemein als eines der gefährlichsten und schädlichsten Basiselemente der Sprache angesehen. Wird sie für eine nicht bedingte (und nahezu asynchrone) Beendigung von Tasks verwendet, sind kaum sinnvolle Rückschlüsse auf das Verhalten einer gegebenen Task-Verarbeitungsstruktur möglich. Es gibt jedoch seltene und ganz spezifische Umstände, unter denen eine Anweisung abort erforderlich ist.
Beispiel: Es werden untergeordnete Services bereitgestellt, die keine Zeitlimiteinrichtung haben. Die einzige Möglichkeit, ein Zeitlimit einzuführen, ist die Bereitstellung des Service durch eine Hilfsagenten-Task, das Warten (mit einem Zeitlimit) auf eine Antwort vom Agenten und das anschließende Abtöten des Agenten mit abort, sofern der Service nicht innerhalb der Wartezeit bereitgestellt wurde.
Ein Abbruch (abort) ist tolerierbar, wenn nachgewiesen werden kann, dass nur der Abbrechende und der Abgebrochene betroffen sind (wenn z. B. keine andere Task die abgebrochene Task aufrufen kann).
Verwenden Sie delay-Anweisungen nur eingeschränkt.
Die beliebige Aussetzung einer Task kann zu schwerwiegenden Zeitplanungsproblemen führen, die nur schwer festzustellen und zu korrigieren sind.
Verwenden Sie die Attribute 'Count, 'Terminated und 'Callable nur eingeschränkt.
Das Attribut 'Count sollte nur als grobe Angabe verwendet werden. Zeitplanentscheidungen sollten nicht davon abhängig gemacht werden, ob der Wert des Attributs gleich oder ungleich null ist, da die tatsächliche Anzahl wartender Tasks sich zwischen dem Zeitpunkt der Attributauswertung und dem Verwendungszeitpunkt des Wertes ändern kann.
Mit bedingten Eingangsaufrufen (oder dem äquivalenten Konstrukt mit accept) können Sie zuverlässig überprüfen, ob keine wartenden Tasks vorhanden sind.
select The_Task.Some_Entry; else -- etwas anderes tun end select;
Die obige Angabe ist der folgenden vorzuziehen:
if The_Task.Some_Entry'Count > 0 then The_Task.Some_Entry; else -- etwas anderes tun end if;
Das Attribut 'Terminated ist nur von Bedeutung, wenn seine Auswertung true ergibt, und das Attribut 'Callable nur, wenn seine Auswertung false ergibt, was ihre Zweckmäßigkeit stark einschränkt. Sie sollten diese Attribute nicht verwenden, um die Tasks beim Systemabschluss zu synchronisieren.
Verwenden Sie Prioritäten nur eingeschränkt.
Prioritäten haben in Ada begrenzt Einfluss auf die Zeitplanung. Insbesondere wird die Priorität von Tasks, die auf Eingaben warten, bei der Sortierung der Eingabewarteschlangen oder der Auswahl des Eintrags für ein selektives Warten nicht berücksichtigt. Dies kann zu einer Umkehrung der Priorität führen (siehe [GOO88]). Prioritäten werden von der Planungsfunktion nur verwendet, um aus den ausführungsbereiten Tasks die nächste Task auszuwählen. Wegen der Gefahr der Prioritätsumkehrung sollten Sie sich für einen gegenseitigen Ausschluss nicht auf Prioritäten verlassen.
Mit Familien von Einträgen ist es möglich, die Eingabewarteschlange in mehrere untergeordnete Warteschlangen zu unterteilen. Nach einer solchen Unterteilung ist es oft möglich, ein explizites Dringlichkeitskonzept einzuführen.
Falls keine Prioritäten notwendig sind, sollten Sie keiner Task eine Priorität zuordnen.
Wenn Sie einer Task eine Priorität zugeordnet haben, ordnen Sie allen Tasks der Anwendung eine Priorität zu.
Diese Regel ist notwendig, weil die Priorität von Tasks ohne ein Pragma 'Priority' nicht definiert ist.
Halten Sie die Anzahl der Prioritätsstufen aus Gründen der Portierbarkeit
klein.
Der Bereich des Subtyps System.Priority ist implementierungsdefiniert. Die Erfahrung zeigt, dass der tatsächlich verfügbare Bereich von System zu System stark schwankt. Darüber hinaus ist es geschickt, die Prioritäten zentral zu definieren, ihnen Namen und Definitionen zu geben und nicht in allen Tasks ganzzahlige Literale zu verwenden. Ein solches zentrales Paket System_Priorities verbessert die Portierbarkeit und ermöglicht zusammen mit der vorherigen Regel ein einfaches Auffinden aller Task-Spezifikationen.
Um Verschiebungen in zyklischen Tasks zu vermeiden, sollten Sie beim Programmieren der Anweisung delay
die Verarbeitungszeit, den Systemaufwand und die Bevorrechtigung von Tasks berücksichtigen.
Next_Time := Calendar.Clock; loop -- Job ausführen Next_Time := Next_Time + Period; delay Next_Time - Clock; end loop;
Beachten Sie, dass Next_Time - Clock negativ sein kann, was darauf hinweist, dass die zyklische Task verspätet ausgeführt wird. Es könnte ein Zyklus verloren gehen.
Um die Planbarkeit garantieren zu können, sollten Sie
zyklischen Tasks Prioritäten nach monotonen Raten zuordnen, d. h., der am häufigsten ausgeführten Task die höchste Priorität. (Diesbezügliche Details finden Sie in der Veröffentlichung [SHA90].)
Ordnen Sie sehr schnellen Zwischen-Tasks wie Überwachungsprogrammen und
Puffern eine höhere Priorität zu.
Stellen Sie dann sicher, dass diese Tasks sich nicht selbst durch Rendezvous mit anderen Tasks blockieren. Dokumentieren Sie diese Priorität im Code, damit sie während der Programmwartung respektiert werden kann.
Um den Effekt von Abweichungen zu minimieren, sollten Sie
Eingabemuster oder Ausgabedaten und nicht den Zeitraum selbst mit einer Zeitmarke versehen.
Vermeiden Sie aktives Warten (Polling).
Stellen Sie sicher, dass Tasks durch Auswahl- oder Eingangsaufrufe warten oder verzögert werden, anstatt ständig zu überprüfen, ob es etwas zu tun gibt.
Stellen Sie für jedes Rendezvous sicher, dass mindestens eine Seite wartet und nur
eine Seite einen bedingten / zeitlich festgelegten Eingangsaufruf hat oder wartet.
Andernfalls besteht vor allem in Schleifen die Gefahr, dass der Code in eine Konkurrenzsituation gerät, die im Ergebnis starke Ähnlichkeit mit dem aktiven Warten hat. Durch eine schlechte Verwendung von Prioritäten kann diese Situation noch verschlimmert werden.
Achten Sie beim Kapseln von Tasks darauf, dass einige ihrer spezifischen Merkmale
in hohem Maße transparent bleiben.
Falls Eingangsaufrufe in Unterprogrammen verdeckt sind, stellen Sie sicher, dass der Leser der Spezifikation dieser Unterprogramme weiß, dass der Aufruf eines solchen Unterprogramms blockiert werden kann. Geben Sie zusätzlich an, ob die Wartezeit begrenzt ist. Wenn ja, geben Sie eine Schätzung für die Obergrenze. Geben Sie mit einer Namenskonvention die potenzielle Wartezeit an (siehe obigen Abschnitt "Unterprogramme").
Falls die Ausarbeitung eines Pakets, der Aufruf eines Unterprogramms oder die Instanziierung einer generischen Einheit eine Task aktiviert, machen Sie diese Tatsache für den Client transparent.
package Mailbox_Io is -- Dieses Paket arbeitet eine interne Kontroll-Task aus, -- die den gesamten Zugriff auf die externe Mailbox -- synchronisiert. procedure Read_Or_Wait (Name: Mailbox.Name; Mbox: in out Mailbox.Object); -- -- Blockierung (unbegrenztes Warten)
Verlassen Sie sich beim selektiven Warten nicht darauf, dass die Einträge in einer bestimmten Reihenfolge ausgewählt
werden.
Eine gewisse Fairness bei der Auswahl der in Warteschlangeneinträgen enthaltenen Tasks können Sie erreichen, indem Sie die Warteschlangen explizit ohne Wartezeit in der gewünschten Reihenfolge überprüfen und dann auf alle Einträge warten. Verwenden Sie nicht 'Count.
Verlassen Sie sich bei Tasks, die in demselben Deklarationsabschnitt ausgearbeitet werden, nicht auf eine bestimmte
Aktivierungsreihenfolge.
Falls eine spezifische Startreihenfolge angestrebt wird, sollten Sie diese über Rendezvous mit speziellen Starteinträgen erreichen.
Implementieren Sie Tasks so, dass sie normal beendet
werden.
Sofern die Natur der Anwendung nicht erfordert, das eine einmal aktivierte Task endlos ausgeführt wird, sollten Tasks enden, wenn sie abgeschlossen sind oder durch eine Beendigungsalternative. Für Tasks, deren Master ein Paket auf Bibliotheksebene ist, ist das nicht immer möglich, da das Ada-Referenzhandbuch nicht angibt, unter welchen Bedingungen solche Tasks enden sollten.
Falls die Master-abhängige Struktur keine saubere Beendigung zulässt, sollten Tasks spezielle Abschlusseinträge enthalten, die beim Systemabschluss aufgerufen werden, und auf diese warten.
Nach der allgemeinen Philosophie werden Ausnahmen nur für Fehler (logische und Programmierfehler, Konfigurationsfehler, beschädigte Daten, Nichtverfügbarkeit von Ressourcen usw.) verwendet. Als allgemeine Regel gilt, dass Systeme unter normalen Bedingungen keine Ausnahmen auslösen sollten, wenn keine Überlastung und kein Hardwarefehler vorliegt.
Verwenden Sie Ausnahmen zur Behandlung von logischen und Programmierfehlern, Konfigurationsfehlern, beschädigten Daten und der
Nichtverfügbarkeit von Ressourcen. Dokumentieren Sie Ausnahmen und den Auslösepunkt so früh wie möglich mit dem entsprechenden
Protokollierungsmechanismus.
Minimieren Sie die Zahl der Ausnahmen, die aus einer gegebenen Abstraktion exportiert werden.
Wenn man es bei großen Systemen auf jeder Ebene mit einer Vielzahl von Ausnahmen zu tun hat, ist der Code schwer zu lesen und zu warten. Der Aufwand für die Ausnahmebehandlung kann größer als der für die normale Verarbeitung sein.
Es gibt mehrere Möglichkeiten, die Anzahl der Ausnahmen zu minimieren:
Verbreiten Sie keine Ausnahmen, die nicht im Design spezifiziert sind.
Vermeiden Sie in Ausnahmebehandlungsroutinen eine Alternative when
others, bis die abgefangene Ausnahme erneut ausgelöst wird.
Sie können also eine lokale "Bereinigung" durchführen und müssen sich nicht mit Ausnahmen belasten, die auf dieser Ebene nicht behandelt werden können:
exception when others => if Io.Is_Open (Local_File) then Io.Close (Local_File); end if; raise; end;
Das Ende eines Task-Hauptteils ist eine andere Stelle, an der eine Alternative when others verwendet werden kann.
Verwenden Sie für häufige, vorhersehbare Ereignisse keine Ausnahmen.
Die Verwendung von Ausnahmen für Bedingungen, die keine Fehler im eigentlichen Sinne sind, bringt einige Beeinträchtigungen mit sich:
Verwenden Sie beispielsweise keine Ausnahme las eine Art Sonderwert, der von einer Funktion zurückgegeben wird (wie Value_Not_Found bei einer Suche). Verwenden Sie eine Prozedur mit einem Parameter "out", führen Sie einen Sonderwert mit der Bedeutung Not_Found ein oder packen Sie den zurückgegebenen Typ in einen Datensatz mit einer Diskriminante Not_Found.
Verwenden Sie für die Implementierung von Steuerstrukturen keine Ausnahmen.
Dies ist ein Sonderfall der vorherigen Regel. Ausnahmen sollten nicht als eine Art "goto"-Anweisung verwendet werden.
Wenn Sie vordefinierte Ausnahmen abfangen, stellen Sie die Behandlungsroutine in einen sehr kleinen Rahmen um das Konstrukt,
das die Ausnahme auslöst.
Vordefinierte Ausnahmen wie Constraint_Error, Storage_Error etc. können an vielen Stellen vorkommen. Falls eine solche Ausnahme aus einem bestimmten Grund abgefangen werden muss, sollte der Geltungsbereich der Behandlungsroutine so eng wie möglich begrenzt werden.
begin Ptr := new Subscriber.Object; exception when Storage_Error => raise Subscriber.Collection_Overflow; end;
Beenden Sie Ausnahmebehandlungsroutinen in Funktionen mit einer Anweisung
"return" oder
"raise". Andernfalls wird im Aufrufer die Ausnahme Program_Error ausgelöst.
Überprüfungen sollten Sie nur eingeschränkt unterdrücken.
Durch die modernen Ada-Compiler können die Einsparungen beim Codeumfang und die Durchsatzsteigerungen, die mit dem Unterdrücken von Überprüfungen zu erzielen sind, vernachlässigt werden. Die Unterdrückung von Überprüfungen sollte deshalb auf genau eingegrenzte Codeabschnitte beschränkt bleiben, die sich (durch Messungen) als Leistungsengpässe herausgestellt haben. Wenden Sie eine solche Unterdrückung nie auf ein gesamtes System an.
Ebenso gilt, dass keine gesonderte explizite Bereichs- und Diskriminantenüberprüfung nur für den unwahrscheinlichen Fall durchgeführt werden sollte, dass später beschlossen werden könnte, Überprüfungen generell zu unterdrücken. Vertrauen Sie auf die integrierten Prüffunktionen von Ada.
Geben Sie Ausnahmen nicht über den Geltungsbereich ihrer Deklaration hinaus
weiter.
Der Clientcode kann Ausnahmen dann nicht mehr explizit behandeln, bis auf eine Alternative when others, die unter Umständen nicht spezifisch genug ist.
Aus dieser Regel folgt: Wenn Sie einen Typ durch Ableitung reexportieren, denken Sie daran, die Ausnahmen, die die abgeleiteten Unterprogramme auslösen können, ebenfalls zu reexportieren, z. B. durch Umbenennung. Andernfalls müssen die Clients das ursprüngliche definierende Paket mittels "with" importieren.
Behandeln Sie
Numeric_Error und Constraint_Error immer zusammen.
Das Ada-Board hat festgestellt, dass alle Umstände, unter denen Numeric_Error ausgelöst wird, stattdessen Constraint_Error auslösen sollten.
Stellen Sie sicher, dass Statuscodes einen geeigneten
Wert haben.
Wenn Unterprogramme den Statuscode als einen Parameter "out" zurückgeben, stellen Sie sicher, dass dem Parameter "out" ein Wert zugeordnet ist, indem Sie diese Anweisung zur ersten ausführbaren Anweisung im Hauptteil des Unterprogramms machen. Machen Sie alle Status standardmäßig zu einem Erfolgs- oder Fehlerstatus. Denken Sie an alle Möglichkeiten, das Unterprogramm zu verlassen, einschließlich der Ausnahmebehandlungsroutinen.
Führen Sie Sicherheitsprüfungen lokal durch und überlassen Sie sie nicht
dem Client.
Falls ein Unterprogramm fehlerhafte Ausgaben erzeugen kann, wenn keine korrekte Eingabe erfolgt, installieren Sie im Unterprogramm Code, um kontrolliert ungültige Eingaben erkennen und dokumentieren zu können. Verlassen Sie sich nicht auf einen Kommentar, in dem der Client angewiesen wird, korrekte Werte zu übergeben. Theoretisch ist garantiert, dass der Kommentar früher oder später ignoriert wird, was zu kaum feststellbaren Fehlern führen kann, sofern die ungültigen Parameter nicht gefunden werden.
Weitere Informationen hierzu finden Sie in der Veröffentlichung [KR90b].
Dieser Abschnitt beschäftigt sich mit Ada-Features, die an sich nicht portierbar sind. Sie sind in Kapitel 13 des Reference Manual for the Ada Programming Language [ISO87] definiert. Die Compiler-spezifischen Features sind im "Anhang F" der Ada-Compiler-Anbieter beschrieben.
Lesen Sie sorgfältig Anhang F des
Ada-Referenzhandbuchs. (Führen Sie kleine Tests durch, um sicherzugehen, dass der Inhalt verstanden wurde.)
Verwenden Sie Darstellungsklauseln nur eingeschränkt.
Darstellungsklauseln werden nicht von allen Implementierungen einheitlich unterstützt. Da ihre Verwendung viele Fallstricke mit sich bringen kann, sollten sie nicht frei in einem System verwendet werden.
Darstellungsklauseln sollten für Folgendes verwendet werden:
Darstellungsklauseln können in folgenden Situationen vermieden werden:
Beispiel:
Dieser Code sollte ersetzt werden:
type Foo is (Bla, Bli, Blu, Blo); for Foo use (Bla => 1, Bli =>3, Blu => 4, Blo => 5);
type Foo is (Invalid_0, Bla, Invalid_2, Bli, Blu, Blo);
Gruppieren Sie Typen mit Darstellungsklauseln in Paketen und bezeichnen Sie diese klar als Pakete mit implementierungsabhängigem
Code.
Gehen Sie beim Satzaufbau nie von einer bestimmten Reihenfolge aus.
Geben Sie in einer Satzdarstellungsklausel
immer die Position aller Diskriminanten an, bevor Sie Komponenten in den Varianten angeben.
Vermeiden Sie Ausrichtungsklauseln.
Vertrauen Sie auf den Compiler. Er kennt die Ausrichtungsvorgaben des Ziels. Durch den Programmierer eingeführte Ausrichtungsklauseln können später zu Ausrichtungskonflikten führen.
Achten Sie besonders darauf, ob in uneingeschränkten zusammengesetzten Typen Compiler-generierte Felder vorhanden sind, z. B.
in Datensätzen die relative Adresse dynamischer Felder und in Arrays Dope-Vektoren.
Details hierzu finden Sie in Anhang F zum Compiler. Verlassen Sie sich nicht auf die diesbezüglichen Angaben in Kapitel 13 des Ada-Referenzhandbuchs [ISO87].
Verwenden Sie Unchecked_Conversion nur eingeschränkt.
Die einzelnen Ada-Compiler unterscheiden sich stark hinsichtlich der Unterstützung für Unchecked_Conversion. Das genaue Verhalten von Unchecked_Conversion kann, insbesondere bei Anwendung auf zusammengesetzte Typen und Zugriffstypen, im Einzelfall geringfügig abweichen.
Stellen Sie in einer Instanziierung von
Unchecked_Conversion sicher, dass sowohl die Quellentypen als auch die Zieltypen eingeschränkt sind und dieselbe Größe haben.
Nur so ist eine begrenzte Portierbarkeit möglich. Nur so lassen sich auch Probleme mit Informationen vermeiden, die von der Implementierung hinzugefügt werden (wie Dope-Vektoren). Eine Möglichkeit, eine identische Größe beider Typen sicherzustellen, ist das Einschließen der Typen in einen Satztyp mit einer Satzdarstellungsklausel.
Ein Weg, die Eingeschränktheit des Typs zu gewährleisten, ist eine Instanziierung mit einer Oberflächenfunktion, bei der die Einschränkung im Vorfeld berechnet wird.
Wenden Sie
Unchecked_Conversion nicht auf Zugriffswerte oder Tasks an.
Dies wird nicht von allen Systemen unterstützt (z. B. nicht vom nativen Rational-Compiler). Außerdem sollte Folgendes nicht vorausgesetzt werden:
Hier sollten noch einmal die wichtigsten Dinge zusammengefasst werden, auf die Sie achten sollten:
Dieses Dokument ist eine direkte Ableitung aus Ada Guidelines: Recommendations for Designer and Programmers, Application Note 15, überarbeitete Auflage 1.1, Rational, Santa Clara, CA, 1990 [KR90a]. Für die Erstellung wurden jedoch zahlreiche weitere Quellen herangezogen.
BAR88 B. Bardin & Ch. Thompson, "Composable Ada Software Components and the Re-export Paradigm", Ada Letters VIII, 1, Januar/Februar 1988, Seiten 58-79
BOO87 E. G. Booch, Software Components with Ada, Benjamin/Cummings (1987)
BOO91 Grady Booch: Object-Oriented Design with Applications, Benjamin-Cummings Pub. Co., Redwood City, California, 1991, Seite 580
BRY87 D. Bryan, "Dear Ada", Ada Letters 7, 1, Januar/Februar 1987, Seiten 25-28
COH86 N. H. Cohen, Ada as a Second Language, McGraw-Hill (1986), Seiten 361-362
EHR89 D. H. Ehrenfried, Tips for the Use of the Ada Language, Application Note 1, Rational, Santa Clara, CA, 1987
GAU89 M. Gauthier, Ada-Un Apprentissage, Dunod-Informatique, Paris (1989), Seiten 368-370
GOO88John B. Goodenough and Lui Sha: "The Priority Ceiling Protocol", Sonderausgabe der Ada Letters vom Herbst 1988, Seiten 20-31
HIR92 M. Hirasuna, "Using Inheritance and Polymorphism with Ada in Government Sponsored Contracts", Ada Letters XII, 2, März/April 1992, Seiten 43-56
ISO87 Reference Manual for the Ada Programming Language, International Standard ISO 8652:1987
KR90a Ph. Kruchten, Ada Guidelines: Recommendations for Designer and Programmers, Application Note 15, überarbeitete Ausgabe 1.1, Rational, Santa Clara, CA, 1990
KR90b Ph. Kruchten, "Error-Handling in Large, Object-Based Ada Systems", Ada Letters Band X, Nr. 7, (Sept. 1990), Seiten 91-103
MCO93 Steve McConnell, Code Complete-A Practical Handbook of Software Construction, Microsoft® Press, Redmond, WA, 1993, Seite 857
MEN88 G. O. Mendal, "Three Reasons to Avoid the Use Clause", Ada Letters 8, 1, Januar/Februar 1988, Seiten 52-57
PER88 E. Perez, "Simulating Inheritance with Ada", Ada letters VIII, 5, September/Oktober 1988, Seiten 37-46
PLO92 E. Ploedereder, "How to program in Ada 9X, Using Ada 83", Ada Letters XII, 6, November 1992, Seiten 50-58
RAC88 R. Racine, "Why the Use Clause Is Beneficial", Ada Letters 8, 3, Mai/Juni 1988, Seiten 123-127
RAD85 T. P. Bowen, G. B. Wigle & J. T. Tsai, Specification of Software Quality Attributes, Boeing Aerospace Company, Rome Air Development Center, Technical Report RADC-TR-85-37 (3 Bände)
ROS87 J. P. Rosen, "In Defense of the Use Clause", Ada Letters 7, 7, November/Dezember 1987, Seiten 77-81
SEI72 E. Seidewitz, "Object-Oriented Programming with Mixins in Ada", Ada Letters XII, 2, März/April 1992, Seiten 57-61
SHA90 Lui Sha and John B. Goodenough, "Real-Time Scheduling Theory and Ada", Computer Band 23, Nr. 4 (April 1990), Seiten 53-62
SPC89 Software Productivity Consortium: Ada Quality and Style-Guidelines for the Professional Programmer, Van Nostrand Reinhold (1989)
TAY92 W. Taylor, Ada 9X Compatibility Guide, Version 0.4, Transition Technology Ltd., Cwmbran, Gwent, Vereinigtes Königreich GB, November 1992
WIC89 B. Wichman, Insecurities in the Ada Programming Language, Report DITC137/89, National Physical Laboratory (Vereinigtes Königreich GB), Januar 1989
Die meisten Begriffe, die in diesem Dokument verwendet werden, sind in Anhang D des Reference Manual for the Ada Programming Language, [ISO87], definiert. Definitionen zusätzliche Fachbegriffe finden Sie hier:
ADL: Ada Design Language. Dieser Begriff bezieht sich auf die Verwendung von Ada zum Ausdrücken von Designaspekten. In diesem Kontext wird Ada auch als PDL (Program Design Language) bezeichnet.
Umgebung: Die verwendete Ada-Softwareentwicklungsumgebung.
Bibliotheksschalter: In der Rational-Umgebung eine Kompilierungsoption, die für eine ganze Programmbibliothek gilt.
Modellwelt: In der Rational-Umgebung eine spezielle Bibliothek für die Erfassung einheitlicher Einstellungen von Bibliotheksschaltern für ein ganzes Projekt.
Veränderlich: Eigenschaft eines Datensatzes, dessen Diskriminanten Standardwerte haben. Einem Objekt eines veränderlichen Typs können alle Werte des Typs zugeordnet werden, auch Werte, die zu einer Veränderung der Diskriminanten und damit der Struktur des Objekts führen.
Oberfläche: ein Unterprogramm, dessen Hauptteil nur als Relais fungiert. Im Idealfall enthält es nur eine Anweisung, die ein anderes Unterprogramm aufrufen, das eine identische Gruppe von Parametern hat oder Parameter, die aus und in die Parameter des Oberflächenprogramms konvertierbar sind.
PDL: Program Design Language.