Code organisieren in .NET Programmen

Veröffentlicht: 19. Sep 2001 | Aktualisiert: 07. Nov 2004
Von Ralf Westphal

Bild03

Ordnung ist das halbe Leben, sagt ein Sprichwort. Und bei genügend großen Softwareprojekten liegt der Prozentsatz sicherlich noch deutlich über 50%. Das betrifft zunächst die Organisation aller projektrelevanten Informationen in gedruckter und elektronischer Form. Aktenschränke und Verzeichnishierarchien müssen geplant und gepflegt werden. Daten, die vorhanden, aber nicht auffindbar sind, schaden mehr, als dass sie nützen.

Große Mengen von Dingen übersichtlich zu organisieren ist eine Herausforderung. Das gilt für Briefmarken wie Akten oder Dateien. Und auch für Sourcecode. Insbesondere, weil die Organisation von Code zwei teilweise gegensätzliche Ziele gleichzeitig erreichen muss: die transparente, verständliche logische Zusammenfassung sowie eine für das Deployment effiziente Aufteilung. In .NET Programmen können Sie kompromislos beide Ziele unabhängig von einander verfolgen.

Auf dieser Seite

 Traditionelle Mittel der Codeorganisation
 Lücken bei den Organisationsmitteln
 Codeorganisationsmittel in .NET
 Assemblies
 Logische und physische Codeorganisation

Traditionelle Mittel der Codeorganisation

Ordnung sollte aber nicht nur bei physischen Projektbestandteilen herrschen. Ordnung ist auch innerhalb von Code auf verschiedenen Ebenen nötig. Dafür gibt es eine Reihe von Mitteln:

  • Klassen/Module: Thematisch zusammengehöriger Code, z.B. alle Routinen zur Verwaltung von Kundendaten, kann in logische Einheiten gefasst werden. In OOP-Sprachen sind das gewöhnlich Klassen, in nicht-OOP-Sprachen (z.B. Modula, aber auch VB) Module. Klassen und Module bilden zunächst nur syntaktische Einheiten im Sourcecode, die durch eine Klammerungen gekennzeichnet sind, z.B. Class...End Class oder Module...End Module. Zweck dieser Zusammenfassung ist aber nicht nur die Ordnung, sondern auch, Programmierschnittstellen zu definieren. Die Prozeduren in einem Modul bilden einen flachen API, von Klassen jedoch können Objekte erzeugt werden, die pro Instanz Daten und darauf operierende Prozeduren verbinden.

  • Dateien: Auf der nächsten Ebene können Sie Klassen bzw. Module in Dateien organisieren. Ob und wie das geschieht, ist von der Syntax einer Programmiersprache und dem eigenen Organisationssinn abhängig. Manche Sprachen (z.B. VB6) erlauben nur ein Modul bzw. eine Klasse in einer Datei abzulegen. Andere Sprachen machen da keine Einschränkungen. Es ist dann den Entwicklern überlassen, von Fall zu Fall zu entscheiden, welcher Code am besten in einer Datei beieinander stehen sollte.

  • Komponenten: Dateien wiederum werden zu Komponenten zusammengefasst. Heute gibt es dafür nur ein Ziel: Die Akkumulation von Funktionalität. Eine Komponente soll eine sinnvolle Programmierschnittstelle bieten.

Code wird also logisch und physisch organisiert. Klassen/Module sind logische Organisationseinheiten, Dateien und Komponenten physische. Allerdings dienen Komponenten durchaus auch der logischen Zusammenfassung, weil sie - wie Klassen/Module - eine Programmierschnittstelle definieren.

Lücken bei den Organisationsmitteln

Die vorhandenen Organisationsmittel für Code sind gut und wichtig. Leider weisen sie aber noch Lücken auf:

  • Namenskonflikte: Innerhalb von Modulen, Dateien, Komponenten und sogar komponentenübergreifend kann es zu Namenskonflikten kommen. Diese Organisationseinheiten lassen es gewöhnlicht nicht zu, dass in ihnen zwei Konstrukte, z.B. Klassen, Enumerationen, Strukturen, den gleichen Namen haben. Logisch auf verschiedenen Ebenen liegende Konstrukte stehen in ihnen nebeneinander und teilen sich einen Namensraum, in dem alle Namen eindeutig sein müssen. Das wird spätestens dann zu einem Problem, wenn Organisationseinheiten verschiedener Herkunft integriert werden sollen, z.B. Komponenten verschiedener Hersteller.

  • Datei-/Projektgröße: Wenn logisch Zusammengehöriges einen bestimmten Umfang erreicht, muss es u.U. künstlich physisch aufgeteilt werden. Dateien ab einer bestimmten Länge und Komponenten ab einer gewissen Zahl Dateien sind einfach unübersichtlich.

  • Nachladen von Code: Zukünftig werden Anwendungen zunehmend Code transparent über Internetverbindungen nachladen. Komponenten sind nachladbare Organisationseinheiten. Bei der Zusammenfassung von Code in ihnen sollte daher nicht nur die Definition einer Programmierschnittstelle Ziel sein, sondern auch die Minimierung der Netzwerklast beim Download. Beide Ziele sind durchaus gegensätzlich.

  • Versionierung von Code: Der Aufrufer einer Programmierschnittstelle erwartet deren Dienste in einer bestimmten Version, um korrekt funktionieren zu können. Daten und Funktionen müssen in Typ, Inhalt, Signatur und Funktion den Erwartungen entsprechen. Die Einheit zur Versionierung von Code ist heute die Komponente. Für manche Projekte ist das die falsche Granularität, für sie wäre es vorteilhaft, wenn logische Programmierschnittstellen aus verschiedenen physischen, versionierbaren Teilen bestehen könnten.

Codeorganisationsmittel in .NET

Der .NET Framework und Visual Studio .NET sind darauf ausgelegt, dass große Projekte mit ihnen realisiert werden. Eines ihrer Ziele ist es daher, deren Organisation zu erleichtern. Neben den bekannten Organisationsmitteln bietet .NET daher Erweiterungen bzw. Veränderungen, die helfen sollen, die existierenden Lücken zu schließen:

Typen
Die unterste, logische Organisationseinheit für Code sind Typen. Sie implementieren Code als Methoden von Klassen. (Auch der Werttyp struct (C#) bzw. Structure (VB .NET) ist eine Klasse.) Das .NET Typsystem ist komplett objektorientiert. Die Notation dafür geben die einzelnen .NET Programmiersprachen vor. Am Ende werden deren Datentypen jedoch auf die Typen des Common Type System (CTS) abgebildet. Hier als Beispiel eine Reihe von in C# definierten, geschachtelten Typen:

class OuterClass 
{ 
class InnerClass 
{ 
} 
struct InnerStruct 
{ 
class ClassInStruct 
{ 
} 
} 
}

mit ihrer Abbildung in die .NET Intermediate Language (IL):

.class ... OuterClass ... 
{ 
  .class ... InnerClass ... 
  {...} 
  .class ... InnerStruct extends [mscorlib]System.ValueType 
  { 
    ... 
    .class ... ClassInStruct ... 
    { ... } 
  } 
}

Die hierarchische Definition von Typen hilft bei der logischen Codeorganisation und reduziert die Wahrscheinlichkeit von Namenskonflikten. Eingeschachtelte Typen können nur über die Qualifikation ihres Namens mit dem des umliegenden Typs angesprochen werden, z.B. OuterClass.InnerStruct.ClassInStruct.

Namespaces
Wenn Sie Typen schachteln, geschieht das vordringlich, weil innere Typen vor allem lokale Bedeutung für den sie umschließenden Typ haben. Das Kriterium ist eher Zweckorientierung. Eine logische, thematische Organisation von Typen ist so nicht möglich. Sie findet normalerweise über Dateien oder Komponenten statt. Warum sollte logische Organisation jedoch auf physischer Basis ausgedrückt werden müssen?
Das CTS bietet daher als neues Konzept so genannte Namensräume an. Sie erlauben die logische Organisation von Typen in geschachtelten Bereichen, innerhalb derer Namen eindeutig sein müssen. Hier ein Beispiel in VB .NET:

Namespace N1
                Class C1
                End Class
                Class C2
                End Class
                Namespace N2
                         Class C1
                         End Class
                         Class C3
                         End Class
                         Namespace N3.N4
                             Class C2
                             End Class
                             Class C3
                             End Class
                             End Namespace
                         End Namespace
        Namespace N5
               Class C3
               End Class
        End Namespace
        End Namespace

Die Identifikation eines Typs erfolgt immer über einen voll qualifizierten, "absoluten" Namen, dem alle Namespaces vorangestellt sind, in denen er direkt bzw. indirekt eingeschachtelt ist, z.B. N1.C1 oder N1.N2.C3 oder N1.N2.N3.N4.C3. Die Notationen

Namespace N1 
Namespace N2 
End Namespace 
End Namespace

und

Namespace N1.N2 
End Namespace

sind dabei gleichbedeutend.
Namespaces ähneln also Verzeichnissen im Dateisystem. Statt Dateien organisieren sie jedoch Typen und statt "\" benutzen sie "." als Trenner zwischen Namen.
Anders als Verzeichnisse lassen sich Namespaces allerdings beliebig oft auf derselben Ebene definieren:

Namespace N1 
End Namespace 
Namespace N1 
End Namespace

Wenn Code einen Namespace "importiert", werden dessen Typen zu den vorhandenen "dazugemischt". Durch die Qualifikation von Namen mit ihren Namespaces lassen sich alle Typen nebeneinander in einer Liste halten, deren Schlüssel der vollständige Typname ist. (Zu ihm tritt intern sogar noch der Name der Komponentendatei, die ihn implementiert.) Kollisionen sind einfach festzustellen und es ist unerheblich, wann und wo ein Typ in einem Namespace definiert wurde.

Der "physische" Import von Namespaces geschieht durch Referenzen auf Komponenten. Sobald einem .NET Compiler mitgeteilt wird, er solle die Typen in einer Komponente berücksichtigen, baut er deren Liste auf und erkennt ihre Nutzung im Code. Der C# Compiler bindet automatisch die Komponente mscorlib.dll ein, so dass er den folgenden Code problemfrei übersetzt:

class hello 
{ 
static void Main() 
{ 
System.Console.WriteLine("Hello, World!"); 
} 
}

Console ist eine Klasse im Namespace System, von dem Teile mscorlib.dll implementiert.
Neben einem "physischen" Import von Namespaces ist in den Microsoft .NET Sprachen C#, VB.NET und JScript.NET auch ein "logischer" möglich. Er soll Schreibarbeit sparen und Code übersichtlicher machen:

Imports System; 
class hello 
{ 
static void Main() 
{ 
Console.WriteLine("Hello, World!"); 
} 
}

Imports weist den Compiler an, Typen im genannten Namespace (hier: System) auch ohne vollständige Qualifikation zu akzeptieren (hier: Console). Das funktioniert gut, solange in verschiedenen importierten Namespaces nicht Typen gleichen Namens definiert sind. Ist das der Fall, müssen Sie sie wieder vollständig qualifizieren.

Wenn Sie einen Namespace importieren, muss dies ausdrücklich geschehen. Bezogen auf das obige erste Beispiel bedeutet das:

Imports N1; 
Imports N1.N2; 
Imports N1.N2.N3.N4; 
Imports N5;

Sie können leider nicht schreiben:

Imports N1.*;

um N1 und alle darin eingeschachtelten Namespaces zu importieren.

Dateien
Selbstverständlich können Sie innerhalb von .NET Projekten weiterhin Typen in Quelldateien zusammenfassen. Die Compilation löst diese physische Organisation aber auf und blickt auf den Sourcecode, als sei er nicht aufgeteilt.

Namespaces - wie auch Typdefinitionen - können Dateigrenzen allerdings nicht überschreiten. Aber natürlich kann derselbe Namespace immer wieder in verschiedenen Dateien geöffnet werden.

Anders als z.B. in VB6 haben .NET Quelldateien keinen eigenen, inhaltsbezogenen Typ mehr. In einer sprachspezifischen Datei - abhängig von der Dateiextension, z.B. *.vb, *.cs - können Sie beliebig viele Typen beliebiger Art definieren.
Sourcecode auf Dateien aufzuteilen dient dazu, ihn mengenmäßig in einem Editor handhabbar zu machen. Für eine logische Organisation sind Dateien im Grunde aber ein veraltetes Mittel. Der Class View in VS .NET bietet eine Sichtweise aller Typen in einem Projekt, für die Dateigrenzen keine Rolle spielen (Abbildung 1).

0109-abb1a 0109-abb1b1

Abbildung 1: Links die Sicht auf ein Projekt mit mehreren Dateien. Für die Arbeit an einem Typ müssen Sie wissen, in welcher er definiert ist.

Rechts die Sicht auf das selbe Projekt als Class View. Er zeigt alle Typen in einstellbarer Ordnungen (z.B. alphabetisch oder nach Art) unabhängig davon, in welcher Datei sie definiert sind.

Assemblies

Die eigentliche physische Organisationseinheit für Code sind Projekte bzw. ihr Ergebnis, die ausführbaren Dateien. .NET nennt sie Assemblies, unabhängig davon, ob es sich um eine EXE oder eine DLL handelt. EXE-Dateien können weiterhin direkt ausgeführt werden, DLLs sind weiterhin Bibliotheken, die eine EXE zur Ausführung benötigen. Andere Unterschiede zwischen beiden Arten von Assemblies gibt es nicht. Assemblies sind Komponenten, die Typen beinhalten und veröffentlichen. Insofern ist ihr erster Zweck die Definition von Programmierschnittstellen.

Darüber hinaus sind Assemblies aber auch die Einheiten, in denen Code nachgeladen und versioniert wird:

  • Wenn Code auf einen Typ zugreift, lädt die Common Language Runtime (CLR) dessen Assembly von einem Datenträger oder über ein Netzwerk oder eine URL vom Internet. Die Granularität in der das geschieht, ist durch die Größe der Assemblies gegeben. Im einen Extremfall besteht eine Anwendung nur aus einer großen Assembly, einer EXE-Datei von vielleicht mehreren MB Umfang. Im anderen Extremfall besteht sie aus hunderten kleinster Assemblies - DLLs -, die jede nur eine oder zwei Typen enthalten.

    Das Problem der DLL-Hölle existiert unter .NET nicht mehr. Die verschiedenen Gründe sollen an dieser Stelle nicht erläutert werden. Für mehr Informationen schauen Sie sich die Whitepaper und technischen Artikel bei MSDN Online und MSDN Magazine an. An dieser Stelle ist lediglich wichtig, dass das Deployment Ihrer Applikationen gleich einfach und robust ist, egal ob Sie nur eine große EXE oder viele DLLs ausliefern.
    Die Kriterien für die Zusammenfassung von Typen in Assemblies ergeben sich damit ausschließlich aus geschäftslogischen Überlegungen zum Umfang ihrer Programmierschnittstelle und zur Granularität des Nachladens. Da es sozusagen auf eine Assembly mehr oder wenig nicht ankommt, kann insbesondere der letzte Aspekt Ihre Entscheidung beeinflussen, welcher Typ in welcher Assembly definiert wird. Das ist neu bei der Komponentenentwicklung unter Windows, die in den letzten Jahren zum einen davon bestimmt war, die Zahl der DLLs klein zu halten, um Deploymentprobleme zu vermeiden, zum anderen Szenarien dynamischen Nachladens aber auch noch gar nicht kannte.

  • Assemblies verfolgen darüber hinaus noch einen weiteren Zweck: Sie etikettieren die Summe der in ihnen enthaltenen Typen mit einer Versionsnummer. Die dient der Prüfung, ob eine Komponente kompatibel zu den Erwartungen des Aufrufers ist. Das eröffnet die Möglichkeit, eine Untermenge von Typen einer Programmierschnittstelle zu Assemblies zusammenzufassen, wenn sie einen anderen Versionszyklus haben, als der Rest der Typen. Dieses Kriterium tritt zu den Überlegungen bzgl. Umfang der Programmierschnittstelle und Ladegranularität. Assemblies sind insofern auch Auslieferungseinheiten von Code: Geänderte Versionen können getrennt von anderen Assemblies auf Zielsysteme geschoben werden.

Logische und physische Codeorganisation

.NET bietet mit Namespaces und Assemblies zwei Organisationskonzepte, die eine saubere Trennung logischer und physischer Codeeinheiten erlauben.
Logische Zusammengehörigkeit drücken Sie dadurch aus, dass Sie Code in Namespaces zusammenfassen. Das kann datei- und assemblyübergreifend geschehen. Der .NET Framework macht es vor: Der System-Namespace ist über viele Assemblies verteilt, die Sie einzeln referenzieren müssen. Sie müssen bei der logischen Organisation also keine Kompromisse eingehen. Die Funktion ihrer Typen bestimmt, wo im Gesamtnamensraum Ihrer Anwendung sie aufzuhängen sind.

Völlig getrennt von der logischen Zusammengehörigkeit entscheiden Sie, wie Sie ihre Typen physische zusammenfassen. Das übliche Kriterium der Definition von Komponenten mit Programmierschnittstellen tritt dabei in den Hintergrund. Am Ende stehen Ihnen in Ihrer Applikation alle Typen in allen Namespaces ohnehin zur Verfügung, egal ob Sie in einer monolithischen EXE oder in vielen DLLs definiert sind. Viel wichtiger für die physische Aufteilung sind Überlegungen dazu, in welcher Granularität Typen zusammen versioniert, wiederbenutzt und über Netzwerke geladen werden.

Und zuguterletzt: Da logische und physische Organisation klar getrennt sind, macht die CLR Ihnen auch kaum Vorschriften darüber, wo Assemblies abzulegen sind. Sie sind nicht gezwungen, eine bestimmte Verzeichnishierarchie anzulegen, um logische Zusammengehörigkeit auszudrücken. Alles Assemblies einer Anwendung können im selben Verzeichnis liegen, müssen es aber nicht. .NET bietet also maximale Freiheit und Entkopplung der "Organisationsphären."


Anzeigen: