zurück zum Artikel

Herausforderung Brownfield, Teil 5: Explizite Architektur als Ziel für Refaktorisierungen

Stefan Lieser, Ralf Westphal

Durch die Zerlegung in Partitionen und Bounded Contexts fällt es leichter, mit der Sanierung eines Systems zu beginnen. Nun erfordern die beschränkten Ressourcen Fokussierung.

In der Brownfield-Serie wurde bereits der "Big Ball of Mud", der große Klumpen Matsch, in Partitionen und Bounded Contexts zerlegt. Durch die Zerlegung fällt es leichter, mit der Sanierung des Systems zu beginnen. Das liegt vor allem daran, dass nun nicht mehr das Gesamtsystem auf einmal in den Blick zu nehmen ist, sondern sich einzelne Bounded Contexts oder Partitionen betrachten lassen. Nun erfordern die beschränkten Ressourcen Fokussierung.

Ein Unternehmen sollte aus betriebswirtschaftlicher Sicht ein Ziel vor Augen beziehungsweise eine Strategie entwickelt haben. Das mögen im einen Jahr zusätzliche Funktionen sein, von denen man sich ein weiteres Abheben von der Konkurrenz erwartet. Im anderen Jahr mag es angezeigt sein, Kosten zu reduzieren, in dem das Unternehmen den Support dadurch entlastet, dass es die Probleme beseitigt, die die meisten Ressourcen im Support binden.

Mehr Infos

Herausforderung Brownfield

Im Rahmen einer Artikelserie beleuchten die Initiatoren der "Clean Code Developer"-Initiative, Stefan Lieser und Ralf Westphal, die Herausforderungen an Clean Code Developer in Brownfield-Projekten.

Wie auch immer die Strategie aussieht, ist ohne ein solches Ziel die Sanierung von Brownfield-Projekten zu wenig fokussiert. In der Folge droht die Gefahr, dass an allen möglichen Stellen gearbeitet wird, ohne zu berücksichtigen, welche Auswirkungen das auf die weitere Entwicklung des Unternehmens hat. Die Unternehmensführung ist demnach gefragt, konkrete Vorgaben zu formulieren.

Hilfreich ist eine Einteilung der Anwendungsbereiche in drei Kategorien:

Diese Kategorisierung findet sich im Abschnitt "Strategic Design" in Eric Evans' Buch "Domain Driven Design" [1]. Im Bereich der General Domain liegen Aufgabenstellungen, die so häufig in Softwareprodukten anfallen, dass man sie "von der Stange" einkaufen kann. Als Beispiel sei der Bereich Buchhaltung genannt. In den meisten Fällen wird es für ein Unternehmen nicht sinnvoll sein, den Bounded Context [6] Buchhaltung selbst zu entwickeln. Dabei würde man Entwicklerressourcen binden, ohne dadurch einen Wettbewerbsvorteil zu erlangen.

Im Bereich der Supporting Domain liegen Aufgabenstellungen, bei denen eine Eigenentwicklung sinnvoll sein kann, es aber auch denkbar ist, die Funktionen hinzuzukaufen. Im Einzelfall ist dann genau zu prüfen, ob es strategisch vorteilhaft wäre, den Bereich in die Hand zu nehmen, möglicherweise auch nur teilweise, indem man zugekaufte Komponenten anpasst. Beispielsweise seien UI Controls genannt, bei denen es zu aufwendig wäre, sie selbst zu entwickeln. Liegt das strategische Ziel eines Unternehmens jedoch darin, die Bedienbarkeit seiner Anwendungen von der der Mitbewerber abzuheben, mag es sinnvoll sein, die Controls selber zu entwickeln.

Im Bereich der Core Domains spiegelt sich die strategische Entscheidung des Unternehmens für den Bau eines Systems wieder. Da es strategische Entscheidungen regelmäßig anpasst, bleibt auch die Core Domain nicht konstant. Deren Fokus ändert sich im Laufe der Zeit. Kann ein Unternehmen einen Wettbewerbsvorteil erlangen, indem es innerhalb des Gesamtsystems etwas auf eine bestimmte Art und Weise tut, definiert das die Core Domain. Mit der Zeit werden die Wettbewerber den Vorteil vielleicht aufholen oder beginnen, die Dinge zu kopieren oder zumindest Ähnliches zu tun.

Als Verdeutlichung dessen sei an eine Textverarbeitung ohne Silbentrennung gedacht. Das Unternehmen, das als Erstes die Silbentrennung in die Textverarbeitung integriert, hat dadurch gegenüber den Mitbewerbern einen Vorteil. Die Silbentrennung stellt demnach die Core Domain dar. Allerdings werden die Konkurrenten nach und nach ebenfalls eine Silbentrennung ergänzen, womit sich diese aus der Core- in die Supporting Domain verschiebt. Nach einiger Zeit ist Silbentrennung etwas so Generisches, dass sie niemand mehr selbst entwickelt, sondern jeder eine der vielen Implementierungen verwendet.

Ist die Core Domain identifiziert, kann es endlich losgehen. Doch was konkret sind die ersten Schritte? Die Entwicklungsumgebung starten und loslegen? Sicherlich nicht. Code ist auf einem zu niedrigen Abstraktionsniveau angesiedelt, um daran die Sanierung zu beginnen. Zunächst wäre erst mal die Frage zu beantworten, in welche Richtung denn die Sanierung erfolgen soll. Was konkret soll am Code verändert werden? Das lässt sich nur auf einem höherem Abstraktionsniveau beantworten. Gefragt sind eine Architektur der Gesamtanwendung und ein Modell der Implementierung (siehe [2 [7]], [3 [8]]). Die Implementierung ist das Konkrete mit allen Details, das Modell abstrahiert und lässt bewusst Details weg. Dafür fällt im Modell der Überblick leichter, da es nicht durch viele Details verwirrt.

Eine Landkarte mag als Beispiel für ein Modell dienen. Mit ihr lässt sich eine Wanderung planen, und sie verschafft einem während der Wanderung den Überblick über den Weg. Wäre in der Karte jeder Baum verzeichnet, würde das den Überblick behindern. Genauso verhält es sich mit Software. Den Überblick behält man nur mit einem Modell, die Implementierung enthält hingegen zu viele, die Sicht verstellende Details.

Die Anwendung besteht selbstverständlich aus mehr als nur der Core Domain. Da sie so wichtig ist, muss sie irgendwie im großen Ganzen erkennbar sein. Wie stellt man jedoch das große Ganze dar? Wie wird die Core Domain in einen Gesamtzusammenhang gestellt? Dafür ist Architektur verantwortlich. Sie befasst sich, vom Gesamtsystem ausgehend, mit der Zerlegung des Systems in Bounded Contexts und Partitionen. Doch gerade Letztere sind noch zu groß, um schon hier mit Architektur aufzuhören. Partitionen lassen sich weiter zerlegen, ohne dass dabei Dinge auftauchen, die erst im Rahmen der Implementierung von Bedeutung sind. Eine Partition lässt sich auf Maschinen verteilen. Ein Beispiel hierfür ist jede Webanwendung. Sie ist immer mindestens auf zwei Maschinen verteilt, eine für den Webserver, eine weitere für den Browser. Manchmal handelt es sich vielleicht um virtuelle Maschinen, von denen mehrere auf einer physischen ablaufen. Doch das ändert nichts am Konzept einer Maschine. Sie lässt sich zerlegen in Prozesse. Auch dafür sind Webanwendungen ein Beispiel. Ein Teil der Prozesse mag im Webserver laufen, ein anderer als eigenständiger Prozess.

Als letzten Baustein zerlegt der Architekt den Prozess in Komponenten. Diese sind wiederum als binäre Funktionseinheiten mit separatem Kontrakt zu verstehen. Binär bezieht sich dabei auf die Art und Weise der Verwendung. Da die Komponenten in binärer Form genutzt werden und nicht in Form von Quelltext, wird die Referenz einer Komponente auf eine Assembly beziehungsweise ein Package gesetzt. Der separate Kontrakt bedeutet, dass der Kontrakt der Komponente getrennt von der Implementierung abgelegt ist. Dadurch können Verwender der Komponente den Kontrakt referenzieren, ohne von einer konkreten Implementierung abhängig zu sein. Die Trennung von Kontrakt und Implementierung ermöglicht dann eine parallele Entwicklung der Komponenten. Jede Komponente lässt sich isoliert von anderen implementieren, da die Abhängigkeiten über die separaten Kontrakte abgetrennt wurden.

Unterhalb von Komponenten stehen im Rahmen der Implementierung verwendete Funktionseinheiten. Das sind im objektorientierten Paradigma Assembly/Package, Klasse und Methode. Allerdings spielen die Funktionseinheiten im Rahmen von Architektur keine Rolle mehr. Natürlich verfügen auch diese Einheiten über einen Kontrakt, er ist jedoch nicht separat abgelegt und häufig auch nicht explizit, sondern implizit. Eine Klasse lässt sich durch die Definition eines Interface mit einem expliziten Kontrakt versehen. Ohne Interface hat sie lediglich einen impliziten Kontrakt, der aus allen nach außen sichtbaren Bestandteilen besteht.

Separate Kontrakte sorgen für Stabilität und Unabhängigkeit im Entwicklungsprozess. Innerhalb einer Komponente ist diese Stabilität im Rahmen der Implementierung erforderlich, damit ein Entwicklerteam ungestört eine Komponente implementieren kann. Zudem sorgen die Kontrakte dafür, dass auch das Zusammenspiel der Komponenten stabil bleibt und sich damit über eine Komponente hinweg ungestört arbeiten lässt.

Innerhalb der Architekturbausteine (Bounded Context, Partition, Maschine, Prozess, Komponente) muss sich die Core Domain einordnen lassen. Sie muss daher mindestens in einer Komponente als kleinste Einheit zu finden sein und ist im Extremfall ein kompletter Bounded Context als größte Einheit von Architektur. Dass die Core Domain nur aus einer einzelnen Komponente besteht, ist ein seltener Spezialfall. In der Regel besteht die Core Domain aus mehreren Komponenten. Möglicherweise ist sie so umfangreich, dass eine gesamte Partition die Core Domain ausmacht. Dass ein Bounded Context die Core Domain repräsentiert, dürfte ebenfalls eher die Ausnahme sein.

Somit besteht die Aufgabe nun darin, eine Architektur des Gesamtsystems zu entwickeln. Das Ziel ist dabei einerseits, innerhalb der Architektur die Core Domain zu verorten. Andererseits ist die Architektur dafür verantwortlich, die nichtfunktionalen Anforderungen abzubilden. Das ist erforderlich, weil nichtfunktionale Anforderungen wie Skalierbarkeit einen erheblichen Einfluss auf die Architektur haben. Architektur gibt hierbei den groben Rahmen vor, in dem sich später die Modellierung und die Implementierung zu bewegen haben. Ohne die Vorgabe einer Architektur sind nichtfunktionale Anforderungen nicht umsetzbar. Eine Anwendung, die nicht für tausende Transaktionen pro Minute ausgelegt ist, wird ohne entsprechende Architektur nicht "einfach so" skalieren.

Vergegenwärtigt sei kurz noch mal, dass es hier um Brownfield-Projekte geht. Die zu entwerfende Architektur weicht daher vielleicht deutlich vom Ist-Zustand ab. Dennoch ist es wichtig, sie zu entwerfen, da sie das Ziel definiert, zu dem die Anwendung hin zu refaktorisieren ist. Erst durch den Architekturentwurf lassen sich beim Refaktorisieren fundierte Entscheidungen treffen. Fehlt eine klare Vorstellung über die angestrebte Architektur, werden Entscheidungen nach Belieben getroffen.

Nachdem die Architektur entworfen ist, ist im nächsten Schritt ein Modell für die Core Domain zu erstellen. Es geht nicht darum, ein Modell für die gesamte Anwendung zu erstellen. Noch weniger geht es darum, die ganze Anwendung aufzupolieren. Die knappen Ressourcen fokussieren sich stattdessen auf die Core Domain. Das Ergebnis der Modellierung wird im nächsten Schritt als Ziel für Refaktorisierungen verwendet und dient dann als Vorlage für die Implementierung. Statt also "wild drauf los" zu refaktorisieren, gibt das Modell ein Ziel vor. Jeder Refaktorisierungsschritt sollte den Code nach Möglichkeit in diese Richtung bewegen.

Die Modellierung findet in den Grenzen der entworfenen Architektur statt. Damit gibt die Architektur bereits die Struktur vor, in die sich das Modell der Implementierung einordnen muss. Bei der Modellierung geht es um die Umsetzung der funktionalen Anforderungen, während sich die Architektur um die nichtfunktionalen Anforderungen kümmert. Diese klare Trennung ist bei der Modellierung beizubehalten. Es ist beispielsweise nicht mehr die Kreativität gefragt, die Anwendung auf Prozesse zu verteilen. Entscheidungen sind bereits im Rahmen der Architektur gefällt worden. Somit kann sich die Modellierung allein auf die funktionalen Anforderungen konzentrieren.

Das Modell sollte nicht gleich alle Funktionen der Partition umfassen. Zunächst modelliert man nur die aktuell zu ergänzenden oder zu erweiternden Funktionen. Modellierung und Implementierung sollten Feature für Feature erfolgen. Eine modellierte Funktion ist zunächst vollständig umzusetzen, bevor man mit der Modellierung der nächsten beginnt. Andernfalls würde man "auf Halde" produzieren und nicht umgesetzte Modelle vor ihrer Umsetzung "einlagern". Problematisch daran ist, dass Modelle, die noch nicht konkret in die Umsetzung gehen, möglicherweise gar nicht an die Reihe kommen. Es könnte sich beispielsweise herausstellen, dass eine bereits modellierte, aber noch nicht implementierte Funktion doch nicht umgesetzt werden soll. Der Aufwand für die Modellierung wäre damit verschenkt. Ferner können sich während der Modellierung systematische Fehler einschleichen, die man erst bei der Umsetzung entdeckt. Wird ein solcher Fehler sofort nach der Modellierung der ersten Funktion erkannt, ist er nur in einem Modell zu korrigieren. Diese Erfahrungen lassen sich bei der weiteren Modellierung sofort nutzen. Beim Modellieren "auf Lager" wären womöglich mehrere Modelle vom Fehler betroffen.

Der Idealfall liegt vor, wenn Entwickler eine Funktion erst vollständig fertigstellen, bevor sie mit der nächsten beginnen. Dadurch reduzieren sie den sogenannten Work in Progress (WIP) auf eins. Es befindet sich in einem Team immer nur eine Funktion in Bearbeitung. Nicht immer ist das allerdings erreichbar. Nichtsdestoweniger sollte man versuchen, die Anzahl der in Arbeit befindlichen Funktionen zu reduzieren, weil das einen erheblichen positiven Einfluss auf die Qualität hat. Denn die nächste Funktion wird erst begonnen, wenn der Product Owner das zuvor Implementierte abgenommen hat. Auch bei der Implementierung gilt daher eine Früherkennung von Fehlentwicklungen und Fehlern. Systematische Probleme wirken sich nicht auf mehrere Implementierungen aus, sondern werden so früh wie möglich erkannt und beseitigt.

Sind Funktionen umfangreich, lohnt eine Zerlegung in sogenannte Feature-Scheiben. Der Begriff "Scheibe" soll darauf hinweisen, dass das Zerteilen in jedem Fall im Sinne eines Längsschnitts vertikal erfolgen soll statt horizontal. Erst die komplette UI zu implementieren und dann mit der zugehörigen Logik zu beginnen, ist nicht geeignet, verlässliches Feedback vom Kunden zu erhalten. Das liegt maßgeblich daran, dass das einen horizontalen Schnitt, also einen Querschnitt, darstellen würde. Daher ist es günstiger, sowohl die UI als auch die Logik nur in Teilen zu implementieren, dafür aber eine Feature-Scheibe fertigzustellen. Dadurch kann der Kunde konkreteres Feedback über Teilfunktionen geben, anstatt nur das fertige UI zu sehen. Vom Umfang her sollte eine Feature-Scheibe nur so groß sein, dass ein Team sie bequem an einem Tag umsetzen kann. Das gewährleistet, dass der Kunde jeden Tag konkretes Feedback zu realisierten Funktionen geben kann (siehe [4]).

Doch wie modelliert man nun eine zur Implementierung anstehende Funktion beziehungsweise eine Feature Scheibe? UML wäre eine nachvollziehbare Antwort darauf. Doch Vorsicht, die meisten bei Entwicklern bekannten UML-Diagrammtypen eignen sich nicht zur Modellierung. Exemplarisch sei das Klassendiagramm herausgestellt. Der Versuch, mit einem Klassendiagramm zu modellieren, ist zum Scheitern verurteilt. Das liegt daran, dass Klassendiagramme nicht abstrakter sind als Code. Klassen in einem Klassendiagramm liegen auf demselben Abstraktionsniveau vor wie Klassen, die in Quellcode formuliert sind. Es liegt lediglich eine andere Repräsentationsform vor. In einem Klassendiagramm sieht man nach Meinung der Autoren lediglich die Abhängigkeiten. Wie die einzelnen Methoden der modellierten Klassen die Daten verarbeiten, ist nicht aus dem Diagramm ersichtlich.

Während der Modellierung will man sich aber nicht um alle Details kümmern müssen – das ist ja gerade der Sinn der Modellierung. Daher sollte man sich ruhig trauen, ein paar Kringel und Pfeile zu malen, zu denen man erst später entscheidet, ob diese Funktionseinheiten bei der Implementierung in Methoden, Klassen oder Komponenten übersetzt werden. Damit möchten die Autoren nicht der reinen Willkür das Wort reden. Die so erstellten Diagramme sollen sich später in Code übersetzen lassen. Daher müssen Kringel und Pfeile eine Bedeutung haben, andernfalls gelingt eine Übersetzung nicht. Statt aber die Abhängigkeiten zu modellieren, sei empfohlen, die Datenflüsse zu modellieren. Dabei ergibt es sich auf ganz natürliche Weise, Funktionseinheiten als Aktionen zu betrachten. Es klingt viel natürlicher, eine Funktionseinheit "berechne Preis" zu modellieren, als von einem "Preisrechner" zu sprechen. Statt also nach Klassen zu suchen, ohne genau zu wissen, was sie eigentlich tun sollen, sei lieber nach Aktionen gesucht. Zwischen ihnen fließen Daten, die die jeweiligen Aktionen benötigen, um ihre Aufgabe zu erfüllen.

Das dahinter liegende Prinzip ist etwas in Vergessenheit geraten, dennoch hat es seine Gültigkeit: Funktionseinheiten arbeiten nach dem EVA-Prinzip der Eingabe, Verarbeitung und Ausgabe. Zu einem Warenkorb als Eingabe kann eine Funktionseinheit "berechne Preis" den Gesamtpreis des Warenkorbs als Ausgabe berechnen. Eine andere Funktionseinheit kann die gleiche Eingabe zur Ausgabe von Mehrwertsteuerdaten verarbeiten. Seiteneffekte wie das Persistieren von Daten in einer Datenbank oder das Erzeugen einer PDF-Datei im Dateisystem sind möglich.

Modelliert man eine Funktion in kleinen Funktionseinheiten, die jeweils eine Eingabe zu einer Ausgabe verarbeiten, ergibt sich ein Datenflussdiagramm. Es beschreibt, wie Eingabedaten durch diverse Funktionseinheiten hindurchfließen und am Ende als Ausgaben zur Verfügung stehen. Dabei lassen sich auch Benutzerinteraktionen über eine UI als Eingabequelle modellieren. Ein solches Modell ist deutlich abstrakter als Quellcode, lässt sich aber dennoch in Quellcode übersetzen.

Die Herausforderung liegt darin, eine Form der Modellierung zu finden, die mit möglichst wenigen Abhängigkeiten auskommt. Für Datenflüsse ist das gegeben. Hierbei sind die einzelnen Funktionseinheiten nur noch über die Datentypen abhängig, die auf den "Datenleitungen" transportiert werden. Passt der Datentyp einer Ausgabe zum Datentyp der Eingabe einer anderen Funktionseinheit, lassen sich diese beiden hintereinander "schalten". Auf die Weise fließen die Daten von der einen zur anderen Funktionseinheit. Zu welchem Zeitpunkt der Transport geschehen soll, regeln Events. Daher bezeichnen die Autoren und andere diese Form der Umsetzung als Event-Based Components (EBC) (siehe [5]).

Das konsequente Modellieren in Datenflüssen erfordert ein wenig Umdenken, führt aber zu minimalen Abhängigkeiten und damit zu geringer Kopplung. Ferner sind diese Modelle evolvierbar. Änderungen und Erweiterungen lassen sich leicht einbringen, ohne dadurch Implementierungen und Tests ändern zu müssen. Des Weiteren ergibt sich auf die Weise eine klare Trennung zwischen solchen Funktionseinheiten, die Logik implementieren, und solchen, die für die Verbindungen zwischen Funktionseinheiten zuständig sind. Eine Funktionseinheit ist niemals für beides zuständig – entweder sie realisiert Logik oder sie verbindet Funktionseinheiten.

Bei der Implementierung des Modells ist darauf zu achten, dass die Implementierung jederzeit den Entwurf spiegelt. Andernfalls wäre die Modellierung vergebens. Im Modell identifizierte Funktionseinheiten müssen sich später im Code wiederfinden lassen. Umgekehrt sollten Funktionseinheiten im Quellcode auch im Modell zu finden sein. Das stellt sicher, dass das Modell wirklich ein abstraktes Abbild des Quellcodes ist. Dann kann es wie die Wanderkarte als Orientierung dienen. Das Modell fungiert also auch als Dokumentation, nachdem die Implementierung erfolgt ist, und trägt dadurch wesentlich zur Evolvierbarkeit bei.

Um eine modellierte Funktion im Brownfield-Projekt zu implementieren, sind in der Regel Refaktorisierungen erforderlich. Die neue Funktion ist schließlich in den Code zu integrieren. Dazu müssen meist erst mal die Schnittstellen geschaffen werden. Je weniger Code dabei anzupassen ist, desto besser. Schließlich bergen solche Anpassungen das Risiko von Fehlern und sind daher durch automatisierte Tests abzusichern.

Da die Abhängigkeiten bei EBC gering sind, ist eine Integration in Code relativ einfach zu bewerkstelligen. Das liegt maßgeblich daran, dass die Verbindungen zwischen EBC-Funktionseinheiten lediglich als Datenflüsse modelliert sind. Somit sind an der Schnittstelle zum Brownfield-Code lediglich ein- beziehungsweise ausgehende Datenflüsse zu integrieren.

Architektur und Modellierung stehen in jedem Fall vor der Implementierung von Funktionen. Das gilt nicht nur bei Projekten auf der grünen Wiese, sondern auch im Brownfield-Kontext. Da Architektur für nichtfunktionale Anforderungen wie Skalierbarkeit verantwortlich ist, ist sie explizit zu entwerfen und umzusetzen. Innerhalb des von der Architektur vorgegebenen Rahmens sorgt die Modellierung dafür, dass man vor der Implementierung auf höherem Abstraktionsniveau über die Lösung nachdenken kann. Nachdenken hilft, ist allerdings auf Codeebene heruntergebrochen nur begrenzt möglich. Um die begrenzten Ressourcen jedoch nicht zu verpulvern, ist ein Fokussieren auf die Core Domain erforderlich. Das Identifizieren der Core Domain ist daher eine Führungsaufgabe des Unternehmens. Ohne strategische Entscheidungen würde die Brownfield-Sanierung ziellos erfolgen und durch die mangelnden Ressourcen ohnehin nicht "fertig" werden.

Stefan Lieser
ist freiberuflicher Berater/Trainer/Autor aus Leidenschaft. Seine Schwerpunkte liegen im Bereich Clean Code Developer sowie Domain Driven Design.

Ralf Westphal
ist Microsoft MVP für Softwarearchitektur und freiberuflicher Berater, Projektbegleiter und Trainer für Themen rund um .NET-Softwarearchitekturen

  1. Eric Evans; Domain-Driven Design: Tackling Complexity in the Heart of Software; Addison-Wesley 2003
  2. Ralf Westphal; Was ist Softwarearchitektur? – Teil 1 [9]
  3. Ralf Westphal; Was ist Softwarearchitektur? – Teil 2 [10]
  4. Ralf Westphal; Von den Anforderungen zum fertigen Programm; in dotnetpro 10/2010 bis 12/2010
  5. Ralf Westphal; Artikelserie zu EBC; in dotnetpro 6/2010 und 7/2010

(ane [11])


URL dieses Artikels:
https://www.heise.de/-1187086

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Clean-Code-Developer-in-Brownfield-Projekten-855114.html
[2] https://www.heise.de/hintergrund/Herausforderung-Brownfield-Teil-2-Das-Sicherheitsnetz-aufspannen-888901.html
[3] https://www.heise.de/hintergrund/Herausforderung-Brownfield-Teil-3-Das-Sicherheitsnetz-erweitern-956969.html
[4] https://www.heise.de/hintergrund/Herausforderung-Brownfield-Teil-4-Komplexitaet-bewaeltigen-durch-Differenzierung-1031414.html
[5] https://www.heise.de/hintergrund/Herausforderung-Brownfield-Teil-6-Partitionen-durch-Refaktorisierung-evolvierbar-machen-1333013.html
[6] http://www.heise.de/developer/artikel/Spezialisierte-Datenmodelle-1031427.html
[7] http://ralfw.blogspot.com/2010/12/was-ist-softwarearchitektur-teil-1.html
[8] http://ralfw.blogspot.com/2010/12/was-ist-softwarearchitektur-teil-2.html
[9] http://ralfw.blogspot.com/2010/12/was-ist-softwarearchitektur-teil-1.html
[10] http://ralfw.blogspot.com/2010/12/was-ist-softwarearchitektur-teil-2.html
[11] mailto:ane@heise.de