Software-Architektur (Monolith / Death Start / Tier etc.)

Ghost_Rider_R

Lieutenant
Registriert
Nov. 2009
Beiträge
758
Hallo zusammen,

ich hätte mal eine ganz allgemeine Frage zur Strukturierung von Software. In meinen Anfängen habe ich eigentlich immer nur Monolithen gebaut, d.h. eine Klasse, im Worst-Case nur die Main-Methode die alles macht. Nicht erweiterbar, nicht modifizierbar, nicht testbar etc. Ein Monolith eben. Bei größeren Softwareprojekten habe ich dann gemerkt, jetzt wird es aber langsam kompliziert und jede Änderung wird immer Aufwändiger.

Dann habe ich mich in den Grundgedanken der Bibliotheken eingearbeitet und kann nun Software in Klasse, Methoden und Bibliotheken verteilen und daraus Micro Services bilden. Soweit so gut.

Nun meine Frage. Gibt es eine Best Practice, wie die Micro Services anschließend verknüpft werden sollte?

So sollte man es wohl nicht machen, Stichwort Death-Star:

1633601952528.png

Quelle: https://www.pogsdotnet.com

Die ganzen Abhängigkeiten erzeugen bei Änderungen wohl ständig diverse Seiteneffekte, überschaubar ist es so also noch nicht.
Aber wie dann?

Eine Möglichkeit wäre das ganze in Schichten aufzuteilen:

1633602293780.png


Oder aber, man strukturiert die Software als Tier?
1633603116873.png

Quelle: https://dzone.com/

Oder doch besser als Stern?
1633602643287.png

Quelle: https://www.pinterest.de/

Ich bin mir leider noch nicht so ganz sicher, nach welchen Suchbegriffen ich da schauen müsste, da dies ein neuer Bereich für mich ist.

Vielen Dank schon mal für eure Ideen und Anregungen.

LG Ghost.
 
Nein, ich bin da noch komplett unerfahren auf dem Gebiet der Softwarestrukturierung und versuche da gerade einen ersten Überblick zu bekommen. Was bedeutet das?
 
Nur um evtl. etwas Verwirrung vorzubeugen, deine Definition von Monolith ist nicht die gleiche die üblicherweise verwendet wird wenn man über Monoliths und Microservices spricht. Monolith bedeutet nicht nur eine Klasse, sondern eine Anwendung die als ganzes gestartet wird.

Man kann auch sehr modulare Monoliths bauen, Microservices sind nicht die einzige Methode um modulare Server Software zu schreiben. Ich würde eher empfehlen dich da erstmal etwas umzusehen und zu versuchen bessere Monolithen zu entwicklen. Microservices fügen eine ganze Reihe von neuen Problemen hinzu die auftreten können, da muss man schon sehr aufpassen.

Es gibt da sehr viele Meinungen wie man gute modulare Software schreibt und was für Architekturen da geeignet sind. Da wirst du aber auch wenn du 10 Entwickler fragst mindestens 11 verschiedene Antworten bekommen, das ist ein Thema über das man endlos diskutieren kann ohne zu einem Konsens zu kommen. Ich würde erstmal im kleinen anfangen zu versuchen modularer zu arbeiten, also einzelne Funktionen und gegebenenfalls Klassen und nicht gleich die ganze Architektur. Und generell gibt es da immer etwas Spannungen zwischen mehr Abstraktion und einfachem Code, beide Extreme können zu Software führen die extrem schwierig zu ändern oder zu verstehen ist.

Ich bin aber generell sehr skeptisch gegenüber reinen Microservices und halte die für 99% der Software für Blödsinn.
 
  • Gefällt mir
Reaktionen: Ghost_Rider_R und BeBur
@Dalek Vielen Dank für die Rückmeldung. Ja in den Anfängen hatte ich einfach nur eine Anwendung, teilweise nur eine Klasse und im Extremfall noch alles nur in der Main-Methode, bis ich selbst gemerkt habe, dass man hiermit irgendwann an Grenzen stößt.

Kann mir jemand sagen, ob dieser Ansatz bei modularer Software grundsätzlich in Ordnung oder zu vermeiden ist:
1633617905553.png


D.h. es darf mehrere Abhängigkeiten zwischen den Klassen untereinander geben. Oder sollte es immer nur maximal eine Abhängigkeit geben, was dann eigentlich immer auf eine Art Stern bzw. auch Baum hinausläuft?
1633617874930.png


Da bin ich mir noch etwas unsicher, ob Baumstrukturen notwendig sind oder ob manche Klasse auch z.B. Dreiecke bilden dürfen...
 
Hi @Ghost_Rider_R, ich schliesse mich @Dalek s Text an.
Zu den Abhängigkeiten:
Bei 2 Modulen, ist eines nur Lieferant, das andere nur Kunde. Das ist eine der Kernaussagen deines Schichten-Abbilds. Das bedeutet auch, ein Kunde kann von unendlich vielen Lieferanten konsumieren.
Gilt genauso für Dienste.
Klassen kannst du grundsätzlich verknüpfen wie du lustig bist - also auch zirkulär. Bspw. kann eine Rechnung all ihre Rechnungspositionen kennen, genauso wie eine Rechnungsposition die zugehörige Rechnung kennen darf.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Ghost_Rider_R
Eventuell ist Dependency Injection (Microsoft, Autofac) noch ein Stichwort, welches dir hilft, die Anhängigkeiten einfacher zu handhaben.
Dein Ansatz ist in meinen Augen soweit in Ordnung und geht halt Richtung klassischer 3-Schichten-Architektur.
Von echten Microservices rate ich Anfängern ab. Das ist wie schon von Dalek geschrieben meist völlig oversized und bringt ziemlich viele neue Probleme mit sich.
 
Sind ,,echte" Microservices denn mehr als nur das Auslagern einzelner für sich selbst funktionierender Teile in eigene Komponenten? so hatte ich das zumindest im ersten Schritt aufgefasst, allerdings habe ich hier auch schon was mit diversen Protokollen / Datenbus gelesen, was mir dann auch oversized erscheint, zumindest für meine Projekte.

Wegen der Dependency Injection habe ich gestern schon ein Video gesehen, welches mein Verständnis für Architektur wieder ein Stück erweitert hat, da werde ich aber noch versuchen tiefer einzusteigen.

Trotzdem noch abschließend meine Frage, ist es in der Softwareentwicklung in Ordnung, wenn man zirkulare Abhängigkeiten hat oder sollte man diese auflösen?

In dieser Grafik erkennt man ganz gut was ich meine. Im linken Beispiel wurde eine Baumstruktur / Sternstruktur gebildet, welche eine Abhängigkeitskette darstellt. So hat jede Komponente immer nur direkten Zugriff auf seinen direkt angrenzende Komponente. In der rechten Grafik handelt es sich momentan eher um meine momentane Art der Softwareentwicklung. Hier können einzelne Objekte übersprungen werden, was die Anzahl der Methoden teilweise reduziert, dafür entstehen aber auch mehr Abhängigkeiten:

1633677231680.png


Quelle: http://prinzipien-der-softwaretechnik.blogspot.com/

Da ich leider keinen Programmierer persönlich kenne, ist es für mich immer schwierig abzuschätzen, wie es denn da draußen so gehandhabt wird. @Micke hat hier zwar schon bestätigt, dass dies möglich ist, ich bin mir aber noch unschlüssig, ob das so auch immer ein guter Stil ist.

Hier gibt es noch weitere Modelle zu sehen, welche in diese Richtung gehen:
1633678448364.png


Quelle: http://prinzipien-der-softwaretechnik.blogspot.com/

Speziell die ,,Clique" Grafik erscheint mir als gutes Anti-Pattern oder? ich meine das wird auch gerne Death-Star in Entwicklerkreisen genannt und ist das Extrembeispiel.

Vielen Dank für eure Meinungen!

Liebe Grüße Ghost
 
Zuletzt bearbeitet:
Zu Microservices gibt es von Microsoft ein recht umfangreiches und kostenloses Buch.
https://aka.ms/microservicesebook

Wenn man eine klassische 3-Schichten-Architektur oder Onion-Architektur verwendet, dann sollten die Zugriffe natürlich nur direkt auf die eine darunterliegende Schicht erfolgen. Auch wenn Abkürzungen von der GUI zur Datenbankschicht manchmal verlockend sind, ist davon abzusehen. Es entstehen dann auch keine zirkulären Abhängigkeiten.
 
Ghost_Rider_R schrieb:
Sind ,,echte" Microservices denn mehr als nur das Auslagern einzelner für sich selbst funktionierender Teile in eigene Komponenten?
Microservices (afaik) bedeutet schlicht, dass du Funktionalität in getrennte Programme auslagerst.

Ghost_Rider_R schrieb:
Trotzdem noch abschließend meine Frage, ist es in der Softwareentwicklung in Ordnung, wenn man zirkulare Abhängigkeiten hat oder sollte man diese auflösen?
ImHo zirkuläre Abhängigkeiten sollte man ganz stark vermeiden.

Ghost_Rider_R schrieb:
In dieser Grafik erkennt man ganz gut was ich meine. Im linken Beispiel wurde eine Baumstruktur / Sternstruktur gebildet, welche eine Abhängigkeitskette darstellt.
Ich find den Artikel bzw. die Graik nicht ganz optimal, da die Pfeile zwei Bedeutungen haben, nämlich "X implementiert Y" oder "X hängt ab von Y". Siehe im Fließtext zu Bild 2a/b: "C verwendet nun das Interface I, das von A implementiert wird."

Ghost_Rider_R schrieb:
Kann mir jemand sagen, ob dieser Ansatz bei modularer Software grundsätzlich in Ordnung oder zu vermeiden ist:
ImHo würde man die gegenseitige Abhängigkeit auflösen, indem man z.B. geteilte Funktionalität von Ping und Pong zusammenfasst zu "PingPong" und Ping und Poing sind dann jeweils nur noch abhängig von "PingPong".


Generell, zumindest soweit ich das mitkriege, ist das Schichtmodell sehr verbreitet und good practice.
 
  • Gefällt mir
Reaktionen: efcoyote
Schichtmodell ist auch wegen der unterschiedlichen Tech Stacks und der daraus resultierenden Spezialisierung beliebt.

Microservices die über ein (lahmes) REST Api miteinander kommunizieren fängt man eigentlich erst ab einer gewissen Skalierung an zu bauen.
 
  • Gefällt mir
Reaktionen: kuddlmuddl
Ok, vielen Dank für eure Rückmeldung. Dann werde ich die strukturellen Verbindungen hier künftig überarbeiten und zirkulären Verbindungen vermeiden, auch wenn es hier und da verlockend ist. Ich denke ein Schichtenmodell bzw. eine Baumstruktur (ist ja das selbe, nur anders grafisch dargestellt oder?) ist für mich der richtige Ansatz. Daraus haben sich jetzt aber noch weitere Fragen ergeben.

Könnt Ihr mir noch sagen, wie es sich hierbei mit Vererbungen verhält und wie, wenn zwei Klassen ein Objekt einer Klasse kennen müssen?

Ein Beispiel:

1633692831865.png


Angenommen, Klasse X kommuniziert nur noch über den Controller und Klasse Y ebenfalls. Klasse X kann Klasse Y also ausschließlich über den Controller erreichen und nicht direkt.

1. Eine Bidirektionale Kommunikation zwischen Klasse X und dem Controller ist in Ordnung oder? also Klasse X darf den Controller kennen und umgekehrt?

2. Und wie ist es, wenn Klasse X ein Objekt vom Typ Bauplan an Klasse Y schicken möchte. Dürfen Klasse X und Klasse Y dann direkt ein Objekt vom Bauplan erzeugen und das durch den Controller zum jeweils anderen schicken? Beide Klassen müssten hierfür dann ja die Klasse Bauplan kennen, kommunizieren aber nicht darüber sondern erstellen nur Objekte davon und übergeben Sie über den Controller zum anderen.
Ist das in Ordnung?

3. Noch ein ganz anderes Beispiel, Stichwort Vererbung. Ist es in Ordnung, wenn sowohl Klasse X, also auch Klasse Y von der Klasse Bauplan erben? hier wird dann ja nicht über die Klasse Bauplan kommuniziert, aber hier müssten sowohl Klasse X als auch Klasse Y die Klasse Bauplan kennen bzw. diese erben.
Kann man das machen oder wäre das zu vermeiden?

Vielen Dank dafür.

LG Ghost.
 
Ich kann mir vorstellen, dass dir die Lektüre von Büchern wie PoEAA (erster Link) oder auch GoF weiterhelfen könnten: Link
Im Idealfall natürlich die Bücher selber, die man nicht unbedingt einmal komplett von vorne nach hinten durchlesen muss, sondern auch einfach drin "schmökern" bzw. später dann nachschlagen kann.
 
BeBur schrieb:
ImHo zirkuläre Abhängigkeiten sollte man ganz stark vermeiden.
Bibliotheken & Schichten ja, bei Klassen gilt das nicht grundsätzlich.
Bsp:
var anzahl = GetRechnung().Positionen.First().Rechnung.Kreditor.Rechnungen.Count();

zirkuläre Abhängigkeiten: Re - Pos, Re - Kreditor
Welche Probleme siehst du dabei ?

Ghost_Rider_R schrieb:
@Micke hat hier zwar schon bestätigt, dass dies möglich ist, ich bin mir aber noch unschlüssig, ob das so auch immer ein guter Stil ist.
"immer" ? Du nimmst meine Antwort leider nicht genau. Für unterschiedliche Dinge gelten unterschiedliche Regeln.


Ghost_Rider_R schrieb:
... Dann werde ich die strukturellen Verbindungen hier künftig überarbeiten und zirkulären Verbindungen vermeiden

1. Eine Bidirektionale Kommunikation zwischen Klasse X und dem Controller ist in Ordnung oder? also Klasse X darf den Controller kennen und umgekehrt?

2. Und wie ist es, wenn Klasse X ein Objekt vom Typ Bauplan an Klasse Y schicken möchte. Dürfen Klasse X und Klasse Y dann direkt ein Objekt vom Bauplan erzeugen und das durch den Controller zum jeweils anderen schicken?
Ironischerweise widersprichst du deinem Fazit sofort mit 1. und 2. :)
Du solltest auch Klasse X und Y konkret benennen. Je nachdem welche Aufgabe sie haben, unterscheidet sich die Antwort bzgl. der Abhängigkeiten.
 
Zuletzt bearbeitet:
Ich habe dir mal ein kleines Beispielprojekt erstellt. Wie du siehst, findet die Kommunikation immer nur mit der nächsten Ebene statt. Machbar und auch nicht schlimm, wäre auch die Kommunikation mit einer Klasse der gleichen Ebene, sprich wenn CityService eine Methode des CountryService benötigt. Die Schicht für das Frontend ist in dem Fall eine einfache Konsole, wobei sie 1:1 durch eine Webanwendung mit Controllern oder ein WPF-Projekt ersetzt werden kann.
Du kannst es dir ja mal anschauen und gerne fragen, wenn noch etwas unklar ist. Ich habe versucht es möglichst einfach zu halten und daher auch auf Interfaces, die man normalerweise bei DI verwendet, verzichtet.
 
  • Gefällt mir
Reaktionen: ZuseZ3 und Ghost_Rider_R
Mextli schrieb:
Schichtmodell ist auch wegen der unterschiedlichen Tech Stacks und der daraus resultierenden Spezialisierung beliebt.

Microservices die über ein (lahmes) REST Api miteinander kommunizieren fängt man eigentlich erst ab einer gewissen Skalierung an zu bauen.
Ich möchte mich hier zu 100% anschließen!
Habe 2 große Software-Projekte scheitern sehen, die alles auf Micro-Service auslegen wollten mit zig Komponenten und jeweils REST-Apis,. die großteils nach >10 Mannjahren nicht fertig geworden sind.
Und ich hab deutlich mehr Software-Projekte gesehen, die Schichten ("Zwiebel")-basiert sehr erfolgreich durchgeführt wurden.
 
  • Gefällt mir
Reaktionen: JP-M und efcoyote
du hast einfach 2 Nachteile bei Microservices:
1. du verlierst sehr sehr wahrscheinlich Performance. Das liegt nicht zwingend an langsamen REST API Verbindungen, denn Microservice beschreibt nur, dass du quasi einzelne Programme/Dienste pro Aufgabe hast und dadurch ggf. für eine Tätigkeit, die man normalerweise in einen einzelnen Dienst einbaut, Anzahl n Microservices/Aufrufe hast und jedes Mal die gleichen Daten verschickst, obwohl dies gar nicht nötig wäre.
2. Du brauchst eine vernünftige Middleware Lösung, um die Microservices zu orchestrieren. Denn häufig scheitert es genau an dieser Aufgabe, da ein orchestrieren der Microservices meist nicht nur beim Aufrufen der Microservices startet und endet, sondern ggf. zw. den Aufrufen der Microservices geloggt, getrackt, konvertrierungen von strukturierten Daten und zwischenspeichern von Daten geschieht etc.pp.

Allerdings: wenn Microservices vernünftig geplant und konzipiert wurden sowie das dazugehörige Orchestrieren, so kann man neue Projekte super schnell umsetzen und einbinden. Es scheitert halt nur zumeist direkt am Anfang an der Architektur und Planung.
 
Micke schrieb:
var anzahl = GetRechnung().Positionen.First().Rechnung.Kreditor.Rechnungen.Count();

zirkuläre Abhängigkeiten: Re - Pos, Re - Kreditor
Welche Probleme siehst du dabei ?
Ich nehme an, dass das nur ein Beispiel war, aber so ein Code würde bei mir durch keine Review kommen - das ist schlichtweg schlecht.

Aber zur Frage, warum circular dependencies zu vermeiden sind:
Angenommen du hast DI in deinem Projekt (was imho ein must have ist), was glaubst du passiert, wenn deine Services voneinander abhängen (sprich A benötigt B & B benötigt A.)?
 
VD90 schrieb:
Angenommen du hast DI in deinem Projekt (was imho ein must have ist), was glaubst du passiert, wenn deine Services voneinander abhängen (sprich A benötigt B & B benötigt A.)?

C#:
public A(Lazy<B> b) { }

public B(Lazy<A> a) { }
;)

Autofac kann es Out-of-the-Box und bei MS muss man es gesondert registrieren.
 
@efcoyote Vielen Dank für das recht aufwendige Beispiel, habe ich mir durchgesehen.

Wegen der Rückfrage mit dem GetRechnung().Positionen.First().Rechnung.Kreditor.Rechnungen.Count();
Das sieht für mich ein bisschen nach einer Rekursion / Endlosschleife auf, falls das damit gemeint ist?

###############################################################################
###############################################################################

In Summe habe ich mir jetzt einige Tage lang den Kopf darüber zerbrochen, was für mich am meisten Sinn macht und bin für künftige Projekte zu diesem Konsens gekommen:

1. Klassen dürfen grundsätzlich fast immer und überall bekannt sein und müssen dies auch, sonst kann man damit ja nicht arbeiten. Diese sollten beim Schichtenmodell so also nicht direkt betrachtet werden, die Klasse String oder DateTime verwende ich ja auch ständig, ich denke so eine Abhängigkeit ist nicht mit dem Schichtenmodell gemeint.

2. Objekte sollten wenn möglich immer via Dependency-Injection von der Root erzeugt werden und an die darunterliegenden Klassen durchgegeben werden. Hier sollte man also in Schichten denken. Sollte zwei Klassen in einer darunterliegenden Schicht das selbe Objekt benötigen z.B. eine DB-Verbindung, so kann das selbe Objekt auch an mehrere Zweige durchgereicht werden, sodass zwei Klassen das selbe Objekt verwenden können.

3. Verwende wenn möglich immer Interfaces, sodass die dahinterliegende Implementierung austauschbar bleibt.

4. Das Schichtenmodell bezieht sich auf erstellte Objekte und deren Abhängigkeiten, nicht jedoch wie in Punkt 1 auf die bloße Kenntnis darüber, wie eine Klasse aussieht. Wenn in jeder darunterliegenden Schicht, warum auch immer, z.B. die Klassen String und DateTime verwendet werden, dann ist das in Ordnung und das Schichtenmodell ist damit trotzdem eingehalten, da es hierbei um erstellte Objekte und nicht um die Verwendung von Klassen geht.

5. Mikroservices hatte ich teilweise falsch verstanden. Ich möchte nicht für jede Funktion eine eigene Anwendung erstellen, welche dann für sich selbst läuft, aber ich möchte für jede zusammenhängende Funktion eine eigene Bibliothek erstellen, welche ich dann bei diversen Anwendungen wiederverwenden kann. So z.B. eine Bibliothek für DB-Zugriffe, eine für REST-API zugriffe, eine zum einlesen von Excel-Daten, eine Klasse ,,Konvertierung", welche bei mir häufig verwendete Konvertierungsfunktionen bereitstellt etc.
Dann kann ich in meinen Projekten einfach auf die jeweiligen DLL-Dateien zugreifen und die Funktion immer wiederverwenden, ohne das Rad jedes mal neu zu erfinden.

Ich denke mit diesem modularen Ansatz müsste ich auch in größeren Projekten gut fahren. Habe ich das soweit richtig aufgefasst?

LG Ghost.
 
Zurück
Oben