Читать книгу Zukunftssichere Architektur - Ralf Westphal - Страница 9

Geschachtelte Komponenten

Оглавление

Software muss auf beliebig vielenAbstraktionsebenen darstellbar sein. Die Modellierungselemente müssen daher schachtelbar sein. Modelle laufen immer Gefahr, von der Realität überholt zu werden. Das führt zu Pflegeaufwand beim Modell, weil es immer wieder der Implementierung nachgeführt werden muss. Wenn allerdings die Modellierungselemente gleichzeitig auch Implementierungsartefakte sind, ist dieser Gefahr zu begegnen. Denn dann kann das Modell aus der Coderealität bei Bedarf generiert werden. Die Klassendiagramme in Visual Studio sind dafür ein Beweis.

Um die Anforderung nach Schachtelbar-keit und Modell-Codeartefakt-Dualität zu erfüllen, schlage ich vor, die Konventionenlücke von unten mit Komponentenorientierung zu füllen. Komponente definiert sich für diesen Zweck minimal als:

 Binäre Codeeinheit- denken Sie "Assembly" -

 mit einem separaten Kontrakt - denken Sie wieder "Assembly" -,

 deren 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 setzen Sie eine Komponentenwerkbank auf, also eine Visual-Studio-Projektmappe, in der Sie fokussiert und isoliert die Komponente implementieren können.

 Und für jede zusammengesetzte Komponente tun Sie dasselbe - siehe das Projekt composites/KLi auf der Heft-CD und in Abbildung 13. Auch wenn darin dann kein neuer Implementierungscode steht, manifestieren Sie dadurch doch zumindest das Modell in Code. Das Superkomponentenprojekt dient in diesem Fall dann nur dazu, die Implementierungen der eingeschachtelten Komponenten zusammenzuziehen und zu verschmelzen. Die Werkbank repräsentiert die Integration und kann mit eigenen Tests ausgestattet werden.


[Abb. 13] Bauen Sie in eigenen Projekten zusammengesetzte Komponenten.

Superkomponenten machen es Ihnen leicht, Ihre Architektur über die Zeit weiterzuentwickeln. Sie können jederzeit Abstraktionsebenen einziehen oder herausnehmen. Wenn Sie bei der Implementierung einer Komponente feststellen, dass es sich eigentlich lohnen würde, daraus mehrere zu machen, dann bleibt sie im Modell erhalten und wird dort nur zu einer Superkomponente. Die Clients und Services merken von der Zerlegung in Teilkomponenten nichts.

Die Aggregation von Komponenten können Sie übrigens auch mit Drittanbieter-bibliotheken betreiben. Zur Komponente M in Abbildung 11 könnte zum Beispiel noch log4net [5] als Instrumentierungsdienstleistung gehören. Dann könnten Sie die eigentliche Implementierung von M und die log4net-Assembly mit ILMerge zu Mi zusammenschweißen. Die Abhängigkeit der Komponente M von log4net würde so nach außen hin verborgen.

Falls allerdings log4net von mehreren Komponenten der Anwendung gebraucht wird, dann verbietet sich natürlich das Verschweißen der Bibliothek mit einzelnen. Das würde einem Copy-and-paste auf Quellcodeebene gleichkommen. Eine solche Vervielfachung von Funktionalität ist nicht wünschenswert.

Sie können aber auch weitergehen und am Ende alle Assemblies der Anwendung zu einer großen Applikations-Assembly zusammenfügen. Dann kann darin auch eine Drittanbieterbibliothek, die von vielen benutzt wird, eingehen. Ich mache das oft, um das Deployment von Memorystick oder CD oder per Email einfach zu halten. Dann "backe“ ich Anwendungs-Assem-blies und Infrastruktur zu einer einzigen EXE-Datei zusammen. Das funktioniert ohne Probleme.

Wenn ich also dafür plädiere, Anwendungen während der Entwicklung in viele kleine Assemblies zu zerlegen, die das Modell eins zu eins widerspiegeln, dann bedeutet das nicht notwendigerweise, dass Sie auch all diese Assemblies genau so ausliefern müssen. Mit ILMerge können Sie quasi beliebige Komposite daraus zusammenschmelzen. Die Zahl der Assemblies für das Verteilen kann also viel kleiner sein als die Zahl der Assemblies während der Entwicklung. Der Gewinn: einfaches Deployment und flexible, produktive Entwicklung. Wie es weitergeht mit mehr Regeln und Konventionen, verrate ich Ihnen in der nächsten dotnetpro. [jp]

[1] Wikipedia: Scrum, QdnpLink SL0806Kolumne1

[2] Krick Modelltechnik, www.krick-modell.de

[3] DATS2/BATSLite DAO/ORM Library, QdnpLink SL0806Kolumne2

[4] ILMerge, QdnpLinkSL0806Kolumne3

[5] log4net, QdnpLink SL0806Kolumne4

Zukunftssichere Architektur

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