Follow my new blog

Sonntag, 21. April 2013

Warnung vor der funktionalen Abhängigkeit

Immer noch bin ich dem Gefühl auf der Spur, warum funktionale Abhängigkeiten "böse" sind. Sie sind so natürlich, so naheliegend - und doch beschleicht mich immer wieder Unwohlsein. Ich versuche schon lange, sie zu vermeiden. Deshalb betreibe ich Flow-Design. Doch einem Gefühl allein will ich mich da eigentlich nicht hingeben. Es mag als Auslöser für eine Veränderung alter Gewohnheiten sehr nützlich sein, nur ist es schwer, rein aufgrund eines Gefühls diese Veränderung weiter zu tragen. Deshalb will ich noch Mal genauer hinschauen. Gibt es eine rationale Erklärung für mein Gefühl? Ich habe dazu schon Anläufe hier im Blog genommen. Doch jetzt will ich es mehr auf den Punkt bringen.

Funktionale Abhängigkeit

Für mich besteht eine funktionale Abhängigkeit, wenn...
  1. eine Funktion c() wie Client eine Funktion s() wie Service aufruft und
  2. c() selbst die Parameter für s() durch Berechnungen vorbereitet und
  3. c() selbst das Ergebnis von s() selbst in Berechnungen weiterverarbeitet.
Wenn c() in dieser Weise s() benutzt, dann braucht c() s().

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  return ... z ...;
}

c() ist funktional abhängig, weil c() Logik enthält, die auf s() abgestimmt ist. Allemal sind das die Berechnungen, die auf den Aufruf von s() folgen.

Wie gesagt, so zu arbeiten, ist ja total normal. Am Ende können und wollen wir auch nicht ohne. Nur ist mein Gefühl, dass wir zu oft in diesem Stil arbeiten. Zu gedankenlos gehen wir die Probleme ein, die er mit sich bringt. Das Resultat: Code, der sich alsbald nur noch schwer verändern lässt. So werden Brownfields produziert.

Problem #1: Bereitstellungsaufwand

Das erste Problem mit solcher funktionalen Abhängigkeit scheint ein Feature - ist aber ein "Bug". Es besteht darin, dass c() überhaupt auf s() zugreifen muss. Das kann in statischer Weise geschehen: c() bzw. die Klasse, zu der c() gehört, kennt die Klasse, zu der s() gehört. Also wird bei Bedarf eine Instanz der unabhängigen Serviceklasse erzeugt:

int c(int a) {
  U u = new U();
  ...
  int y = u.s(x);
  ...
}

Ok, dass das "böse" ist, wissen wir inzwischen alle. Bestimmt tun Sie das schon lange nicht mehr, sondern folgen dem Prinzip Inversion of Control (IoC) und benutzen Dependency Injection (DI).

class A {
  IU _u;
  public A(IU u) { _u=u; }

  public int c(int a) {
    ...
    int y = _u.s(x);
    ...
  }
}

Die abhängige Klasse A von c() kennt nur eine Abstraktion der unabhängigen Klasse U von s(), z.B. in Form des Interface IU. Jedes A-Objekt bekommt dann ein Objekt irgendeiner Klasse injiziert, die IU implementiert. Das kann eine Instanz von U sein, aber auch eine Attrappe.

Aus der statischen Abhängigkeit ist somit eine dynamische geworden. Die Kopplung zwischen c() und s() ist gesunken. c() ist besser testbar. Wunderbar. Und dennoch...

Das Problem besteht für mich darin, dass wir uns überhaupt um so etwas Gedanken machen müssen. IoC/DI sind nur Symptomkuren. Ohne funktionale Abhängigkeit würde sich die Frage nach statischer oder dynamischer Kopplung nicht einmal stellen.

Wer das Single Responsibility Principle (SRP) ernst nimmt, der muss doch sehen, dass A hier zwei Verantwortlichkeiten hat. Da ist zum einen die Hauptverantwortlichkeit von A, seine Domäne, in der c() eine Funktionalität bereitstellt.

Darüber hinaus leistet A aber auch noch auf eine technischen Ebene etwas. A stellt s() bereit. Auf die eine oder andere Weise. Dass c() überhaupt Zugriff auf s() haben kann, ist eine eigenständige Leistung, die nicht zur "Geschäfts"domäne von A gehört.

Funktionale Abhängigkeit vermischt zwei Aspekte. Das wird noch deutlicher beim zweiten Problem, das ich sehe:

Problem #2: Topologische Kopplung

Die Vermischung der Domäne mit dem "Bereitstellungsaspekt" mag für Sie quasi schon unsichtbar sein. Sie ist total normal und scheint unvermeidbar. Aber das ist noch gar nichts. Es wird noch subtiler.

Nehmen wir an, c() ist funktional abhängig von zwei Funktionen, s1() und s2().

class A {
  IU _u;
  public A(IU u) { _u=u; }

  public int c(int a) {
    ...
    int y1 = _u.s1(x);
    ...
    int y2 = _u.s2(z);
    ...
  }
}

Was passiert, wenn Sie in einem Anfall von SOLIDer Programmierung das Interface Segregation Principle (ISP) anwenden und sich entscheiden, s1() und s2() auf verschiedene Interfaces zu legen?

Sie müssen A ändern. Obwohl sich an der Domäne von A nichts geändert hat. Das ist ein klarer Widerspruch zum SRP, das "once [business] reason to change" zur Einhaltung fordert.

Die gut gemeinte Refaktorisierung nach dem ISP zieht so einiges nach sich für A:

class A {
  IU1 _u1;
  IU2 _u2;
  public A(IU1 u1, IU2 u2) { _u1=u1; _u2=u2; }

  public int c(int a) {
    ...
    int y1 = _u1.s1(x);
    ...
    int y2 = _u2.s2(z);
    ...
  }
}

A muss den Änderungen an der Verteilung der Servicefunktionen auf Interfaces nachgeführt werden, weil A nicht nur von s1() und s2() abhängig ist, sondern auch daran gekoppelt ist, wo (!) diese Funktionen definiert sind. A ist an die Topologie ihrer Umgebung gekoppelt.

Auch das scheint alles ganz normal und unvermeidbar. Dafür gibt es Refaktorisierungswerkzeuge, oder?

Könnte man so sehen. Aber ich finde es besser, sich gar nicht erst in den Fuß zu schießen, als die Wunder der modernen Medizin loben zu müssen.

Interessanterweise wird die topologische Kopplung zu einem umso größeren Problem, je sauberer Sie mit funktionalen Abhängigkeiten programmieren wollen. Denn desto mehr Klassen bekommen Sie (SRP) und desto mehr Interfaces auf diesen Klassen wird es geben (ISP). Der Anteil an "Bereitstellungsinfrastruktur" in den abhängigen Klassen wird immer größer.

Problem #3: Grenzenloses Wachstum

Umfangreiche Funktionen kennen Sie bestimmt auch. Ich habe schon welche mit 10.000 Lines of Code (LOC) gesehen. Sie kennen auch die Empfehlungen, den Funktionsumfang zu begrenzen. Manche sagen, Funktionen sollten nur 10 oder 20 oder 50 LOC enthalten. Andere sagen, sie sollten nicht länger als eine Bildschirmseite sein. Wieder andere beziehen sich nicht auf die LOC, sondern auf die "Komplexität" des Codes in Funktionen (die mit der Zahl der möglichen Ausführungspfade durch ihn zu tun hat).

An guten Ratschlägen mangelt es wahrlich nicht, das uralte Problem der beliebig umfangreichen Funktionen in den Griff zu bekommen. Nützen tun sie aber offensichtlich wenig. Denn bei allem guten Willen wachsen Funktionen auch bei Ihnen wahrscheinlich weiterhin. Sie mögen das Wachstum bewusster verfolgen, es in Code Reviews immer wieder diskutieren, Maßnahmen zur Eindämmung ergreifen (Refaktorisierung)... Doch am Ende ist jede Metrik dehnbar. Ihre besten Absichten können und werden Sie aussetzen, wenn eine andere Kraft nur stark genug ist.

Das ist so. Darüber sollten wir uns nicht grämen. Es bleibt nämlich so. Unser Fleisch ist schwach. Der Nahkampf mit dem Code im Tagesgeschäft ist dreckig.

Trotzdem, nein, deshalb lohnt eine Frage nach der Ursache solchen grenzenlosen Wachstums. Wie kann es dazu überhaupt kommen? Das passiert ja nicht einfach. Wir tun es. Aber warum?

Wie lassen Funktionen grenzenlos wachsen, weil wir es können.

So einfach ist das.

Und das, was es uns möglich macht und motiviert, Funktionen wachsen zu lassen, das ist die funktionale Abhängigkeit.

Ja, so einfach ist das, glaube ich.

Alles beginnt mit etwas Logik in c():

int c(int a) {
  int x = ... a ...;
  return ... x ...;
}

Dann kommt eine funktionale Abhängigkeit dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  return ... x ...;
}

Und dann kommt noch eine dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  int k = t(z);
  return ... k ...;
}

Und dann kommt noch eine dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  int k = t(z);
  int l = ... k ...;
  int m = v(l);
  return ... m ...;
}

Das geht so weiter, weil nach dem Funktionsaufruf vor dem Funktionsaufruf ist.

Die eigentliche Logik von c() kann beliebig fortgeführt werden, weil ja bei Bedarf immer wieder die Berechnung von Teilergebnissen durch Delegation (funktionale Abhängigkeit) eingeschoben werden kann.

Klingt normal. Dafür sind Funktionsaufrufe ja da. Das möchte ich ganz grundsätzlich auch nicht missen.

Aber wir müssen erkennen, dass genau diese Möglichkeit dazu führt, dass abhängige Funktionen grenzenlos wachsen.

Dem können wir dann zwar mit SOLIDem Denken und Refaktorisierungen entgegenwirken - doch das sind eben Maßnahmen, die angewandt werden können oder auch nicht. Ob das SRP noch eingehalten ist, ob eine Methode schon zu lang ist und refaktorisiert werden sollte... das entsteht im Auge des Betrachters. Darüber kann man sich trefflich streiten in einem Code Review. Und im Zweifelsfall lässt man die Finger von der Veränderung laufenden Codes.

Problem #4: Syntaktische Kopplung

Wenn c() die Funktion s() aufruft, dann muss c() wissen, wie genau das zu geschehen hat. c() muss die Form, die Syntax von s() kennen. Wenn s() einen int-Parameter brauch und sein Ergebnis als int zurückliefert, dann muss c() sich darauf einstellen.

Was aber, wenn die Form von s() sich ändert? Parameter könnten zusammengefasst werden, Parameter könnten Typ oder Reihenfolge ändern... Dann muss c() sich dieser Änderung beugen.

c() muss sich mithin also ebenfalls ändern, wenn sich "nur" an der Form von s() etwas ändert. Und das hat mit der Domäne, der single responsibility von c() nicht unbedingt etwas zu tun.

Problem #5: Semantische Kopplung

Wenn c() die Funktion s() braucht, um anschließend mit deren Ergebnissen weiter zu rechnen, dann ist c() von der Semantik von s() abhängig.

Wenn s() z.B. heute eine Liste von sortierten Daten produziert, die c() weiterverarbeitet, dann kann es eine für c() relevante semantische Veränderung sein, dass s() morgen diese Liste nicht mehr sortiert zurückliefert.

Syntax und Semantik sind orthogonal. An der Syntax kann sich etwas verändern ohne Semantikveränderung. Genauso kann sich die Semantik ohne Syntaxveränderung verschieben. In jedem Fall hat c() nicht die Hoheit über seine innere Ausprägung, sondern muss sich s() beugen. Weil s() "in c() eingebettet ist".

Fazit

Das scheinen mir fünf sehr rationale Gründe für mein Unwohlsein in Bezug auf funktionale Abhängigkeiten. Das ist alles nichts Neues, würde ich sagen. Damit leben wir täglich. Und wenn die Probleme uns nicht so explizit bewusst sind, so leiden wir dennoch an ihnen. Sie verursachen uns Schmerzen beim Ändern oder Verständnis von Code.

Jedes einzelne Problem erscheint vielleicht klein. Und die Auswirkungen sind ja auch unterschiedlich nach Domäne von c() und Form und Semantik von s1(), s2() usw. In Summe jedoch machen diese Probleme aus scheinbar simplen funktionalen Abhängigkeiten eine große Last, glaube ich.

Wir werden nicht ohne funktionale Abhängigkeiten auskommen. Ich möchte die Möglichkeit, sie einzugehen, auch nicht abschaffen. Sie sind ein Mittel zur effizienten Lösung von Problemen. Das wollen wir in unserem Programmierhandwerkszeugkasten haben.

Dieses Mittel darf nur kein Selbstzweck sein. Und es darf sich nicht als alternativlos darstellen. Programmierung mit funktionalen Abhängigkeiten gleichzusetzen, würde sie auf ein unverdientes Podest heben.

Mir geht es deshalb um eine Bewusstmachung und eine Begrenzung. Bindungen bewusst eingehen und immer wieder fragen, ob sie noch ihren Zweck erfüllen, ist wichtig. Je größer die potenziellen Probleme durch Bindungen, desto mehr sollte auch geprüft werden, ob es Alternativen gibt. Denn oftmals werden Bindungen enger geschnürt als nötig. Weil man nicht weiß, wie es anders gehen könnte oder weil man die Notwendigkeit größerer Nähe überschätzt (Premature Optimization).

Mit diesen fünf guten Gründen habe ich also jetzt zumindest für mich geklärt, was mein Gefühl ausmacht, dass funktionale Abhängigkeiten "böse" sind. Ich halte sie für ausreichend, um meine Praxis der Minimierung funktionaler Abhängigkeiten fortzuführen. Denn es gibt Möglichkeiten, sie zu vermeiden.

12 Kommentare:

pvblivs hat gesagt…

Irgendwie sehe ich keine 5 Probleme. Zum Teil klingen sie so, als wenn ich sage "Problem#6: Ich muss atmen um zu leben". Das kann ich als Problem in verschiedenen Situationen sehen oder es ist eben so. Embrace breathing!

Ist semantische Kopplung, technische Kopplung nicht der Kern einer jeden technischen Realisierung von Semantik?

Ja, ich verstehe schon, das hast Du ja auch so gesagt: Du willst funktionale Abhängigkeit nicht abschaffen, eher das Bewusstsein schärfen. Aber trotzdem bin ich mit der Art und Weise wie Du das Thema schärfst, nicht ganz einverstanden.

Im Grunde, wenn man mal die Fokussierung auf die Probleme funktionaler Abhängigkeit bei Seite lässt, wirfst Du eine Designfrage auf. Vielleicht sogar die Designfrage schlechthin: Wenn ich zwei technische Komponenten habe, die jeweils ihre Aufgabe erfüllen, wie gestalte ich die Schnittstelle zwischen den beiden - und warum?

Aus solchen Fragen wurden genau Prinzipien wie das SRP oder Ziele wie hohe Kohäsion und niedrige Kopplung abgeleitet. Solche Fragen spiegeln sich sogar in Mensch-Maschine-Schnittstellen wieder. Muss der Nutzer die ganze Semantik der Waschmaschine verstehen oder reicht heutzutage mit genug Computing-Power auch ein einfacher Knopf, wenn das Ergebnis das selbe ist? Eigentlich eine einfache Frage, denkt man. Viele Menschen wollen aber die Transparenz und die Klarheit. Sie wollen wissen, dass am Ende geschleudert wird und auch mit welcher Drehzahl, sie vielleicht sogar einstellen, auch wenn sie am Ende immmer "800" auswählen.

Wie gesagt, ich verstehe Deine Probleme. Aber ich bin nicht mit ihnen einverstanden. Sie das heiße Problem herum. Und das ist: Es gibt keine beste Designlösung. Design ist immer abwägen, immer redigieren, immer Geschmacksfrage. Was nun guter Geschmack ist - darüber hast Du ja schon eine Menge geschrieben ;-)

Ralf Westphal - One Man Think Tank hat gesagt…

@pvblivs: Schönes Beispiel das mit dem Atmen :-)

Es stimmt, wir müssen atmen. Und es stimm, wir kommen ohne funktionale Abhängigkeit nicht aus.

Ist deshalb jedes Atmen aber auch gut? Kaum. Geh mal zum Kundalini Yoga oder zu einem Atem-Sprech-und-Stimmtherapeuten und du wirst sehen: vergiss dein Atmen, wie du es bisher betrieben hast.

Genau so meine ich das mit der funktionalen Abhängigkeit.

Vergiss, die bisherige unbewusste Anwendung dieser Notwendigkeit.

Und wenn Design immer Geschmacksfrage wäre... Dann hätten wir ein echtes Problem. Dann wäre Softwareentwicklung nämlich nicht lehrbar. Und dann könnten wir uns jedes Gerede über Prinzipien sparen.

Nein, Design ist keine Geschmacksfrage, sondern eine Frage von Heuristiken und der Abwägung. Nur dafür muss man überhaupt etwas haben, zwischen dem man abwägen kann. Und das habe ich hiermit und mit dem nächsten Posting geliefert.

Stelzi79 hat gesagt…

Ich sehe das auch so ungefähr wie pvblivs.

Problem 1 und 2 sind ohnehin nur der derzeitig in Mode gekommen Implementierungen von objektorientierten Programmierung geschuldet. Schon alleine der Umstand, dass hier mit keinem Wort die Möglichkeit genannt wird, einfach ausgelagerte Funktionalität in der gleichen Klasse reinzuschreiben, zeigt wie abstrakt dieses Thema anscheinend aufbereitet werden muss. Einfache in sich selbst abgeschlossene Funktionalität, die nur von der Klasse selbst konsumiert wird, gehört in die Klasse selbst rein. In sich selbst abgeschlossene Funktionalität, die von mehreren verschiedenen Klassen konsumiert werden soll, muss in einer statischen Funktionalitäten-Sammlungs-Klasse als statische Methode rein geschrieben werden. Da braucht es auch keinen irren Klassen-Instanziierungs-Wahnsinn und womöglich auch noch mit Interfaces. Jeder Programmierer, der für so was Interfaces benutzt, muss nahegelegt werden, dass doch lieber in eine anderes Berufsfeld wechseln soll, denn außer dem starren Befolgen von in Mode gekommen Programmier-Dogmen, hat er keine logischen Problemlösungs-Möglichkeiten.

Zu Problem 3: Sollte nicht gerade die Auslagerung von Funktionalitäten die Übersicht von immer Umfangreicher werdenden Funktionen sicher stellen? Und was hat dieses Problem kausal mit funktionale Abhängigkeiten und einer möglichen Warnung solcher zu tun? Bei solchen "Problemen" fällt mir dann immer die Situationen im FastFoodRestaurant ein, wo sich zuerst ein Kunde über zu wenig Salz in den Pommes beschwert und dann wenn diese nach gesalzen werden, diese dann auf einmal versalzen sind.

Problem 4 und 5: Wenn ein Programmierer oder ein Team einfach so Methoden so dermaßen verändern, dass auch der Aufrufer davon betroffen ist und abgeändert werden muss, der hat ohnehin andere Probleme als das hier die Ursache für das Problem in der funktionalen Abhängigkeit gesucht werden müsste. Das wäre hierbei ohnehin nur ein Symptom und keine Ursache.

Grundsätzlich muss ich ja Anmerken, dass funktionale Abhängigkeiten immer in Anwendungen vorkommen müssen, außer man drückt sämtliche Funktionalität einer Anwendung in einer einzigen Main-Methode rein und da wären wir wieder beim Problem 3. Nur wenn funktionale Abhängigkeiten nicht linear von Oben nach Unten durch strukturiert werden, ist höchste Gefahr geboten. In einem Punkt werden wir uns doch einig sein, dass wenn eine aufgerufene Funktion s() von einer aufrufenden Funktion c() Bescheid wissen muss, ist wirklich Feuer am Dach und dann werden solche funktionalen Abhängigkeiten wirklich böse.

Ralf Westphal - One Man Think Tank hat gesagt…

@Wolfgang: Find ich ja schön, dass hier solcher Gegenwind bläst :-) Zeigt mir, wie tief die Verdrängung dieser Probleme sitzt. Das ist, wie Übergewicht als normal zu akzeptieren. Weil alle Erwachsenen um einen herum 150 kg wiegen, ist das so und muss auch nicht geändert werden :-)

Problem 1: Hat natürlich nichts mit OOP zu tun. Wo eine Funktion s() bekannt sein muss, da muss sie bekannt sein. Und entweder kennt man sie direkt (statische Abhängigkeit) oder man kennt eine Indirektion (dynamische Abhängigkeit).

Problem 2: Ja, das ist ein Problem der OOP. Und damit ist es wohl für die Majorität der Entwickler relevant. Das ist keine Modeerscheinung, sondern Mainstream seit 20 Jahren.

"Ausgelagert" ist auch keine Funktionalität in der eigenen Klasse. Klassen mit 100.000 LOC (habe ich gesehen!) sind genauso unwartbar wie Methoden mit 1.000 LOC.

Und ob man nun jemandem, der nicht in statische Klassen auslagert, einen Berufswechsel nahelegen sollte, oder umgekehrt, jemandem, der das tut... das lasse ich mal dahingestellt. Ich empfehle da mal den Besuch unterschiedlicher Veranstaltungen, um zumindest festzustellen, dass nicht alle dieselbe Auffassung von Professionalität haben.

Problem 4 und 5 gehen nicht weg, wenn man Entwickler, die solche Änderungen vornehmen müssen, wiederum als unprofessionell tituliert.

Dass s() c() kennt, habe ich nicht thematisiert. Das kann ich nicht mal denken ;-) Aber sicherlich: solche zirkuläre Referenz ist natürlich "böse".

pvblivs hat gesagt…

Design ist natürlich nicht nur eine Geschmacksfrage. Aber doch ein wichtiger Teil davon.

Ich stimme Dir zu, dass die Software-Entwicklung immer stark unter Broken Windows leidet. Software Entwicklung hat generell sehr geringe Hemmschwellen oder Aktivierungsenergie um irgendetwas zu machen. Alles was es braucht ist Zeit, Willen und einen Kopf. Aber einfach schnell was reinzuschweinern ist noch viel einfacher, ist so sensationell billig, dass unter großem Druck oft genau das passiert: Das Design geht "flöten". Es skaliert nicht mit der Software mit, solange bis die Software unwartbar wird.

Zurück zum Design also. Wenn man die zerbrochenen Fenster zuerst außen vor lässt, dann enden gute Entwickler oft in (manchmal lokalen) Optima, die von Design-Standpunkt nicht sehr stabil sind. Man diskutiert dann, ob man die Methode noch auslagert, ob die Klasse noch zu viel macht - oder ob die Logik jetzt zu sehr verteilt ist und damit die Klarheit nicht verloren geht - oder ob man noch eine Generalisierungsstufe höher gehen sollte - oder ob die Generalisierung nicht den Blick für das Wesentliche trübt. Da ist dann oft Geschmack im Spiel. Ja, es sind auch viele andere Erfahrungen dabei, aber die habe ich einfach mal vorausgesetzt. Außer Dein Post richtet sich an "Anfänger". Das hatte ich so nur nicht abgelesen.

Was mir dabei einfällt, was ich in meinem ersten Post vergessen hatte, ist ein Thema, dass mir in letzter Zeit sehr wichtig ist: Wie kann ich einen Prozess oder eine Software so designen, dass glasklar ist, wie es weitergeht. Dass die Broken Window Theorie zwar eintreffen könnte, aber so offensichtliche Geschmacksverirrungen offenbaren würde, dass man sich lieber an das Design hält.
Das halte ich allerdings noch für sehr hypothetisch. Erstens ist gerade eine wichtige Eigenschaft von Design seine Einfachheit, damit man nicht das Unknown Unknown von morgen versucht durch unnötige Verkomplizierungen zu verhindern. Und zweitens sollte Design sich eben auch den Gegebenheiten anpassen und nicht statisch sein.

Die Bottom Line für mich ist, dass klassisches Projekt-Denken gefährlich ist (fester Beginn, fester Abschluss). Man sollte sich darüber im Klaren sein, dass nur die wenigsten Projekte wirklich ein absehbares Ende haben und auch genau so an Softwareentwicklung herangehen. Jegliche Software braucht Betrieb und Wartung und oft sind noch viele Änderungen nötig, durch Fehler oder durch den Wandel der Zeit. Nach dem Hauptteil des Projektes sollte man versuchen Leute mit Schlüssel-Know-How nicht zu verlieren und in der Mitte des Projektes nicht plötzlich aus Zeitdruck irrational viele Entwicklermengen dazupacken.

Wenn ich also Deinen initialen Post nochmal nehme und die Probleme streiche und stattdessen hinschreibe "ich fordere Zeit für Reflektion beim Design", passt es dann auch? ;-)

Ralf Westphal - One Man Think Tank hat gesagt…

@pvblivus: Da steckt jetzt ne Menge drin, wozu ich auch nicken würde.

Auf "ich fordere Zeit zur Reflexion" würde ich mein Posting allerdings nicht zusammenstreichen. Ne, so allgemein will ich hier nicht sein. Mir geht es wirklich um Probleme. Ganz spezifische. Genau die, die ich beschrieben habe.

Und das sind auch keine Anfängerprobleme. Das sind eher die Probleme derjenigen, die schon glauben, mit IoC/DI und ISP und noch einer Prise SRP alles getan zu haben.

Die basteln dann nämlich munter Klassenmodelle mit vielen kleinen Klassen, die munter in tiefen Nutzungshierarchien stehen. Das brodelt es dann nur vor funktionalen Abhängigkeiten. Aber Dank IoC/DI sind die kein Problem? Ne, das sehe ich anders. Mockup-Frameworks sind keine Lösung, sondern ein Symptom eines mit anderen Mitteln fortgesetzten Problems. (Ich brauche sie übrigens schon lange nicht mehr.)

SRP ist gut. Keine Frage. Aber damit entsteht nicht nur das Problem der Benennung einer zunehmenden Zahl kleiner Klassen. Es entsteht auch ein Abhängigkeitsproblem. Um genau das geht es mir.

"Anfängern" kann man das übrigens viel leichter austreiben als "Profis". "Anfänger" sind noch nicht so verdorben, d.h. tief drin in Glaubenssätzen. Denn darum geht es hier: um Glaubenssätze. Wir müssen etwas dringend so weiter tun, wie in der Vergangenheit - auch wenn es uns schmerzt. Wahrscheinlich tun wird es einfach nur noch nicht gut genug. Also mehr davon. Funktionale Abhängigkeiten mit IoC/DI nur noch ein bisschen einfacher machen und noch ein Tool oben drauf. Dann wird es schon.

Sorry, daran glaube ich nicht mehr. Nicht "mehr vom Selben" hilft, sondern mal was anderes.

Funktionale Programmierung geht da schon in eine gute Richtung. Aber das PoMO haben die auch noch nicht so deutlich im Blick. Doch denen fällt es leichter, es einzuhalten.

Aber PoMO ist noch nicht genug. Denn funktionale Abhängigkeiten lassen sich zwar zurückdrängen, nicht jedoch ganz abschaffen. Also muss noch etwas dazu kommen. Davon ein andermal :-)

"Mehr Zeit für Design" wäre natürlich gut. Was aber, wenn es die nicht gibt? Dann müssen "hard and fast rules" her, um eine Grundsicherung in Sachen Evolvierbarkeit herzustellen. Das PoMO gehört in diesen Grundsicherungskasten. Also: Nicht so lange schnacken, sondern anwenden :-)

Unknown hat gesagt…

Lassen sich viele Probleme nicht leichter vermeiden, wenn man sich stärker am Client orientiert?
Das genannte Beispiel für ISP würde so nicht entstehen, da man ein für c() ppassendes Interface definieren würde.
Eine stärkere Ausrichtung am Client hat auch zur Folge, dass Änderungen eher aus Clientsicht getrieben werden und somit häufiger "von oben" statt "von unten" implementiert werden.
Die im Artikel vorausgesetzte Freiheit von s(), sich zu ändern und dadurch auch c() zu Änderungen zu zwingen, würde somit minimiert, wenn nicht sogar umgekehrt werden.
Verschiedene Designprobleme lassen sich oft leichter lösen, wenn man den Client im Auge behält... Das funktioniert dann sogar bis hin zum menschlichen Client, der letztlich in 90% der Fälle der einzige Auslöser für Änderungen, Fehlerkorrekturen, Erweiterungen usw. ist.
Anders formuliert: c() sollte definieren, wie s() aussieht oder sich verhält. c() sollte die gewünschte Syntax und die notwendige Semantik vorgeben.
Sicherlich klappt das nicht immer. Für diese Fälle kann ich zu einem Großteil dem Artikel in seiner Essenz zustimmen: Designe unter entwickle bewusster!

Ralf Westphal - One Man Think Tank hat gesagt…

@Tobias: Klar, APIs sollten vom Client getrieben werden.

Sollten.

Werden sie aber nicht immer. Immer wieder gibt es andere Kräfte, die an ihnen zerren. Das beginnt schon, wenn es nicht nur ein c() als Client von s() gibt. s() wäre Diener mehrerer Herren. Und dann?

Wo keine funktionale Abhängigkeit besteht, da gibt es dann keinen Ansatzpunkt. So einfach ist das.

Und wie weit willst du das ISP treiben? Solange du mehr als eine Funktion auf einem Interface hast, hast du auch topologische Kopplung. Und wenn du nur noch eine Funktion pro Interface hast... dann hast du FP :-) Auch schön.

Bewusster Entwickeln ist ne gute Sache. Wenn ich aber nur das hätte sagen wollen, hätte ich einen anderen Artikel geschrieben. Wie schon erwähnt: Ich meine es ernst mit dem PoMO. Ist ja auch nicht das erste Mal, dass ich drüber schreibe. Mit diesem Artikel wollte ich meine Argumente nur zuspitzen.

Anonym hat gesagt…

Grrr, Immer diese Abkürzungen... was zum Henker ist PoMO?

Ralf Westphal - One Man Think Tank hat gesagt…

Wer googeln kann, ist klar im Vorteil :-) Suche nach "pomo one man think tank" und finde.

Aber das wird im nächsten Artikel auch nochmal erklärt.

Hannes Preishuber hat gesagt…

ich bin mal Grundehrlich. Ich les das, (nein überflieg das), verstehe jedes Wort, kenne jedes Akronym und kann den Sinn nicht erfassen bzw. suche die Relevanz für mein Leben und finde sie nicht.

Ralf Peine hat gesagt…

Ich habe in den letzten 20 Jahren, insbesondere in den letzten 3en, mit genau diesen Problemen und Mocking-Frameworks gekämpft. Besonders in grossen Systemen mit mehreren eigenen Produkten sind die Probleme nur durch Entkopplung zu lösen.

Schon wegen der besseren Testbarkeit habe ich immer "Funktionale-", "Daten-" und "Builder"-Klassen implementiert. "Integrative" Klassen werden jetzt noch dazukommen.

Da ich auch viel in ungetypten Sprachen wie Perl programmiere, habe ich keine Probleme z.B. mit loser Kopplung.

Lose Kopplung wähle ich immer für die Verbindung von "fernen" Klassen, d.h. die inhaltlich voneinander unabhängig sind oder aus anderen Bereichen (Libs/Appl.) stammen.

Die Verwendung von Interfaces erzeugt eine enge Kopplung, daher verwende ich das nur in den Fällen, wo die Kopplung inhaltlich eh schon vorhanden ist und auf keinen Fall Lib-/App-übergreifend.

Zur Zeit entwickle ich das PerlOpenReportFramework, indem die Funktionalität erst nachträglich durch anonyme Subs (Actions, C#: Delegates, Lambdas) eingefügt wird.

Der Zugriff auf die Daten erfolgt nur über anonyme Subs, dadurch gelingt es, den Report vollständig unabhängig von Art und Struktur eines einzelnen Datensatzes zu implementieren, indem der Anwender des Reports den Zugriff konfiguriert. Daher ist nur eine (integrative) Klasse für den Report notwendig. Siehe dazu auch http://www.jupiter-programs.de/prj_public/erg/index.htm

VG Ralf.