Leistung beschrieben wird durch eine Spezifikation bestehend aus der Summe des exportierten Kontrakts und der importierten Kontrakte
und die sich beliebig schachteln lässt.
Das sind die unverbrüchlichen Eigenschaften von Komponenten. Abbildung 9 stellt die ersten drei dar. An dieser Stelle nur stichpunktartig die Argumente für eine Trennung von Kontrakt und Implementierung:
Ein separater Kontrakt ermöglicht den einfachen Austausch von Implementierungen. Das kann zu Testzwecken geschehen - Stichworte: Testattrappe, Mockup - oder um Alternativen bereitzustellen - Stichwort: Plug-ins.
Ein separater Kontrakt, der auch noch vor einer ersten Implementierung festgelegt ist - Stichwort: Contract-first-Design -, erlaubt die parallele Implementierung vieler Komponenten. Das steigert die Produktivität oder erleichtert das Outsourcing von Teilen.
Jeden Kontrakt in einer eigenenAssembly zu beschreiben, verringert die kognitive Belastung während der Entwicklung an einer Implementierung. Derjenige, der eine Komponente implementiert, sieht durch die Kontrakte und ihrer Spezifika-tiondenkleinstmöglichenAusschnittaus der Gesamtanwendung. Er kann sich dadurch bei der Arbeit auf das unmittelbar relevante Arbeitsfeld konzentrieren.
[Abb. 9] Komponenten bestehen aus zwei Assemblies - eine für ihren Kontrakt und eine für die Implementierung des Kontrakts.
Die Realisierung solcher Komponenten erfolgt getrennt nach Kontrakt und Implementierung. Für jeden Kontrakt legen Sie ein eigenes Visual-Studio-Projekt an, für jede Implementierung eine eigene Visual-Studio-Projektmappe. Diese Trennung ist wichtig, um einer schleichenden Zunahme der Entropie in einer Anwendung Widerstand entgegenzusetzen. Denn die Entropie, also die Unordnung nimmt immer dann zu, wenn Beziehungen zwischen Codeteilen hergestellt werden. Jede Beziehung - von der Assemblyreferenz über die Instanzierung einer Klasse bis zum Zugriff auf eine statische globale Variable - macht es nämlich schwieriger, Code zu verstehen. Beziehungen, die nicht im Architekturmodell vorgesehen sind, gilt es daher zu vermeiden. Solange große Teile des Codes aber in einer Projektmappe liegen, wie es in vielen Projekten noch der Fall ist, steht dem jedoch nichts im Wege. Deshalb ist es so wichtig, die Trennung von Kontrakt und Implementierung auch in der Codeorganisation zu spiegeln.
Die Schachtelung von Komponenten ist wichtig, um die logischen Abstraktionsebenen eins zu eins in Codeartefakte überführen zu können. Prinzipiell sieht dann die Architektur jeder Anwendung wie in Abbildung 10 aus. Auf drei grundsätzlich verschiedenen Ebenen beschreiben Sie die Struktur Ihrer Software:
Die grobe Applikationsarchitektur beschreibt, welche Betriebssystemprozesse grundsätzlich zur Software gehören. Das kann ein Clientprozess sein, ein Reportserver, ein Applikationsserver, ein Datenbankserver. Je nach Anwendungsart (Desktop/Web), Skalierbarkeitsanforde-rungen und Kommunikation mit sonstiger Infrastruktur lassen Sie den Code unter einem oder mehreren Hosts laufen. Hosts sind die Programme, die Assemblies mit Ihrer Logik laden. Eine selbstgeschriebene Konsolenanwendung gehört ebenso dazu wie Outlook oder die Enterprise Services, bekannt als COM+.
Wenn Sie die Betriebssystemprozesse kennen, die zusammen Ihre Applikation bilden sollen, zerlegen Sie den Code, der in jedem der Prozesse laufen soll, in Komponenten. Die Zahl der Prozesse ist gemeinhin nicht groß, daher ist es nicht schlimm, dass Prozesse sich nicht schachteln lassen. Bei den Prozessen können Sie mit einer logischen, auf das Modell beschränkten Schachtelung der sogenannten Softwarezellen leben. Aber die Zahl der Komponenten in Ihren Anwendungen kann schon sehr groß werden. Deshalb ist eine Schachtelung angezeigt, um den Code auf unterschiedlichen Abstraktionsebenen beschreiben zu können. Egal, ob Sie beim Entwurf top-down oder bottom-up vorgehen: Sie wollen jederzeit die Möglichkeit haben, Funktionalität in einen "Sack“ zu stecken, um sie zu verbergen.
Die Blattkomponenten im Komponentenbaum, das heißt, die Komponenten auf der untersten Ebene, zerlegen Sie in Klassen. Oder genauer: Nicht der Architekt tut das, sondern diejenigen, welche die Komponenten implementieren. Für den Architekten wären das zu viele Details. Hier bekommen deshalb die Kom-ponentenimplementierer Spielraum für eigene Kreativität. Indem Sie als Architekt ihnen vertrauen, reduzieren Sie die Komplexität, mit der Sie im Modell umgehen müssen. Ein Diagramm aller Klassen wird nicht nötig sein.
[Abb. 10] Softwareentwurf findet auf drei grundsätzlich verschiedenen Abstraktionsebenen statt.
Wenn nun aber eine Komponente aus zwei Assemblies besteht - je eine für Kontrakt und Implementierung - wie soll sie in einer umfassenderen Komponente enthalten sein können? Oder wie soll sie selbst andere Komponenten enthalten, die ja ebenfalls Assemblies sind?
Abbildung 11 zeigt eine simple Komponentenarchitektur auf zwei Abstraktionsebenen. Auf hohem Abstraktionsniveau gibt es nur zwei Komponenten: KL und M. KL ist Client von Service-Komponente M. Beim Hineinzoomen zerfällt KL jedoch in zwei Komponenten: K und L, die wiederum füreinander Client und Service sind.
[Abb. 11] Zusammengesetzte Komponenten verbergen Beziehungsdetails.
Konsequent komponentenorientiert gedacht gibt es damit auf dem hohen Abstraktionsniveau vier Assemblies und auf dem niedrigeren sechs. Der Code würde aus den Projektmappen für die Implementierung von K, L und M bestehen (Ki, Li, Mi) sowie Projekte für die Kontrakte von K, L und M enthalten (Kk, Lk, Mk). Wie können dann K und L physisch zu KL werden, damit das Modell sich in den Artefakten widerspiegelt?
Abbildung 12 verrät den Trick: Die Assemblies mit den Implementierungen und der Kontrakt von L werden mittels des Werkzeugs ILMerge [4] zur Implementierung von KL verschmolzen. Der Kontrakt von K hingegen wird eins zu eins zum Kontrakt von KL. Er muss weiterhin separat stehen für Clientkomponenten von KL. Der Kontrakt von L hingegen verschwindet in der Implementierung von KL, weil er außerhalb ihrer keine Relevanz hat.
[Abb. 12] Zusammengesetzte Komponenten wie KL entste hen durch Verschmelzung der Assemblies ihrer Teilkomponenten mittels ILMerge.
Der Aufruf von ILMerge zum Erzeugen der zusammengesetzten Komponente KL könnte zum Beispiel so aussehen:
ilmerge /t:library /out:KLi.dll
Ki.dll Lk.dll Li.dll
Sie weisen ILMerge damit an, eine DLL mit Namen KLi.dll zu erzeugen, die sich aus den bisherigen DLLs Ki, Lk und Li zusammensetzt. Kk.dll bleibt außen vor und behält ihren Namen, weil Ki sie referenziert hat und also KLi sie auch weiterhin refe-renziert. Solange Kk.dll nicht auch mit den anderen DLLs zusammengemischt wird, kann ILMerge die Referenz darauf nicht verändern. Der Kontrakt von KL muss daher Kk und nicht KLk lauten. Das ist schade, doch derzeit nicht zu ändern. Aber ich arbeite an einem Tool, das auch dieses Problem behebt. Mehr dazu in einer zukünftigen dotnetpro. Bis dahin sollten Sie trotzdem nicht auf die Möglichkeit verzichten, Komponenten mit ILMerge zu schachteln. Der Gewinn an Modell-Implementierung-Übereinstimmung ist groß genug, um den Aufwand zu rechtfertigen und diese kleine Unschönheit zu verschmerzen.
Seien Sie einfach konsequent in der Projektplanung und -organisation:
Zerlegen Sie Ihre Applikation zuerst in Betriebssystemprozesse. Das sollte nicht so schwer sein.
Zerlegen Sie dann die Prozesse schrittweise in Komponenten. Immer eine Abstraktionsebene nach der anderen. Sie können dabei top-down oder bottom-up oder - nach Jojo-Manier - wechselweise vorgehen. Am Ende wird der Prozess und so Ihre ganze Anwendung aus mehreren Abstraktionsebenen bestehen. Sie haben mindestens eine Prozessebene und eine Ebene mit Blattkomponenten. Dazwischen können beliebig viele weitere Ebenen mit zusammengesetzten Komponenten oder "Superkomponenten“ liegen.
Für jede Blattkomponente