Mehrschichtigkeit ist immer noch tief in den Köpfen verankert. Und wenn nicht in den Köpfen, dann im Code. Übel daran ist nicht nur, dass Mehrschichtigkeit viele Fragen weder beantwortet noch stellt, sondern dass damit Abhängigkeiten geschaffen werden. Code der einen Schicht hängt irgendwie von Code der anderen ab. Die eine Schicht braucht die andere.
Aber welche Schicht sollte welche brauchen? Darüber hat Jens Schauder in einem Blogartikel nachgedacht. Den habe ich gelesen – und dann habe ich gestutzt. Jens erkennt zurecht Abhängigkeiten als Problem – doch dann sieht er eine Lösung nur darin, dass er sie umdreht. Aus dem üblichen
wird bei ihm nur
Die Domäne soll Interfaces definieren, die UI/Persistence dann nutzen bzw. implementieren [1].
Das empfinde ich als keinen großen Schritt nach vorne. Klar, irgendwie wird jetzt die Domäne etwas mehr betont. Das ist nicht schlecht. Aber das grundsätzliche Problem der Abhängigkeiten wird nicht angegangen.
Was ist dieses grundsätzliche Problem der Abhängigkeiten? Naja, dass das Abhängige das Unabhängige braucht, um etwas leisten zu können. Wer vom Alkohol abhängig ist, der ist nicht arbeitsfähig ohne. Und wo ein UI abhängig ist von der Domain, da kann das UI nichts tun, solange es keine Domain-Schicht in der Hand hat.
Auch das hochgelobte Prinzip IoC macht da keinen großen Unterschied. Aus statischen Abhängigkeiten werden damit nur dynamische. Zur Laufzeit – also auch bei Tests – muss die Abhängigkeit erfüllt werden. Attrappen sind insofern weniger Lösung als Problem, dito DI-Container. Beide kaschieren die fundamental starre Verbindung, die Abhängigkeit selbst.
Ich sehe dafür auch keine Entsprechung in der sonstigen Technik. Wenn ich UI+Domain+Persistence mal zusammen als Application bezeichne, dann könnte die Analogie doch ein Motor sein, oder?
Eine Application besteht aus Teilen, ein Motor auch. Nur im Zusammenspiel aller Teile ergibt sich die gewünschte Funktion des Ganzen.
Jetzt aber der Unterschied, der entscheidende:
In der Application braucht und kennt UI die Domain, und bei Jens braucht und kennt Persistence ebenfalls Domain. UI wie Persistence sind unvollständig ohne Domain. Oder allgemeiner: das Abhängige ist unvollständig ohne das Unabhängige.
Im Motor hingegen… da weiß eine Zündkerze nichts vom Zylinder. Ein Zylinder weiß nichts von einer Zündkerze. Die Kurbelwelle weiß nichts von Kolben und umgekehrt.
In der materiellen Technik gibt es keine Abhängigkeiten wie in der Software.
In der materiellen Technik gibt es nur Teile, die für sich genommen eine Form und eine Funktionsweise haben. Sie folgen dem, was ich mal
Das Prinzip der gegenseitigen Nichtbeachtung
(principle of mutual oblivion (PoMO))
nennen will.
Eine Zündkerze beachtet kein anderes Teil. Ein Kolben, eine Kurbelwelle, ein Keilriemen auch nicht. Alle sind für sich geformt und ausgeprägt.
Dass man sie zusammenfassen kann zu etwas Größerem: klar. Dieses Größere definiert einen Kontext und fordert, dass die Teile zusammenpassen. Es ist die Existenzberechtigung für die Teile. Ohne das Größere müssten wir gar nicht drüber reden.
Aber das große Ganze kann eben in unterschiedlicher Weise seine Teile formen. Es kann das wie beim Motor tun. Das bedeutet, die Teile beachten einander nicht – sondern dienen dem Ganzen, das sie integriert.
Oder das Ganze kann es tun wie in der Software üblich. Das bedeutet, es macht die Teile abhängig von einander.
Konkret steckt diese Abhängigkeit in zwei unheiligen Verbindungen:
- Es wird Input mit Output verbunden: Request/Response ist der vorherrschende Kommunikationsstil zwischen den Softwareteilen.
- Es werden mehrere Funktionalitäten im selben Interfaces zusammengefasst.
Die erste unheilige Verbindung macht den Übergang von synchroner zu asynchroner und damit zu verteilter Kommunikation schwierig. Außerdem führt sie zu einem unbeschränktem Wachstum von Methoden.
Die zweite unheilige Verbindung macht die Umkonfiguration von Funktionalität schwierig – trotz allem ISP- und IoC-Aufwand, den man treiben mag.
Beim PoMO hingegen ist das alles kein Thema mehr. Denn beim PoMO gibt es keine Abhängigkeiten mehr zwischen “Kooperationspartnern”. Funktionseinheiten kennen einander nicht mehr. “Dienstleister” UI kennt keinen “Dienstleiste” Domain oder umgekehrt, dito “Dienstleister” Persistence.
Wenn die Anwendungsarchitektur dem PoMO folgt, dann sieht sie zunächst so aus:
Welch Wonne! Keine Abhängigkeiten, kein Testattrappendrama!
Aber natürlich sind die “Schichten” jetzt gänzlich unverbunden. Da entsteht noch keine Gesamtfunktionalität im Sinne der Application.
Dafür tut nun die Application etwas. Und das tut sie ganz offiziell. In der Abhängigkeitsarchitektur war das nicht so offensichtlich. Aber bei PoMO geht es nun nicht mehr anders. Insofern führt PoMO zu einer fundamentalen Separation of Concerns:
UI, Domain und Persistence gehören zum Concern (Aspekt) “Operation”. Und die Application vertritt den Aspekt “Integration”. Die Application verbindet die Teile so, dass sie zusammen etwas sinniges tun. Diese Verbindung könnte so aussehen:
Grün integriert, schwarz operiert. Teile wissen nichts davon, ob und wie sie verbunden sind. Und die integrierende Funktionalität weiß nichts davon, wie die Teile funktionieren. Sie ist ausschließlich von deren Form abhängig.
Und genau das macht den Unterschied aus zwischen einer PoMO-Architektur und dem Üblichen: Bei den üblichen Abhängigkeiten ist der Abhängige nicht nur von der Form des Unabhängigen abhängig, also dass zum Beispiel B eine Funktion mit einer Signatur wie Func<string,int> implementiert. Nein, A ist auch vom Namen der Funktion abhängig und damit von ihrer Semantik, dem, was sie leistet.
Das klingt ganz natürlich. Aber es ist eine Last. Das ist eine doppelte Abhängigkeit: eine von Form und Inhalt. Und dann obendrein noch die Abhängigkeit davon, dass diese Funktion auf einem bestimmten Interface implementiert ist und auch noch, dass sie ein Ergebnis zurückliefert. Puh… das sind ganz schön viele Abhängigkeiten, oder?
Bei PoMO jedoch, da kennt ein A kein B. Und es gibt keine Rückgabewerte; Daten fließen unidirektional. Und ob Methoden von der einen oder einer anderen Funktionseinheit angeboten werden, ist der integrierenden Funktionalität egal. Sie bringt “Angebote” einer Funktionseinheit mit “Nachfragen” anderer zusammen. Das ist alles.
Integration interessiert sich somit nur für Semantik. Das muss sie ja auch, denn sie definiert ein höheres Abstraktionsniveau.
PoMO löst damit mehrere Probleme von Abhängigkeiten. Manche dadurch, dass Abhängigkeiten abgeschafft werden. Manche dadurch, dass Abhängigkeiten fokussiert werden im Sinne von SoC und SRP.
Das scheint mir the way to go, um mal raus aus dem Mehrschichtigkeitssumpf zu kommen.
Fußnoten
[1] Am Anfang seines Artikels stellt Jens die Frage, was denn Teams überhaupt mit einem Pfeil “–>” in einem Diagramm meinen. Ob da immer Klarheit herrsche? Das kenne ich auch: Es wird bildlich entworfen, Kästchen, Kreise, Pfeile, Linien werden gemalt… Aber was bedeuten die? Zwei Entwickler, drei Meinungen ;-)
Für mich gibt es zwei zentrale Symbole für Beziehungen zwischen Funktionseinheiten:
- Pfeil, A –> B
- Lolli, A –* B
Der Lolli bezeichnet eine Abhängigkeitsbeziehung: A ist abhängig von B, d.h. A braucht B in irgendeiner Weise. A hat zur Laufzeit ein B in der Hand.
In den meisten UML-Diagrammen und auch bei Jens steht für diese Art der Beziehung allerdings der Pfeil. Der ist mir dafür aber verschwendet, weil bei ihm gar nicht recht klar wird, was die Pfeilspitze aussagt. Ein Pfeil ist anders als eine Linie asymmetrisch. Das ist schon passend zur Abhängigkeit. Abhängiges und Unabhängiges haben unterschiedliche Rollen. Doch die Pfeilspitze trägt für mich dabei zuviel Gewicht. Sie ist verschwendet bei einer simplen Asymmetrie.
Bei einem Datenfluss hingegen kommt die Pfeilspitze viel besser zum Einsatz. Da zeigt sie deutlich die Flussrichtung. Das entspricht ihrer üblichen Bedeutung von “Bewegungsrichtung”. In Diagramme stehen Pfeile für mich also dort, wo Daten fließen, z.B. von Funktionseinheit A zu Funktionseinheit B.
Solcher Datenfluss ist auch asymmetrisch; Datenproduzent und Datenkonsument haben unterschiedliche Rollen. Und Datenfluss ist unidirektional.
25 Kommentare:
Und wie würde beispielhaft die Application, UI, etc. aussehen? In Code?
Also ich sehe das so:
Abhängigkeiten sind grundsätzlich nichts Schlechtes sondern ein Funktion (statisch typisierter) Sprachen, anhand derer ein Compiler helfen kann, die Korrektheit eines Programmes zu überprüfen. It's not a bug, it's a feature!
Wer dieses Feature nicht will oder zu brauchen glaubt, kann Abhängigkeiten mit verschiedenen Mitteln wie IoC, Duck Typing, dynamischen Sprachen bis hin zu Events graduell abschwächen oder gar ganz auflösen. Nichts davon ist aber per se besser als das andere. Auf Abhängigkeiten zu verzichten hat seinen Preis (keine Korrektheitsüberprüfung, Mehraufwand der Entkoppelung, mehr Tests, etc.). Man tauscht also lediglich eine Art von Problemen gegen eine andere ein. Wie immer in solchen Fällen gilt es, Vor- und Nachteile verschiedener Lösungen im konkreten Fall jeweils gegeneinander abzuwägen.
Abhängigkeiten gibt es in allen Bereichen - auch in gegenständlichen Dingen. Das kann jeder bestätigen, der schon mal versucht hat, einen IKEA-Schrank ohne den passenden Imbusschlüssel zusammenzubauen. Eine Schraube ist ein Interface, eine Brustwarze ist ein Interface, ein Lenkrad ist ein Interface, das mit bestimmten Erwartungen verknüpft ist, welche Objekte wie damit interagieren sollen. Welche konkreten Objekte das sind, und wie sie ihre Aufgabe erfüllen, ist dabei egal. Das ist dann eben ein Detail der Implementierung.
Mit dem Argument, alles hätte ja seine Vor- und Nachteile, kommen wir nicht recht weiter. Damit kann ich das Rauchen und Narcolepsie auch "schönreden".
Wir müssen genauer hinschauen. Und da kann ich nur sehen: die Abhängigkeiten, die auch mal Vorteile haben können, führen sehr, sehr oft zu Nachteilen, nämlich schwer wartbaren Systemen.
Am schlimmsten sind natürlich logische Abhängigkeiten, vor denen kein Compiler schützt - und die wir weitgehend auch nicht wegbekommen. Aber bald darauf folgen dann "Dienstleistungsabhängigkeiten" wie die beschriebenen.
Das Problem mit diesen Abhängigkeiten geht nicht weg mit statischer Typisierung oder ohne.
Code, bei dem erstens "Dienstleistungsabhängigkeiten" existieren und zweitens Logik über diese Dienstleistungshierarchien hinweg auf allen Ebenen verteilt ist, ist fragil. Änderungen auf einer Ebene setzen sich entlang der Abhängigkeiten leicht nach oben und unten fort.
"Ohne Sprunganweisungen kommt man nicht aus" ist genauso wahr wie "ohne Abhängigkeiten geht es nicht." Und trotzdem scheint die Softwareentwicklung davon profitiert zu haben, Sprunganweisungen zurückgedrängt zu haben. Selbst in C# gibt es noch ein goto - nur benutzt es keiner.
Ein goto hat Vor- und Nachteile. Da muss man abwägen. Aber diese Abwägung fällt sooft gegen das goto aus, dass wir sie im Grunde nicht mehr treffen. Wir leben wunderbar ohne.
Und ich treffe auch keine Abwägung mehr bei Abhängigkeiten. Ich lebe wunderbar ohne. Jedenfalls ohne Dienstleistungsabhängigkeiten wie sie bei der Mehrschichtigkeit vorherrschen. Solch ein Leben ist also mindestens mal möglich :-) Und hier versuche ich zu vermitteln, dass es sogar wünschenswert ist.
Interessante Gedanken Herr Westphal, zeigen Sie in Ihrem nächsten Beitrag auch ein praktisches Beispiel dieser Architektur?
@Anonyme: Wenn jetzt schon zwei nach einer Umsetzung in Code fragen, dann muss ich wohl ein Beispiel geben.
Kommt in den nächsten Tagen. Stay tuned ;-)
Hallo, auch ich interessiere mich für die Implementierung, dann sich wir schon zu dritt.
Ich denke, die Abhängigkeitsfreiheit bekommt man im technischen Sinne hin, aber logisch kann man die Trennung nicht aufbrechen.
Technisch kann man auf Schnittstellen (C# interfaces) verzichten, wenn man sich Delegates bedient.
Der Diensterbringer bietet die Methode void Foo(Bar bar, Baz baz) an, das entspricht der Action<Bar, Baz>
Der Aufrufer weiß lediglich, dass er einen Dienst mit der Signatur entsprechend Action<Bar, Baz> aufrufen will.
Mit diesem Gedanken kann man beide Partner verdrahten, ohne dass dazu ein C#-interface erforderlich ist.
Aber die DTOs Bar und Baz müssen beide Partner kennen.
Davon kann man sich auch trennen, wenn man generische Datenstrukturen (z.B. Dictionary) verwendet. Dann hat man beide Partner vollkommen entkoppelt, hingegen bleibt die logische Abhängigkeit bestehen. Das Aufbrechen von fachlichen Zusammenhängen ist im konkreten Anwendungsfall vielleicht unmöglich.
Ich befürworte sogar, keine generischen Datenstrukturen zu verwenden und stattdessen konkrete DTOs zu verwenden - das hilft dem Entwickler (-> IntelliSense). Versionkompabilitäten im Falle von DTO-Änderungen sind mit entsprechender Vorsicht zu behandeln. Vielleicht sollte man das verbieten (einmal veröffentlichte DTOs gelten ewig) und Änderungen nur durch Mutation (Kopie+Änderung) umsetzen. Das mag DRY verletzen, doppelter Code entstehen, aber vielleicht ist der Preis bezahlbar (Bar V1.1, Bar V1.2, Bar V2.0, Bar V2.1). Die älteren DTOs verwinden von selbst aus dem System, weil sie keine mehr nutzt.
Viel wichtiger halte ich die grundlegenede Diskussion über Abhängigkeiten (Schichtenarchitekturen sind ein verbreiterer Spezialfall), also man hat zwei unabhängige Funktionseinheiten (oder Komponenten) und möchte diese miteinander verknüpfen. Die Diskussion kann man aber an einer Schichtenarchitektur führen.
An dieser Stelle nur soviel: Es reicht eben nicht, Interfaces durch eine Strauß an Delegaten zu ersetzen.
Es kommt darüber hinaus darauf an, was mit Foo(Bar, Baz) bedacht ist. Und solange das eine Dienstleistung ist, ist es auch meiner Sicht noch keine "Nichtbeachtung". Dann interessiert sich A noch konkret dafür, dass B eben den Service Foo() anbietet.
Genau das soll aber eben nicht sein. A interessiert überhaupt nicht, was mit seinen Produkten passiert.
Dass es logische Abhängigkeiten immer geben wird, ist klar. Sobald es zwei Funktionseinheiten in einer Software gibt, sind die irgendwie logisch abhängig.
Und auch Abhängigkeiten von gemeinsamen Daten gehen nicht weg. Aber die sind nicht so schlimm, solange es eben nur Daten sind.
Die große Last der Unwartbarkeit entsteht durch funktionale Kopplung.
Ja, so meinte ich es.
A "produziert" das Tupel (Bar,Baz) und die Infrastruktur (Application) weiß (per Konfiguration), dass sich B für (Bar,Baz) interessiert (bietet den Service Foo an), daher routet die Infrastruktur das Tupel (Bar,Baz) von A an B weiter.
In diesem Szenario sind A und B relativ eng gekoppelt - unabhängig davon wie der Verteilungskontext aussieht: in-process oder distributed und ebenfalls unabhängig davon, wie der zeitliche Zusammenhang (on-the-fly oder batch) zwischen beiden aussieht.
Die Abhängigkeit ist dadurch definiert, dass beide Bar und Baz kennen und interpretieren können (und weitere lustige Interna daraus ableiten). Beide Einheiten A und B gehören entweder zur gleichen Domäne oder haben Adapter zwecks Mapping integriert (aber dann würde ich eher von A->M->B, mit M = Mapper) sprechen.
Ist das die funktionale Kopplung?
Ralf,
ich bin sehr gespannt, wie Du die Kurbelwelle eines VW in den Motor eines Opels integrierst :-)
@Anonym#1: Aber es geht doch nicht darum, jedes Teil in jedem Kontext benutzen zu können.
Der Trick ist aber: wenn einer meint, mit der Kurbelwelle eines Opels arbeiten zu können, dann kann er das. Davon merkt die Opelkurbelwelle nichts. Sie ist selbst von nichts abhängig. Sie braucht keine Dienstleistung. Sie definiert sich rein selbst.
@Anonym#2: Ich sehe das nicht als enge Kopplung von A und B beide von Datentyp T abhängig sind.
Das ist so, wie ein Motor Diesel braucht und eine Raffinerie Diesel herstellt. Wunderbar. Aber der Motor braucht keine Raffineriedienstleistung und die Raffinerie keine Motordienstleistung.
Sie sind aus meiner Sicht eben nicht funktional gekoppelt. Aber sie teilen etwas, klar. Es gibt eine "Materialkontrakt": der eine ist Produzent, der andere Konsument.
Immer diese (blöden) Analogien.
Eine Kurbelwelle wird in keine Labor from Scratch - einfach so entworfen. Nein, Form (Maßen, Toleranzen) und Güte (Kräfte, Ausdehnung, Materialeigenschaften) sind sehr genau spezifiziert (bspw. Testreihen, Nullserien). Eine Kurbelwelle in irgendeiner Lagerhaltung weiss sehr wohl nichts davon, ob sie jetzt eingebaut ist oder nicht (eine tolle Erkenntnis, Material ist dumm).
*ich lasse jetzt die Polemik*
Nein, die Kurbelwelle ist (hoffentlich durch nachgewiesene Qualitätsmaßnahmen) "jederzeit" in der Lage gemäß ihrer Spezifikation eingesetzt zu werden. Man kann sie auch abseits der Spezifikation einsetzen (vielleicht als Hebel), aber deren Auswirkungen (Verbiegung) mag sie für immer als unbrauchbar deklarieren. Tja, soetwas hat man bei Software nicht.
>... enge Kopplung von A und B beide von Datentyp T
Die "Enge" mag relativ sein und Bedarf sicherlich einer genaueren Definition.
Tatsache ich aber, dass A und B nicht von irgendeinem T, sondern von einem ganz konkreten T in der Version Vx.y.z abhängen.
In der Version V1.0 enthält das T vielleicht nur string Location, in der Version V2.0 gibt es Location in dieser Form nicht mehr, sondern ist eine Schnittstelle oder eine Attributsammelung.
Software ist für mich Modellbildung. Modelle sind nur eine sehr eingeschränkte Sicht der Wirklichkeit. Zwei Funktionseinheiten müssen sich jetzt auf diegleiche Modellbildung "einigen" (Attribute, Kardinalitäten) und zwar unabhängig wieviel die jeweiligen Einheiten an Daten benötigen.
(irgendwie erinnert mich das an die Diskussion um DCI ein paar Blogs vorher)
Ein Zusammenhang sollte schon existieren; ansonsten driftet das Ganze in die Beliebigkeit ab und ich habe irgendwas total generisches, was ich nur wieder konfigurieren, mit einem Aufwand, der größer ist, als das mehr oder minder hartcodierte.
Um den Begriff DCI, oder besser die Architektur-Idee von Coplien und Bjørnvig, wieder ins Spiel zu bringen, stimme ich RalfW schon zu, dass das, was sich verändern kann, lose gekoppelt sein sollte, während die Einzelteile, die das System ausmachen, ein entsprechende Kopplung brauchen.
Die grünen Pfeile liefern den Kitt für das System, der aber auch wieder entfernbar sein sollte, wenn sich der Kunde überlegt, dass er einen Zusammenhang anders haben möchte (nicht dass er so etwas jemals tun würde...).
Bezüglich Modelle: Es ist unsere Aufgabe im Rahmen des Software-Engineering sich darüber Gedanken zu machen, was ist fest und was nicht... fast wie im Leben eine Ingenieurs ;)
@stmon:
Kannst Du die URL posten? Dann verkürzt sich die Diskussion durch Redunanzvermeidung.
> ..dass das, was sich verändern kann, lose gekoppelt sein sollte
Das ist z.B. eine Strategy - DCI geht darüber hinaus. Sollbruchstellen für Variantionspunkte wollte ich nicht ausgeschliessen.
> ...Die grünen Pfeile liefern den Kitt für das System
Genau. Nur reicht das Bild und deren Beschreibung allein nicht aus. Es scheint, als ob schwarze Linien "böse" sind, die grünen Pfeile (das Kitt) offenbar als gut erachtet wird. In der Zeichnung war ein Zusammenhang erkennbar: der grüne Rahmen der Application schliesst die schwarzen Boxen UI, Persistence und Domain ein.
Das deutet auf eine Abhängigkeit (=Import von Symbolen) und/oder gar eine Schnittstelle hin - leider wurde das nur vage beschrieben und bisher hat die Diskussion auch noch nicht wirklich für Eindeutigkeit gesorgt.
Dieses gilt und galt es zu schärfen... warten wir einmal die Implementierung von Ralf ab.
Offenbar wird die "Abhängigkeit von Daten" - genauer einer Datenstruktur (DTO) - als weniger böse angesehen, als die zu einem Interface.
@Anonym... der nach meinem Post :)
http://blog.ralfw.de/2012/10/gesucht-dreckige-realitat.html
> DCI...
Jep... DCI geht von der Idee natürlich weiter, aber mir ging es an dieser Stelle ein bisschen mehr um die Trennung "What The System Is" und "What The System Does" und um die Umsetzung (wobei ich das Beispiel in "Lean Architecture" eher als akademisch empfand)
> Dieses gilt und galt es zu schärfen... warten wir einmal die Implementierung von Ralf ab.
Nu' machen Sie mal hin, Herr Westphal ;)
@stmon: Danke für die Links (das Buch ist schon gelesen).
@Herr Westphal: Bitte keine Übereile.
Ich danke den Herren für die bisherige Diskussion.
Ein gelungener Artikel, vielen Dank. Er beschreibt ein fundamentale Problem, das ich schon lange in einigen Köpfen wahrgenommen habe.
Zum Kommentar von Dominik Schmid.
Wenn ich lese das Abhängigkeiten nichts schlimmes sind dann behaupte ich das Sie den Testdriven-Ansatz noch nicht erprobt haben oder leben.
Erst beim Versuch eine hohe Code Coverage durch Unit-Tests und TDD zu erreichen wird man bemerken wie pervers Abhängigkeiten sind.
Hallo Herr Westphal,
ein interessanter Artikel. Bei einer Implementierung eines solchen Ansatzes, würden sich die einzelnen Komponenten (UI, Domäne, Persistenz) dann an die Integration koppeln? Wäre die Integration ein Mediator zwischen den Komponenten?
Viele Grüße
@Robin: In der Integration kommen die Teile zusammen. Insofern ist die Integration abhängig :-( Aber das ist weniger schlimm als das, was wir heute haben, wenn die Aspekte munter durcheinander fliegen.
Die Integration tut ja nur eines: integrieren. That´s it. Also ist die nur von der Semantik der zu integrierenden Teile abhängig - allerdings nur indirekt. Denn sie selbst braucht deren Semantik ja nicht. Sie steckt nur Semantiken zusammen.
Die Kopplung zwischen Integration und Teilen ist mithin trotz Abhängigkeit relativ klein.
Hi Ralf,
da hast du meinen Artikel wohl ein bischen falsch verstanden.
Es ging mir mit nichten darum Abhängigkeiten umzudrehen, sondern nur um zu klären, in welche Richtung sie gehen. Ich halte die von mir beschriebene für die Richtige und Normale, die vielen aber nicht klar zu sein scheint.
Zu der Behauptung es ginge ohne Abhängigkeiten ... nun, dass der Vergleich mit dem Motor mehr als hinkt wurde ja schon in verschieenen Kommentaren genannt. Natürlich gibt es Abhängigkeiten.
Ich denke diese Abhängigkeiten sollten möglichst einfach sein. Ein M8 Gewinde ist besser also so mancher Spezialstecker von Diagnose Geräten. Eine Funktion besser als ein Interface mit 20 Methoden.
Mir scheint deine Argumentation darauf hinaus zu laufen die Interfaces so weit zu vereinfachen, dass sie alle glecih aussehen. Alle Ansätze die ich bisher dazu gesehen haben versteckten die Abhängigkeiten bestenfalls. Und eine versteckte Abhängigkeit, ist noch schlimmer als eine offensichtliche.
@Jens: Nein, es geht mir nicht darum, alles gleich aussehen zu lassen.
Aber es geht mir schon darum, mit den Abhängigkeiten, die du gezeichnet hast, aufzuräumen. Denn du hast Dienstleistungsabhängigkeiten gezeichnet. Layer X benutzt Funktionalität von Layer Y. Die BL ruft das DL. Oder auch andersherum.
Mir ist die Richtung der Abhängigkeit und auch eine Invertierung egal. Ob ein Layer X sich von einer Klasse y in Layer Y direkt abhängig macht oder nur von einer "Abstraktion" (sprich Interface), ist mir einerlei. Beides ist dasselbe Problem - solange eben Layer X eine Dienstleistung beansprucht.
Dazu habe ich in anderen Postings hier und Kommentaren mittlerweile hinreichend erklärt, worin ich den für viele vielleicht feinen, aber für mich gravierenden Unterschied sehe.
Es geht nicht ohne jegliche Abhängigkeit. Klaro. Aber es geht ohne den Verhau, der überall ständig produziert wird. Und der hat nichts mit der Richtung zu tun, in die die Abhängigkeitspfeile zeigen.
Code wird schlicht unübersichtlich und schwer zu warten, wenn Integration und Operation als Aspekte nicht getrennt werden. Und Code in einer Methode wird lang und länger und klebrig, wenn Dienstleistungen beauftragt, statt einfach nur "Daten zur gefälligen Weiterverarbeitung" zu veröffentlichen.
Damit will ich Funktionen nicht abschaffen - aber ich empfehle, sie viel bewusster einzusetzen, eben nur in Integrationen.
@Ralf: Das klingt zu einfach, als dass es bislang noch keinem eingefallen wäre. Ohne Codebeispiel dreht sich die Diskussion hier meiner Meinung nach im Kreis...
Codebeispiele habe ich nun inzwischen zur Genüge gebracht, glaube ich.
Jeder ist auch herzlich eingeladen, es selbst auszuprobieren. Das ist ja technisch alles nicht schwierig.
1. Integration Operation Separation Principle (IOSP)
Eine Methode integriert entweder oder sie operiert. Wenn sie integriert, dann enthält sie keine Kontrollstrukturen und keine Ausdrücke. Zuweisungen und Objekterzeugungen sind aber erlaubt.
In einer Aufrufhierarchie sollten soviele Ebenen wie möglich integrieren.
2. Principle of Mutual Oblivion (PoMO)
Klassen bzw. Methoden rufen keine Dienstleister mehr auf. Stattdessen informieren sie ihre Umwelt über Ereignisse/Ergebnisse.
Diese "selbstbezogenen" Funktionseinheiten werden von integrierenden zu Größerem "verdrahtet".
Ist das noch keinem eingefallen? Doch. PoMO ist zum Beispiel bei Unix immer dann am Werk, wenn auf der Kommandozeile mit Pipes gearbeitet wird.
Nur scheint mir, dass es niemand bisher so knapp als Prinzip formuliert hat, das unabhängig von irgendwelchem Hype ist.
Das IOSP hingegen scheint mir tatsächlich neu. Aber warum auch nicht? Kann jedem passieren, mal ein Prinzip zu erfinden ;-)
Not rocket science - so etwas implementiert eine Datenflussmaschine vermöge eines Eventbusses oder auch ein ESB (Enterprise service bus, java) seit Jahren, oder fehlt da etwas an Funktionalitaet?
Gruss
Martin Prange
@Martin: Wer spricht von rocket science?
Aber auch wenn es keine rocket science ist, kann es noch einfacher werden. Warum sollte ich dafür einen ESB einsetzen?
Es geht mir darum, dass, was woanders schon funktioniert, verständlicher und nutzbarer für viel mehr Entwickler zu machen.
Kommentar veröffentlichen
Hinweis: Nur ein Mitglied dieses Blogs kann Kommentare posten.