Follow my new blog

Montag, 9. Januar 2012

Functions considered harmful

Bisher habe ich die "traditionelle" konzeptionelle Objektorientierung als eine der Ursachen für die heutigen Probleme mit der Wartbarkeit von Software gesehen. Inzwischen regt sich jedoch in mir der Verdacht, dass das Wurzelproblem tiefer liegt.

Womöglich ist die Objektorientierung sogar zu loben, weil sie das irgendwie verstanden hatte und versucht zu helfen. Leider ist das nicht so geglückt, wie man es sich erhoffte. Warum? Weil das Wurzelproblem eben nicht behoben, sondern nur kaschiert wurde. Man hat sich nicht getraut, so tief nach unten zu graben, um es auszureißen.

Denn das Wurzelproblem scheint mir... der Funktionsaufruf.

Ja, genau, der gute alte, unscheinbare und für imperative Sprachen so fundamentale Funktionsaufruf.

Dass wir schreiben können

y = f(x)

ist der Keim vieler Wartbarkeitsübel.

Wie kann das sein?

Problem #1: Unbegrenzte Länge

Durch Funktionen gibt es keine Grenze für das, was eine Codeeinheit tun kann. Ungezügeltes Wachstum von Codeeinheiten wird durch Funktionen ermöglicht. Denn nach einem Funktionsaufruf kann es ja weitergehen im aufrufenden Code.

Funktionen kombinieren Request und Response. Damit kombinieren sie vorbereitenden Code und nachbereitenden Code im Aufrufer. Und selbstverständlich kann nachbereitender Code gleichzeitig vorbereitender Code für den nächsten Funktionsaufruf sein. Nach dem Funktionsaufruf ist vor dem Funktionsaufruf.

Wann sollte also aufrufender Code beendet sein? Wenn er inhaltlich eine Verantwortlichkeit erfüllt. Klar. Aber das ist ein Kriterium, dessen Erfüllung im Auge des Betrachters entsteht. Der eine mag es, Verantwortlichkeiten in höchstens 20 Zeilen zu formulieren, der nächste hat kein Problem, wenn es dafür 100 Zeilen braucht und wieder einem anderen sind auch 10.000 Zeilen recht.

Durch Funktionsaufrufe gibt es kein Halten in Bezug auf den Umfang des aufrufenden Codes. Die Existenz von Regeln, die versuchen, diesen Umfang direkt oder indirekt zu begrenzen, ist der beste Beweis dafür.

Und wie wäre es, wenn es keine Funktionen gäbe? Dann würde eine Codeeinheit immer nur aus soviel Code bestehen wie nötig ist, um einen Request vorzubereiten, der am Ende dann abgeschickt wird. Ohne Funktionen gäbe es keinen Response, den die vorbereitende Codeeinheit nachbereiten könnte, also wäre ihre Aufgabe mit Versand des Request abgeschlossen. Der Response würde von einer anderen Codeeinheit weiterverarbeitet.

Ohne Funktionen ist es aber natürlich sinnlos, von Request und Response zu sprechen. Einen Request gibt es nur, wenn man auch einen Response erwartet. Clients übergeben Requests an Services, die mit Responses antworten.

Ohne Funktionen gibt es nur Daten, die produziert und konsumiert werden. Producer senden Daten an Consumer. Und immer so weiter. Aus Client-Code wie diesem, der sich potenziell unendlich fortsetzt…

Client:
  Vorbereitender Code erzeugt X
  Y= f(X)
  Nachbereitender Code verarbeitet Y

wird Producer-Consumer-Code:

Producer:
  Vorbereitender Code erzeugt X
  Versenden von X

f als Prosumer:
  X nach Y transformieren
  Versenden von Y

Consumer:
  Nachbereiten von Y

Sie sehen, der Umfang jeder Codeeinheit ist ganz natürlich sehr begrenzt, wenn es keine Funktionen gibt. Es lässt sich einfach nur wenig tun, bis wieder Arbeit an eine andere Codeeinheit delegiert werden muss.

Die grenzenlose Kopplung von Vorbereitung und Nachbereitung ist das offensichtliche Problem, zu dem Funktionsaufrufe führen. Sie bläht aufrufende Codeeinheiten auf. Das erschwert schnell die Verständlichkeit und leistet einer Vermischung von Verantwortlichkeiten im Sinne des SRP Vorschub.

Problem #2: Unbegrenzte Tiefe

Unterhalb des Request/Response-Problems liegt leider noch ein weiteres, noch fundamentaleres. Das wird allerdings erst sichtbar, wenn Software weiter wächst. Es ist das Problem des Aufrufs schlechthin.

Zur Erinnerung: Aufrufe von Code sind eigentlich als Mittel zur Platzersparnis erfunden worden. Früher war Speicher eben sehr knapp. Da war jedes Mittel recht, um Bytes zu sparen. Also hat man das CALL/RET Maschinenbefehlpaar erfunden. Aus Code wie:

A
S
T
U
B
S
T
U
C
S
T
U

konnte nun werden:

A
CALL F
B
CALL F
C
CALL F

F: S
T
U
RET

6 Anweisungen statt 10. Das ist selbst mit diesem Pseudocode eine Reduktion um 40%.

Platzersparnis, nicht Wiederverwendbarkeit ist die Motivation hinter Unterprogrammen als Verallgemeinerung von Funktionen. Für Wiederverwendbarkeit wären Macros ausreichend gewesen.

Wie alles im Leben hat natürlich auch die Platzersparnis ihren Preis. Der besteht in der Trennung von Client-Kontext und Service-Code. Solange im obigen Beispiel STU zwischen A und B textuell steht, ist klar, was die Aufgabe von STU ist. STU steht im Nutzungskontext. Der Entwickler sieht zur Entwicklungszeit, was vorher passiert, was zwischendurch passiert und was nachher passiert.

Die Einführung des Unterprogramms F zerstört diese verständliche Einheit. Was zwischendurch passiert, steht nun irgendwo. Wer nun liest

A
CALL F
B

der ist darauf angewiesen, dass F ein ausdrucksstarker Name ist, um zu verstehen, was da passiert. Die vorherige Einheit zur Entwicklungszeit existiert erst wieder zur Laufzeit.

Das bedeutet: Funktionen machen den Aufrufort schwerer lesbar, weil sie dort Code durch einen Namen ersetzen. So eine Ersetzung ist sehr verlust- bzw. missverständnisgefährdet.

Und Funktionen machen Code insgesamt schwerer lesbar, weil sie einen natürlichen Zusammenhang wie

A
S
T
U
B

über die Codebasis verteilen. Auch da gibt es ja kein Halten. Die Aufruf-Schachtelung von Funktionen ist beliebig tief. Aus

A
S
T
U
B

wird zuerst

A
CALL F
B

F: S
T
U
RET

dann vielleicht

A
CALL F
B

F: S
CALL G
U
RET

G: T
X
Y
RET

und dann vielleicht

A
CALL H

H: CALL F
B
RET

F: S
CALL G
U
RET

G: T
X
Y
RET

uns so weiter…

Zur Laufzeit macht das alles keinen Unterschied. Zur Entwicklungszeit jedoch wird es immer schwieriger zu verstehen, was da eigentlich passiert. Inhaltliche Sequenzen werden aufgelöst. Es entsteht ein Granulat an Unterprogrammen, dessen Aufrufhierarchie zur Laufzeit keine formale Entsprechung zur Entwicklungszeit hat. Funktionen führen mithin zu einem fundamentalen Impedance Mismatch zwischen Entwicklungszeit und Laufzeit.

Gut, inzwischen gibt es IDEs, mit denen man die Aufrufhierarchie durchwandern kann. Aber seien wir ehrlich: das ist umständlich. Eine hierarchische Sicht von Funktionsaufrufen ist kein First Class Citizen in den populären IDEs wie eine Projektdateiansicht oder eine Klassenansicht. Und in der Tradition von C gibt es keine geschachtelten Funktionen in C++, Java, C#.

Die Schachtelung von Funktionsaufrufen ist so tief in unser aller Programmiererstammhirn eingebrannt, dass es nicht einmal eine Metrik dafür gibt. Man macht sich Gedanken über LOC pro Funktion oder die Schachtelungstiefe von Kontrollanweisungen in Funktionen. Die Tiefe der Aufrufhierarchie von Funktionen zur Entwicklungszeit hingegen, scheint niemanden zu interessieren. Dabei ist sie es, die den logischen Zusammenhang von Code auseinanderreißt.

Aus unbegrenzter Aufrufschachtelung folgt, dass auch die Problemlösung beliebig über die Tiefe der Aufrufhierarchie verteilt werden kann. Im obigen Beispiel können ja auf jeder Ebene – Aufrufwurzel, H, F oder G – Anteile von “Geschäftslogik” stehen.

  1. Das bedeutet erstens, es bedarf zusätzlichen Aufwands, um zu entscheiden, auf welcher Ebene Geschäftslogik angesiedelt werden sollte. Das ist aber natürlich Aufwand, der nicht zur Lösung des Problems beiträgt. Also scheut man ihn, wo es geht. Das Ergebnis sind flache Hierarchien mit sehr langen Funktionen (s. Problem #1).
  2. Das bedeutet zweitens, Funktionen, die nicht Blätter in der Aufrufhierarchie sind, machen zusätzlichen Aufwand beim Testen. Es muss ja nicht nur ihr Beitrag zur Problemlösung überprüft werden, sondern es müssen auch noch die aufgerufenen Funktionen ersetzt werden (Attrappen).
  3. Und drittens sind Aufrufhierarchien ständig gefährdet, durch Veränderungswellen erschüttert zu werden. Veränderungen breiten sich entlang von Abhängigkeiten aus:
    Wenn C wie Client von S wie Service abhängig ist, dann kann es bei Änderungen an C nötig sein, S nachzuführen. Und falls sich S ändert, kann es nötig sein, C nachzuführen. Je breiter und tiefer Aufrufhierarchien sind, desto unüberschaubarer die Ausbreitung von Änderungen, die immer irgendwo nötig sind. Das ist umso schlimmer, da ja diese Hierarchien nur schwer zu übersehen sind. Sie existieren nicht textuell und auch kaum in anderer Ansicht in den IDEs.

Fazit

Funktionen (oder allgemeiner: Unterprogramme) sind aus meiner Sicht eine der Hauptursachen für die Undurchschaubarkeit von Code. Sie machen seine Ausdehnung in Breite und Tiefe grenzenlos.

Dagegen helfen dann auch keine Ermahnungen und Metriken. Denn die sind sehr geduldig. Wenn es eng wird, hört man nicht hin und setzt sie aus. Immer mit dem Verweis, dass gerade anderes wichtiger sei – und es ja auch ohne ginge.

So entstehen unwartbare Codekonvolute einen Funktionsaufruf nach dem anderen.

*Dagegen hilft nur, Funktionsaufrufe klaren Auges als Gefahr zu sehen und ihre Nutzung zu rigoros zu begrenzen.*

Wir müssen diese Altlast aus den Anfangstagen der Programmierung abwerfen (oder zumindest mit weniger Schädlichem integrieren). Funktionen sind ein Erbe der Nähe zur Mathematik. Das hat uns weit gebracht – aber nun haben wir eine Grenze erreicht, da der Schaden größer als der Nutzen ist.

Kommentare:

cu @Boeffi .net hat gesagt…

... interessanter POV ...

verdauend

cu
@Boeffi

Golo Roden hat gesagt…

Hallo Ralf,

hmmmm ... ist da wirklich die Funktion als solche das Problem, wenn Code ungehindert in Tiefe und Breite wuchert?

Oder liegt es nicht vielmehr am falschen Umgang mit einem an und für sich nützlichen Werkzeug?

Viele Grüße,


Golo

Ralf Westphal - One Man Think Tank hat gesagt…

@Golo: Liegt es wirklich an der Schusswaffe, wenn in den USA jedes Jahr Tausende zu Tode kommen? Oder ist das nicht vielmehr ein Problem derjenigen, die sie einsetzen?

Natürlich ist ein problematischer Einsatz eines Mittels zuallererst im Verwender zu suchen. Nur ist dessen Beeinflussung womöglich schwierig. Es mangelt in den USA ja nicht an Einsicht, dass Schusswaffen gefährlich sind :-) Und es mangelt auch nicht an Versuchen, die Besitzer zu beeinflussen. Haftstrafen oder gar Todesstrafe sind davon nur ein Teil.

Was rauskommt, ist weithin sichtbar: Tote und volle Gefängnisse.

Und bei der Softwareentwicklung ist auch weithin sichtbar, wenn ein unschuldiges Mittel unbedenklich eingesetzt wird.

Wie bei den Schusshaffen hilft da irgendwann nur die Verbannung. In Canada ist das mit den Tötungsdelikten schon kein Problem, obwohl die an die USA grenzen und eine ähnliche Kultur pflegen. Auch in Europa ist es kein solches Problem. Auch nicht Australien.

Weil dort bessere Menschen leben? Nein. Weil einfach der Zugang zu Schusswaffen extrem reglementiert ist.

Keinen anderen Weg sehe ich bei Funktionen (wie damals beim GOTO). Die Nutzung muss stark reglementiert werden.

Ein Anfang könnte sein: Funktionsaufrufe sind nur erlaubt, wo kein Einfluss auf einen API besteht und der eben auf Funktionen ausgelegt ist.

Da kann man eben nichts machen. Wir können nicht warten, bis der .NET Fx mal auf irgendwas anderes umgebaut wurde.

Versuche mal als Übung, mit dieser Maxime eine Code Kata zu lösen. Das wird dir schwer fallen. Aber das wird dir auch eine ganz neue Perspektive eröffnen.

Beobachte, wo es dich hintreibt, wenn du keine eigenen Funktionen definieren darfst und doch das SRP einhalten musst :-) Am Ende soll ja nicht nur 1 Codeeinheit mit 10.000 LOC pro Interaktion herauskommen. Also musst du Wege suchen, deinen Code weiterhin in kleine Verantwortlichkeiten zu zerlegen und die irgendwie zu einem größeren Ganzen zu verbinden.

Happy coding!

Mario Noack hat gesagt…

Und wie sieht dann die Alternative aus? Code bildet ja Logiken des Kunden ab. Die Wiederverwendung sich wiederholender Teile bedingt für mich die Nutzung von Funktionen oder Objekthierarchien. Beschränke ich beides auf ein Minimum, gehen mir die Ideen aus, wie denn dann eine Empfehlung zu besseren Code lauten könnte?

Fragende Grüße, Mario

Ralf Westphal - One Man Think Tank hat gesagt…

@Mario: Ich habe nichts gegen Wiederverwendung bzw. gegen Nutzung von Codeeinheiten in unterschiedlichen Zusammenhängen.

Code in kleinere Einheiten zu zerlegen, aus einem großen Stück kleine und kleinste zu machen, ist also völlig ok. Nein, es geht gar nicht anders. Denn das SRP muss ja auch ohne Funktionen beachtet werden.

An dieser Stelle will ich einfach nicht mit einer Lösung ins Haus fallen ;-) Deshalb: Überleg doch einmal selbst, wie du diesen Code

var x = ...;
var y = f(x);
var z = ...y...;

ohne Funktionsaufruf ausdrücken könntest. Natürlich soll dabei das, was derzeit in f() steckt, nicht plötzlich am Aufrufort stehen.

Und natürlich kommst du auch nicht drumherum, Code, den du von anderem trennen willst, irgendwie in Methoden zu stecken. Das ist ja auch kein Problem. Dass es f() als herausgezogene Verantwortlichkeit gibt, ist ok.

Nicht ok ist es, solche Funktionen "ungezügelt" aufzurufen wie im obigen Codestück.

Mehr möchte ich an dieser Stelle eigentlich nicht erreichen, als dass wir darüber übereinstimmen, dass Funktionsaufrufe sehr schnell zu einem Problem werden.

Wenn wir dann darin übereinstimmen, können wir überlegen, wie wir dieses Problem angehen.

Wie schauts also aus: Folgst du meiner Darstellung? Stimmen wir überein?

Jörg hat gesagt…

Hallo,
den Gedanken finde ich sehr interessant. Aber Alternativen wollen mir nicht wirklich einfallen. Funktionen dienen der Teilung von Code in "besser denkbare" Einheiten von "Funktionalität". Ich muss mit Horror an mein LISP-Programmierpraktikum vor nahezu 20 Jahren zurückdenken. Liste rein und transformieren ohne wirklich benennbare "Funktionseinheiten" hat die Sache nicht einfacher gemacht; auch wenn der Lisp-Code im Vergleich zum Pascal-Pendant nur etwa ein Drittel so groß war.
Klar habe ich auch schon mal den Überblick verloren, wie sich die nicht gut benannten Methoden gegnseitig aufrufen. Aber irgendwie sollten "benennbare Portionen Code" miteinander kombinierbar sein. Und ein sequentielle Fluss von einer Einheit zur nächsten führt idR zu vielen Wiederholungen, die sich weder gut lesen noch warten lassen. Aber ich lasse mich gerne überraschen...
Jörg

Mario Noack hat gesagt…

Also grundsätzlich stimme ich da schon überein, dass der intensive Umgang mit Funktionen ein Problem werden kann, genauso wie eine übertriebene Auslebung der Objektorientierung. Grundsätzlich mangelt es aber an Ideen, es besser zu machen. Ich sehe auch nicht unbedingt diese beiden Punkte als Übel. Das Problem sitzt vor dem Rechner. Ich sehe es leider oft, dass Code geschrieben wird, der nicht verstanden wird. Halbwissen und die Einstellung "Tut doch" sind für mich gravierender und sorgen am Ende für viel Überarbeitungsnotwendigkeiten. Aber an dieses Problem kommt man leider nicth ran, weil man da oft das Gefühl bekommt, dass man gegen eine Wand redet. Weitere Beispiele sind häufige unsinnige Verwendungen von public static Feldern oder auch die schlichte Nichttestbarkeit des entstandenen Codes.

Ralf Westphal - One Man Think Tank hat gesagt…

@Jörg: Im Moment haben wir keine andere Möglichkeit, als Anweisungen zu Funktionen zusammenzufassen. Das ist auch nicht schlimm. Wir brauchen eine benannte Klammer um Anweisungen.

So wie Schachtelungen in LISP aber schnell unübersichtlich werden, so werden Aufruf-Schachtelungen von "Codeklammern" (Funktionen) auch schnell unübersichtlich. Man überschaut einfach nicht den Abhängigkeitsbaum.

Und auch "kombinierbar", d.h. zu Größerem zusammensetzbar müssen diese "Codeklammern" sein. Klar.

Der Trick ist nun: Dafür benutzen wir heute auch wieder Codeklammern. Und diese Codeklammern haben dann nicht nur diesen einen Zweck (s. Problem #2). Dadurch breitet sich "Logik" vertikal in der Hierarchie aus.

Jetzt können wir uns entscheiden: Wir verzichten auf Funktionen, um Funktionen zu Größerem zu integrieren. Das können wir machen; technisch geht das. Oder wir sind konsequent, indem wir solche Integratoren auf diesen Zweck reduzieren. Das ist ganz einfach möglich: dafür braucht es nur ein bisschen Disziplin. Damit wäre dem SRP auch Genüge getan.

Wenn wir besser wartbaren Code haben wollen, müssen wir uns für eines von beidem entscheiden, denke ich.

Ralf Westphal - One Man Think Tank hat gesagt…

@Mario: Wenn das Problem nun schon seit Jahrzehnten vor dem Rechner sitzt, dann sollten wir endlich einsehen, dass "bessere Ausbildung" offensichlich nicht so breit hilft, wie es wünschenswert wäre.

Die Konsequenz muss dann lauten: andere Werkzeuge, mit denen die Probleme vor dem Rechner einfach nicht mehr soviele Probleme im Rechner herstellen können.

Wer keine Schusswaffe in die Hand bekommt oder nur eine mit Platzpatronen, der muss daran auch nicht ausgebildet werden.

JakeJBlues hat gesagt…

Hallo Ralf,

"Wer keine Schusswaffe in die Hand bekommt oder nur eine mit Platzpatronen, der muss daran auch nicht ausgebildet werden."

interessanter Ansatz, da Menschen nun einmal sehr kreativ sind....bin ich sehr gespannt, wie lange "das Problem" vor dem Rechner braucht, um sich mittels Producer, Prosumer und Consumer "Schusswaffen" und "scharfe Munition" zu bauen ;-)

Gruß JJR
P.S.: Ich persönlich glaube --- nicht lange ...

Jörg hat gesagt…

Hallo noch einmal,
ich glaube ich habe das immer noch nicht ganz verstanden: Ich stelle mir Code als einen beliebigen Graphen (inklusive Zyklen) vor. Geht es Dir dann darum, daraus einen Baum zu machen (mit eher geringer Tiefe), oder plädierst Du wirklich hart für eine Kette von Aufrufen? Oder habe ich Dich ganz missverstanden?
Wenn wir künftig nur eine Kette von Funktinseinheiten nacheinander schalten wollen, nehmen wir uns damit nicht die Möglichkeit ein Problem auf unterschiedlichen Abstraktionsebenen zu betrachten? Wird nicht allein die Menge der flachgeklopften, nacheinander geschalteten Funktionseinheiten genauso unübersichtlich wie ein Baum, dessen Ebenen unterschiedliche Abstraktionsebenen des Problems beschreiben? Ich bin noch nicht überzeugt.

Ralf Westphal - One Man Think Tank hat gesagt…

@JJR: Mit jedem Werkzeug kann man auch Unfug anstellen. Das ist klar. Manchmal ist es anderer Unfug als mit anderen Werkzeugen, manchmal schlimmerer oder weniger schlimmer.

Nur weil das aber so ist, sollten wir doch nicht bei einem Werkzeug bleiben, das konsistent große Probleme macht.

Wir stoßen einfach an eine gläserne Decke, ist mein Eindruck. "Mehr vom Selben" hilft nun nicht mehr.

OOP ist insofern nur eine Verfeinerung der prozeduralen Programmierung. Und FP ist ein anderer Ast desselben Stamms.

Ich finde, es schadet nichts, mal ganz anders zu denken.

Ralf Westphal - One Man Think Tank hat gesagt…

@Jörg: Es geht doch gar nicht um Flachklopfen. Wir brauchen Hierarchien - zumindest zum Denken. Anders können wir nicht abstrahieren.

Interessant aber doch, dass die heutigen Hierarchien entweder ganz flach sind (niemand benutzt wirklich schachtelte Klassen) oder nur zur Laufzeit existieren. Beides macht das Nachdenken über Software schwer - denn das findet zur Entwicklungszeit statt. Deine Kritik wäre also am vorherrschenden Paradigma gut angebracht.

Für Hierarchien von Funktionalität (und ich meine echte, zur Entwicklungszeit relevante, sichtbare Hierarchien) brauchen wir aber keine Funktionsaufrufhierarchien. Es reicht, wenn 3GL-Funktionen auf der untersten Ebene solcher Hierarchien codiert sind. Dort müssen sie keine anderen Funktionen aufrufen.

Die Hierarchieebenen darüber können "irgendwie" aussehen. Wenn du sie im ersten Schritt ebenfalls mit 3GL Funktionen beschreiben willst, kannst du das machen - nur sei dir dann bewusst, was du tust. Vermische nicht die Aspekte. Logik hat dort nichts zu suchen.

Palin hat gesagt…

An dieser Stelle will ich einfach nicht mit einer Lösung ins Haus fallen ;-) Deshalb: Überleg doch einmal selbst, wie du diesen Code

var x = ...;
var y = f(x);
var z = ...y...;

ohne Funktionsaufruf ausdrücken könntest. Natürlich soll dabei das, was derzeit in f() steckt, nicht plötzlich am Aufrufort stehen.

Möglichkeit 1.
Member Variablen?
Dann hab ich nur noch.
x()
y()
z()

Möglichkeit 2.
IOC
Statt y = f(x) -> f(y)
bzw.
x.y = y
x.f()
je nach belieben ;)

Möglichkeit 3.
Klassen
y = x.f()

Das würde mir so direkt darauf einfallen.

Ein paar Varianten gibt es sicherlich noch.

Ich denke hier ist die Frage was ich machen will.

Und Funktion/Methoden Aufrufe haben ja auch ihren Sinn um Code
besser Lesbar zu machen.
z.B. IsEMaliAdresse(BenutzerEingabe) (Ich denk mal denn Aufruf versteht jeder. Und denk Rückgabewert brauch ich auch nicht anzugeben ;))

Ich denke es kommt auf die Aufgabe an, die man Lösen will. Und dann kramt man in seinen Werkzeugkoffer und schaut ob man dafür das passende Werkzeug (Entwurfsmuster ect.) hat.
(OK mein Koffer ist noch recht klein, deshalb bin ich ja auch hier im Shop um weiteres Werkzeug zu erwerben. ;) )



Was die Ausbildung angeht möchte ich als Leidtragender mal erwähnen, das Leute ohne je eine Zeile Code programmiert zu haben ihr Informatik Studium abschließen können und noch viel schlimmer auf Leute in der Ausbildung "gehetzt" werden.

Was dann zu aussagen führt wie: "Natürlich ist es richtig dass Kunde und Mitarbeiter von Person erben und beide kann ich in einer Liste von Kunden anzeigen da sie ja von Person erben! Es mag ja sein das man dass so nicht programmieren kann aber das Klassen Diagramm so ist Korrekt."

Gott sei dank hatte ich auch noch einen guten Lehrer der mir z.B. dazu geraten hatte mal in Entwurfsmuster von der GoF rein zuschauen auch wenn es kein Unterrichtsstoff war.


Auch was die Weiterbildung in Betrieben angeht ist es da bei kleinen und mittelständischen Betrieben auch nicht das Gelbe vom Ei.


Was dann nun die Schusswaffen angeht, auch in Kanada können die Leute Waffen haben. Es ist halt nur an Auflagen gebunden.

Aber ich denke dass ist noch nicht einmal der Ausschlag gebende Punkt. Sondern ich denke es ist die Einstellung zu Gewalt und Waffen die Kanada und die USA voneinander unterscheiden.

Und ich denke auch das es ein Punkt ist, der die Code Qualität prägt.
In der Ausbildung werden die Falschen Werte vermittelt (Wenn es einfach nur Läuft bekommt man seine 1, Qualität Fehlanzeige ), der Chef schaut auch nicht auf die Inneren Werte des Codes (wie soll er auch er hat ja nicht wirklich Ahnung). Wenn es schnell Fertig ist und läuft ist es in Ordnung.

Ich denke in der "allgemeinen" Gesellschaft der Entwickler ist es noch nicht angekommen, dass Innere Werte zu besseren Resultaten führen genau so wie in den USA noch nicht angekommen ist, dass es eine schlechte Idee ist wenn jeder Waffen tragen darf.

Nicht die Waffe ist das Problem, sondern das Verständnis der Gesellschaft im Umgang mit der Waffe.


Ich finde dass du hier richtig gute Arbeit leistest den Leuten beizubringen wie man mit seiner Waffe (Code) richtig umgeht (z.B. CCD).
Und darüber Nachzudenken was man macht (sieht man ja auch hier bei den Beiträgen).

Leider kümmern sich darum eigentlich nur die Leute, die wirklich Interesse haben sich weiter zu Bilden und auch wissen wo/wie sie es können.

Und die Weiterbildung Fängt nicht da an wo es wirklich wichtig ist. Wie z.B. Ausbildung und Studium.


In dem Rahmen möchte ich hier noch erwähnen, dass ich noch gute Kontakte zu meinem alten Lehrer habe und und wenn du (oder ein anderer erfahrener Programmierer),mal Lust hast vor einer Klasse von Auszubildenden, dein Wissen weiterzugeben, er würde sich sicherlich Freuen und ich fände es auch als guten Ansatzpunkt. (Der Nachteil für den Vortragenden es gibt sicher kein Geld.) (NRW/Essen)

Mirko Schneider hat gesagt…

@Palin:
vollste Zustimmung!

Gruß
Mirko Schneider

JakeJBlues hat gesagt…

Hallo Ralf,

bin einverstanden, aber ich würde vorschlagen "dem Problem" vor dem Computer zuerst die "mutable Parameter" als Munition wegzunehmen. Ich glaube, dass die meisten "Probleme im Rechner" dadurch entstehen.

Gruß JJR

Golo Roden hat gesagt…

Sorry, aber zu sagen, das Problem ist das Werkzeug, kann es doch nicht sein.

Warum hat denn in DE nicht jeder eine Schusswaffe? Weil es Auflagen gibt, was auch sinnvoll ist.

Warum fährt man erst Auto, wenn man einen Führerschein hat? Weil der Führerschein eine Auflage ist, die sicherstellen soll, dass ich (halbwegs) fahren kann.

Warum übe ich erst mal, bevor ich mich alleine mit einem Fallschirm aus dem Fluzgeug stürze? Weil es gefährlich ist, und jeder seriöse Anbieter wissen will, dass Du Erfahrung hast, bevor er Dich leichtfertig rausspringen lässt.

Es geht immer wieder um Auflagen, wenn die Leute nicht selbst in der Lage sind, sich und ihr Handeln einzuschätzen. Schöner ist natürlich, wenn sie das selbst können, aber im Zweifelsfall muss es eben geregelt werden.

Dass es nun nicht viele Leute gibt (gemessen an der Bevölkerung), die sich mit einem Fallschirm aus einem Flieger stürzen oder die eine Schusswaffe besitzen, liegt daran, dass das Risiko und die Gefahr den Nutzen für die meisten überwiegt.

Das ist beim Autofahren anders. Auch wenn das vielleicht nicht vernünftig ist, und verbindliche Sehtests alle paar Jahre hilfreich wären, um Unfälle zu verhindern.

Worauf will ich hinaus?

Funktionen sind nicht böse. Funktionen sind nicht gut.
Funktionen sind Funktionen.

Böse oder gut ist, was man daraus macht.

Und ein guter Programmierer kann trotz Funktionen gut lesbare und wartbare Software schreiben. Ein schlechter Programmierer wird auch ohne Funktionen alles versauen können.

Hier bei den Funktionen anzusetzen, halte ich für grundfalsch.

Vielleicht haben wir einfach zu viele Programmierer? Die eben mit den Risiken und Gefahren nicht umgehen können, und sich auch noch falsch einschätzen?

Wenn ich mir die Einstellung vieler 9-to-5-Programmierer angucke, die ihren Kopf nur in der Firma anschalten und ansonsten mit Bildzeitung und Bier glücklich sind, dann muss ich leider sagen: Ja, wir haben zu viele Programmierer.

Und dagegen hilft nicht, die mächtigen Werkzeuge für die Profis deppentauglich zu machen, sondern die Deppen fernzuhalten.

Alles andere ist Augenwischerei.

Und was 99% der Leute endlich mal kapieren müssen: Ein PC ist nicht so einfach zu bedienen wie ein Fernseher, und wird es auch nie sein. Ganz einfach deshalb, weil er naturgemäß weitaus komplexer ist, was seine Möglichkeiten angeht.

Und wenn ich nicht bereit bin, mich entsprechend damit zu befassen, sollte ich die Finger von lassen.

Ganz einfach ;-)

Golo Roden hat gesagt…

PS: "Wir müssen diese Altlast aus den Anfangstagen der Programmierung abwerfen (oder zumindest mit weniger Schädlichem integrieren). Funktionen sind ein Erbe der Nähe zur Mathematik. Das hat uns weit gebracht – aber nun haben wir eine Grenze erreicht, da der Schaden größer als der Nutzen ist."

Richtig. Aber nicht, weil Funktionen schlecht wären. Sie haben seit mehreren tausend Jahren in der Mathematik bewiesen, dass sie leistungsfähig sein.

Das Problem ist der Verwender.

Kein Mensch käme auf die Idee, zu behaupten, in der Mathematik müsste man Funktionen abschaffen, weil Fermats letzter Satz so schwer zu beweisen war ...

Vielleicht liegt es einfach daran, dass das Problem für die meisten Menschen zu kompliziert ist?

Golo Roden hat gesagt…

PPS: "Wer keine Schusswaffe in die Hand bekommt oder nur eine mit Platzpatronen, der muss daran auch nicht ausgebildet werden."

Auch richtig. Wenn der jenige dann damit leben kann, dass er eben die Dinge nicht machen kann, zu denen man üblicherweise eine Schusswaffe braucht.

Wenn er das dann aber trotzdem machen will, ja dann muss er eben doch ausgebildet werden.

Geht nicht anders.

Christian Götz hat gesagt…

Hier muss ich Golo Roden zustimmen!

Das Problem sind nicht die Funktionen, sondern die Komplexität an sich.

Funktionen dienen ja dazu, große Probleme in kleine Probleme zu zerlegen.

Ich würde hier eher ein besseres Werkzeug fordern, welches dabei Hilft, den Überblick zu behalten.

Z.B. finde ich es auch lästig, wenn ich wissen will, was eine Funktion macht und ich nur das "Go to Definition" in der IDE zur Verfügung habe. Da wird man aus dem Kontext gerissen und sucht dann erstmal wieder seinen Ursprung.

Viel cooler wäre es, wenn man die Funktion an Ort und Stelle mal schnell aufklappen und vielleicht auch gleich modifizieren könnte.

Wir brauchen bessere Sichten auf den Code, damit man von der Komplexität nicht länger erschlagen wird. Die Werkzeuge müssen uns mehr unterstützen, damit man die Komplexität besser bewältigen kann.

Ein Problem haben wir nämlich immer, egal, was für Konstrukte man bevorzugt anwendet: Die Komplexität der Probleme und damit die steigende Anzahl der LOC.

Wir bräuchten Tools, mit denen man den Code buchstäblich modellieren und betrachten kann, wie es die Situation erfordert. Ich will nicht länger überlegen müssen "In welcher Datei war doch gleich die Funktion XYZ?"

Ich denke, Ralf spricht in seinem Beitrag das Richtige an, gibt aber dem sinnvollen Konzept, statt der Sicht auf die Dinge, die Schuld.

mfg

Christian

Ralf Westphal - One Man Think Tank hat gesagt…

@JJR: Wie lösen die von dir vorgeschlagenen immutable parameters denn die von mir dargestellten Probleme? Wie begrenzen sie Länge von Methoden bzw. führen zu einer konsequenten Trennung der Aspekte Logik und Integration? Und wie machen immutable params die Laufzeithierarchie zur Entwicklungszeit besser sichtbar?

Ralf Westphal - One Man Think Tank hat gesagt…

@Golo: Leider verstehe ich nicht die Vehemenz, mit der du gegen eine Vereinfachung eines Werkzeugs anrennst. Als sei Werkzeugkompliziertheit eine Tugend.

Bist du nicht froh über ein iPad im Vergleich zu einer Z3? Nein, nicht wegen der größeren Leistungsfähigkeit, sondern wegen der größeren Einfachheit.

Bist du nicht froh über Photoshop im Vergleich zu einer eigenen Dunkelkammer? Statt dich lange in die ganzen Gerätschaften und Schritte einer Dunkelkammer einarbeiten zu müssen, kannst du dieselben und bessere Ergebnisse am PC erzielen. Ausbildungsaufwand treiben, um Ergebnisse zu erzielen, ist keine Tugend.

Bist du nicht froh über ein digitale Camcorder und Final Cut/Premiere? Statt langer Ausbildung zu Kameramann und Schnitttechniker und teurem Equipment kannst du heute daheim Filme von hoher Qualität drehen. Schau dir "Monsters" an: der Film hat 25000 USD gekostet - und sieht aus wie mit einem Millionenbudget gedreht. Das war möglich, weil die Werkzeuge einfacher geworden sind.

"Kompliziert" ist ein relativer Begriff. Er setzt Werkzeuge untereinander in Beziehung, aber auch zum Gegenstand. Hier interessiert mich vor allem die Beziehung zum Gegenstand.

Dein Argument ist: Ein Werkzeug ist, wie es ist. Wenn es kompliziert/aufwändig in Bezug auf den Gegenstand erscheint, dann muss man seine Nutzer besser ausbilden.

Ich hingegen behaupte: Wenn sich ein Werkzeug im Hinblick auf den Gegenstand als kompliziert erweist, dann muss ich darüber nachdenken, wie ich es vereinfache. (Bis das geschafft ist, sollte ich natürlich versuchen, seine Nutzer angemessen auszubilden. Aber das ist - wie gesagt - keine Tugend.)

Bei dir, Golo, ist das Werkzeug eher statisch. Es ist halt, wie es ist. Das empfinde ich als fatalistische Einstellung. Damit wären wir nicht aus der Höhle rausgekommen. "Wenn du noch ein Problem mit dem Speer hast, dann musst du mehr üben." - so hätte der Höhlenmensch zum anderen gesagt.

Aber so ist die Weltgeschichte nicht gelaufen. Stattdessen ist aus dem Speer Pfeil und Bogen geworden. Und daraus ist die Armbrust geworden. Und daraus das Gewehr.

Bei jedem Schritt ist die Leistung gestiegen und der Aufwand, das vormalige Ergebnis zu erzielen, geringer. Die Latte wurde höher gelegt.

Mit einem Speer eine 10m entfernte Scheibe treffen, kannst du wahrscheinlich nicht. Mit Pfeil und Bogen wird es aber sofort irgendwie gehen. Und mit einem Gewehr bist du aus dem Stand noch besser.

Warum hätte also irgendwer beim Speer stehenbleiben sollen? Man kann damit eine Scheibe treffen, wenn man lange übt. Aber warum sollte ich üben, wenn es auch einfacher geht? Und verlässlicher obendrein? Das musst du mir mal erklären.

JakeJBlues hat gesagt…

Hallo Ralf,

ein Problem in der Wartung von Software (ist mir zumindest immer über den Weg gelaufen), ist dass man einer Methode einen Parameter übergibt, die Funktion jedoch nicht nur den Rückgabewert berechnet, sondern auch noch meinen Parameter "zerhaut" :-( Ich wollte auch nur "Munition" wegnehmen nicht gleich die ganze "Schusswaffe"....

bzgl. Deiner Anmerkung ->
"Wenn sich ein Werkzeug im Hinblick auf den Gegenstand als kompliziert erweist, dann muss ich darüber nachdenken, wie ich es vereinfache."

Ist der Gegenstand in diesem Zusammenhang der "Rechner" die "Software" oder "der User" der programmieren soll?

Gruß JJR

Ralf Westphal - One Man Think Tank hat gesagt…

@JJR: Der Gegenstand ist das Ergebnis, d.h. die Erfüllung von Anforderungen mittels Software.

Assembler hat sich als kompliziert erwiesen, um damit wachsende Anforderungen zu erfüllen. Deshalb wurden Hochsprachen entwickelt.

Wenn sich nun Funktionen als kompliziert erweisen, um alle Anforderungen - nicht nur die vom Kunden explizierten - zu erfüllen, dann scheint es mir nur konsequent, über eine Verbesserung des Werkzeugs nachzudenken.

Einstweilen bleibe ich dabei: Wenn Funktionen durch ihre Form (!) es so leicht machen, laaaaange Anweisungsfolgen zu schreiben, die dann auch noch Integration und Logik vermischen, wenn Funktionen darüber hinaus eine Hierarchie aufbauen, die wir zur Entwicklungszeit eher nicht sehen, so dass ein Entwicklungszeit/Laufzeit-Mismatch entsteht... dann ist da deutliches Verbesserungspotenzial.

Am Ende soll immer noch nützliche Software herauskommen. Aber einfacher hergestellt, mit weniger negativen Nebeneffekten.

Dass in der Mathematik Funktionen der Hit sind, interessiert mich da grad gar nicht. Die Mathematik löst andere Probleme als der gemeine Softwareentwickler.

Wenn die Mathematik einen Wert für die breite Masse an Softwareentwicklern darstellen sollte, dann nicht in Form ihrer konkreten Abstraktionen, sondern in ihrer Abstraktheit ganz allgemein. Das lässt dann offen, welche konkreten Abstraktionen wir für die Softwareentwicklung benutzen.

JakeJBlues hat gesagt…

Hallo Ralf,

OK, mal angenommen wir machen wirklich nur

Producer-Prosumer-Consumer

Innerhalb des "Prosumer"'s was darf ich aufrufen, verwenden.... ?Wenn ich im Prosumer wäre und ein Tripel "Producer-Prosumer-Consumer" in irgendeiner Art aufrufen, verwenden darf, bin ich doch soweit wie vorher, oder sehe ich da was "falsch" ....

Gruß JJR

Ralf Westphal - One Man Think Tank hat gesagt…

@JJR: Wir können uns einer Lösung nur in Schritten annähern.

Heute leben wir in einer funktionendominierten Welt von F# über C# und Java bis Scala, Clojure und Ruby. Funktionen allerorten. Und die Bibliotheken und Runtimes gehören zu diesem Paradigma.

Deshalb können wir es nicht einfach ganz anders machen. Aber ein bisschen geht schon...

Schritt 1: Du hast weiterhin einen Aufrufbaum zur Laufzeit. Von mir aus :-) Dann sei so konsequent und trenne Integration von Logik. Logik - d.h. Ausdrücke und Fallunterscheidungen - isolierst du in den Blättern. Und auch nur dort rufst du APIs auf, über die du keine Kontrolle hast (z.B. System.IO).

Schritt 2: Du hast keinen (relevanten) Aufrufbaum zur Laufzeit mehr für die Ausführung von Logik. Die Blätter werden vor ihrer Ausführung integriert (nicht während).

Schritt 3: Es gibt keinen 3GL Code mehr zur Integration. 3GL kommt nur noch in Blättern vor.

Das alles lässt sich mit heutigen Mitteln machen. Da müssen wir keine Großartigkeiten erfinden, um damit mal zu arbeiten - und den Unterschied, die Erleichterung zu empfinden gegenüber den Funktionsverhauen.

Ist ja nun auch nicht so, dass ich der einzige wäre, der so eine Vision hat. Andere formulieren sie nur anders, weniger "prinzipiell" oder provokant. Ich find es aber wichtig, genau hinzuschauen und auch mal zu verstören :-)

Golo Roden hat gesagt…

Natürlich ist es legitim, danach zu streben, ein Werkzeug zu vereinfachen. Dagegen sage ich ja gar nichts.

Aber doch bitte nur da, wo es sinnvoll ist.

Ein - wenn nicht DAS - Grundprinzip der Informatik ist das EVA-Prinzip: Du gibst irgendwelche Daten ein, sie werden verarbeitet, Du bekommst irgendwas zurück. Ein Programm, dem eine dieser drei Komponenten fehlen, ist bis auf ganz wenige Ausnahmen sinnfrei.

Und das Spannende an EVA ist nicht das E oder das A, sondern das V: Es geht um die Verarbeitung, um das, was ein Programm macht. Anders gesagt: Um seine Funktion.

Wie schon mal gesagt: Funktionen haben sich über mehrere Jahrtausende als Mittel der Wahl erwiesen, um mathematische Konstrukte aufzubauen und zu beschreiben. Dass auf einmal hier das Problem liegen soll, kann natürlich sein, ist aber unwahrscheinlich.

Nun kannst Du argumentieren, dass es früher weniger Probleme damit gab - ja, es gab früher aber auch weniger Nutzer. Vielleicht ist das Problem ja nicht die auf einmal gewonnene Einsicht, dass Funktionen ach so schwierig zu handhaben seien, sondern die gestiegene Benutzerbasis? Weil sich halt erst mal die Experten mit etwas befassen, und danach "das einfache Volk".

Es gibt Dinge, die lassen sich nicht verbessern. Ich kann keinen schnelleren Sortieralgorithmus finden als die bestehenden, das ist mathematisch bewiesen. Ich brauche auch mindestens so- und soviele Schritte, um das Minimum einer Menge zu finden. Auch das ist bewiesen. Hier zu versuchen, etwas zu verbessern, ist vergebene Liesbesmühe.

Du hast den Vergleich zu einem Speer und dessen "Nachfolgern" gezogen. Der Vergleich hinkt: Was Du versuchst, ist nicht, das Gewehr an Stelle eines Speers zu finden, sondern den Speer so einfach benutzbar zu machen, dass er mit Überlichtgeschwindigkeit fliegt. Und das geht nicht.

Du hast Daten als Eingabe.
Du hast Daten als Ausgabe.

Das ist gesetzt. Und E muss irgendwie zu A transformiert werden. Genau diese Aufgabe übernimmt die Funktion, die die Verarbeitung darstellt.

Das heißt, Du hast zwingend (!):

E -> V -> A

Ob Du das jetzt so hinschreibst, oder als

A = V(E)

oder als

E::V::A

oder als

E + V ===> A

ist dabei wurscht. Das Konzept der Funktion bleibt erhalten. Das kannst Du nicht auflösen, ohne das EVA-Prinzip zu verletzen. Es geht einfach nicht, egal wie Du es auch drehst und wendest.

Eine Funktion hat eine Eingabe und eine Ausgabe. Punkt. Und wenn sie void ist, hat sie sozusagen die leere Menge als Ausgabe.

Das ist in der Mathematik so.
Das ist in der Informatik so.
Das ist bei uns Menschen so: Aktion => Verarbeitung => Reaktion

Quasi ein kausaler Zusammenhang.

Und zu versuchen, daran zu rütteln, ist natürlich vollkommen legitim, aber aussichtslos.
Deswegen verweigere ich mich nicht prinzipiell dagegen, Werkzeuge einfacher zu machen, aber ich nutze meine Energie lieber auf Dinge, die zielführender sind.

PS: Etwas zu kritisieren ist leicht. Einen konstruktiven Vorschlag zu bringen, wie man es besser machen könnte, ist schon deutlich schwieriger. Wie sieht - konkret - Dein Vorschlag als Ersatz für Funktionen aus?

Ralf Westphal - One Man Think Tank hat gesagt…

@Golo: Definiere bitte mal "sinnvoll".

Denn ich halte eine Verbesserung hier für sinnvoll - du aber nicht. Dann nenne bitte ein Kriterium für Sinnenfülle, das so universell und absolut ist, dass es mich quasi zwingt, dir Recht zu geben :-)

Aber zu EVA: Natürlich geht es immer um EVA. Und dass ich das weiß, solltest du wissen, da du beobachtest, was ich so mache. Also nimm mal an, dass EVA auch weiterhin für mich eine Rolle spielt. Ich werde sie nicht verleugnen.

Wenn du meinen Artikel genau liest, dann wirst du auch sehen, dass ich EVA nicht erwähnt habe. EVA macht nicht das Problem. EVA ist unumgänglich, also Teil der Lösung. Auch in der Zukunft.

Ich habe mich gegen Funktionen gewendet. Den Begriff habe ich gewählt, weil er dieses C# Konstrukt beschreibt:

T f(S x) {...}

Entscheidend ist dabei nicht der Begriff. Er ist nur Platzhalter für ein Konzept. Das Konzept, was ich für problemerzeugend halte, das ist die Kopplung von Request/Response. Das habe ich mehrfach betont. Und da der Request leer sein kann und auch der Response leer sein kann, geht es noch allgemeiner um die Rückkehr zum Aufrufort nach einer Delegation einer Teilaufgabe an ein Unterprogramm.

Nochmal in Stichworten:

Problem #1: Wenn nach Delegation zurückgekehrt wird zum "Auftraggeber", dann gibt es kein Halten, wie umfangreich dieser Auftraggeber werden kann. Im Verbund mit fehlender Beschränkung dessen, was der Auftraggeber sonst noch alles machen kann, akkumuliert sich "Geschäftslogik" dann an einem Ort: beim Auftraggeber.

Ich denke, das kannst du bezeugen. Lange Methoden sind die Norm. Brownfield Code kommt ja nicht von nix.

Willst du was dagegen tun, muss du zusätzliche Regeln einführen und deren Einhaltung überwachen. CCD bietet ein solches Regelwerk.

Auch schön - aber umständlicher, als eine Praktik (oder gar ein Tool) zu haben, die es gar nicht erst dazu kommen lässt.

Problem #2: Unterprogramme können sich natürlich geschachtelt aufrufen. Bis in beliebige Tiefe. Ohne weitere Einschränkung durch Regeln, gibt es da kein Halten, wo Geschäftslogik zu finden ist. Sie kann überall in der Hierarchie sein. Das ist eines der gravierendsten Probleme; das führt direkt zu Brownfield Code en masse.

SLA kämpft dagegen. Auch schön. Aber auch dort, wo SLA eingesetzt wird - ja, auch bei Uncle Bob - bleibt es bei einer Vermischung von Logik und Integration. Das ist ein Widerspruch zu SRP.

Ich liefere hier nun ein Ideal, dessen Einhaltung sehr einfach zu erkennen ist: Wer Funktionen aufruft, muss frei von Geschäftslogik sein. (Wenn denn schon Funktionsaufrufe sein müssen.)

Ein DI Container ist ja auch frei von Geschäftslogik. Wunderbar. Da machen uns auch die vielen Abhängigkeiten nichts aus.

Und was das "besser machen" angeht... Da kennst du meine Position :-) Ich kann auch nicht immer was Neues erfinden. Mit meiner steilen These hier habe ich dafür nur eine neue, sehr einfach zu verstehende Begründung nachgeliefert.

Ausdrücklich formuliert: Flow-Design löst das Problem der gefährlichen Funktionen.

Ja, so simpel sieht meine kleine Welt aus :-)

JakeJBlues hat gesagt…

Hallo Ralf,

ich kann mit Flow-Design genauso viel "Sch..." bauen wie mit anderen Programmierkonzepten :-(

Wobei wir dann wieder bei der Ausbildung wären.....

Aber vielleicht gibt es irgendwann ein "Flow-Design"-Tool, welches nicht vom "Problem" vor dem PC abhängig ist, dann kann ich mir in Ruhe einen anderen Job suchen :-)

Gruß JJR

Golo Roden hat gesagt…

Die Sinnhaftigkeit ist in diesem Fall rekursiv aufzählbar: Ich kann "sinnvoll" nicht definieren, wohl aber "sinnlos".

Mal der Versuch einer Definition, die halbwegs formalen Ansprüchen gerecht wird:

"Sei P ein Problem. Der Versuch, eine Lösung L für das Problem P zu finden, wird genau dann als 'sinnlos' bezeichnet , wenn bekannt ist, dass L nicht existiert."

Nun stellt sich die Frage, was eine Funktion ist. Du hast bereits eine Definition geliefert, die ich gerne übernehme:

T f(S x) { ... }

Annahme: Für S und T gilt, dass sie ohne Beschränkung der Allgemeinheit für jeden beliebigen Typ stehen können.

Gemäß so ziemlich jeder Sprachdefinition ist auch 'void' ein Typ.

Demnach sind auch Funktionen, die nichts zurückgeben, Funktionen gemäß dieser Definition.

Du schreibst:

"Das Konzept, was ich für problemerzeugend halte, das ist die Kopplung von Request/Response."

Flow-Design basiert auf der Idee, den Response durch Fluss zu ersetzen. Statt dass eine Funktionseinheit einen Wert zurückgibt, wird der Wert an die nächste Funktionseinheit "weitergereicht".

Dazu wird, beispielsweise in EBCs, dann auf void-Funktionen und Events zurückgegriffen, auch wenn dies nicht zwingend so sein müsste.

Was haben wir also?

Aus

z = g(f(x))

haben wir nun ein

x -> f(x) -> y -> g(y) -> z

gemacht. Request und Response sind dort aber nach wie vor vorhanden, zumindest konzeptuell. Der einzige Unterschied ist, dass der "Ausgangspfeil" nach rechts zeigt, und nicht nach links.

Das ist prinzipiell das gleiche wie wenn ich

y = f(x)
z = g(y)

schreibe, also einfach sequenzielle Ausführung der Funktionen - eine Sequenz liegt ja wortmäßig nicht zufällig nahe an einem Fluss ;-).

Und wenn, um mal auf eine konkrete Implementierung zu sprechen zu kommen, ich EBCs als Beispiel für Flow-Design heranziehe, dann sind Events auch nichts anderes als eine Funktion - nur in einem anderen Sprachkonstrukt "verborgen".

Und: Flow-Design-Funktionen mit void als Rückgabetyp entsprechen obiger Definition von "Funktion", da void eben auch ein Typ ist.

Damit vermeidet auch Flow-Design keine Funktionen, benutzt und "maskiert" sie nur anders.

Der Kern ist der gleiche.

Fazit: Auch Flow-Design kommt nicht ohne Funktionen aus.

Und mit meinem Versuch einer Definition von "sinnlos" von oben sind wir damit an dem Punkt, dass feststeht, dass mit Flow-Design kein L für P existiert.

So interessant Flow-Design auch ist, aber es löst das Problem nicht, ohne Funktionen auszukommen.

q.e.d. ;-)

Golo Roden hat gesagt…

JakeJBlues: +1

Ralf Westphal - One Man Think Tank hat gesagt…

@JJR: Mit jeder Waffe kann man sich in den Fuß schießen. Für jede Waffe braucht man eine Ausbildung.

Wie ich mit dem Speer-Bogen-Armbrust-Gewehr Beispiel aber zeigen wollte, steigt bei einer Vereinfachung der Werkzeuge das Niveau, auf dem man sich in den Fuß schießt. Und es besteht Hoffnung, dass eben manche tumben Fehler weniger passieren.

Nochmal: Dass man sich auf ewig irgendwie in den Fuß schießen kann, sollte kein Argument gegen Kritik oder Weiterentwicklung sein. Wir säßen immer noch bei Assembler und Talglampen, wenn wir so dächten.

Der Übergang von Assembler zu Fortran und dann zu C und zu C++ hat uns - hoffentlich stimmen wir überein - weiter gebracht. Keiner hat gesagt, "Ach, mit so einer Hochsprache kann man immer noch Fehler machen. Lassen wir das sein."

Ergo: Lass uns nicht darüber sprechen, ob Weiterentwicklung sein sollte. Nehmen wir sie an.

Die Frage bleibt: Ist Request/Response ein Problem? Meine Antwort habe ich beschrieben. Eine Widerlegung habe ich hier noch nicht recht gesehen.

Golo Roden hat gesagt…

Request / Response ist kein Problem. Das ganze Web basiert darauf (konzeptuell!), und das funktioniert seit nun knapp 25 Jahren ganz gut ;-)

Ralf Westphal - One Man Think Tank hat gesagt…

@Golo: Du verwechselst Konzept und Implementation.

Dass

y = f(x)
z = g(y)

einen Fluss darstellt, sehe ich auch so. Diese Übersetzung für FD habe ich ja schon lange beschrieben.

Deshalb betone ich hier immer wieder: Solange wir auf 3GL festgenagelt sind, kommen wir nicht wirklich aus der "Funktionsnummer" raus.

Dass in einem Flow Transformationen stattfinden, sehe ich auch so. Nur ist es eben ein Unterschied, ob ich Integration (Verdrahtung) und Logik klar trenne - oder eben nicht.

Beim Flow ist die Trennung da, im "normalen" 3GL Code eben nicht. Wieder lege ich die Berge an unwartbarem Code als Beweis vor. Die Unwartbarkeit resultiert zu einem großen Teil aus eben dieser Vermischung.

Wenn wir aus der Vermischung einfach auf anderem Wege herauskommen, ist mir das recht. Ich sehe das jedoch nicht. Deshalb sehe ich einstweilen das Übel weiterhin darin, dass Request an Response gebunden ist und auf jeder Ebene einer Aufrufhierarchie deshalb alles erlaubt ist.

Wenn du dich mit FD ernsthaft beschäftigt haben solltest, wirst du festgestellt haben, dass der Schlüssel zum Erfolg mit FD darin besteht, dass 3GL eigentlich nur in den Blättern steht. Wenn du sie darüber verwendest, ist das nur deiner Bequemlichkeit ;-) geschuldet. Zumindest ist der Code oberhalb der Blätter trivial, weil frei von Logik. Deshalb schert er mich auch nicht besonders. Da mögen Funktionsaufrufe drin stehen, wenn´s grad noch nicht anders geht. Die tun nicht weh.

Und wenn du FD nicht konsequent betrieben haben solltest, dann bitte ich dich darum, es so zu versuchen: Baue gern weiter Funktionsaufrufbäume nach belieben. Aber halte alle Ebenen außer der untersten frei von Ausdrücken und Kontrollstrukturen.

Versuch das einmal, so wie du TDD as if you ment it in einer Code Kata versuchst. Und dann berichte von deinen Erkenntnissen.

Ralf Westphal - One Man Think Tank hat gesagt…

@Golo: Ich habe nichts gegen Request/Response. Ich habe nichts gegen WCF. Ich habe nichts gegen Threads. Ich habe nichts gegen Objektreferenzzählung. Ich habe auch nichts gegen Gewehre.

Nur all das und viel mehr gehört nicht in die Hände jedes Entwicklers. Und wir haben nicht die Ressourcen - wie inzw. offensichtlich sein sollte -, dass wir alle kompetent machen, diese Tools zu benutzen, ohne damit mittelfristig Probleme zu erzeugen.

Bei Objektreferenzzählung glaubst du mir das doch zumindest, oder? Und ich hoffe, dass du auch bei WCF da mit mir übereinstimmst.

VB hat zu einem Boom geführt, nicht C++. Warum nur?

Und auch .NET Remoting hat zu einem Book geführt, nicht DCOM. Warum nur?

Die Erklärung: Plötzlich waren vormals schwierige Sachen (UI, Memory Management, remote comm.) so einfach, dass damit viel mehr Leute umgehen konnten.

Und jetzt der Trick: War das immer gut so?

Ja und nein. Natürlich war es gut, dass da Sachen einfacher wurden.

Aber es war auf der anderen Seite problematisch, wenn dadurch Fundamentales, Paradigmatisches kaschiert oder außer Acht gelassen wurde.

Problem bei VB: Durch die große Einfachheit in Bezug auf UI und Objekte gab es kein Halten mehr. Es wurde drauf los programmiert. Wesentliche Paradigmen wie SoC wurden nicht beachtet.

Sollte deshalb der Fortschritt aber auf dem Niveau von C/C++ und Windows um 1989 stehenbleiben?

Dito bei .NET Remoting. Verteilung wurde so einfach, dass man vergaß zu lehren, dass darauf zu achten sei, wie das mit der Latenz und Sicherheit usw. war.

Sollte man deshalb aber auf dem Niveau von C/C++ und Sockets stehenbleiben? Kaum.

Mit WCF ist man nun übers Ziel hinaus geschossen. Man hat manches korrigiert - dafür aber wieder ein sehr kompliziertes Tool geschaffen. Das macht keine Freude mehr. Es muss wieder einfacher werden - die Empfehlung, mehr zu lernen, geht am Grundproblem vorbei.

Schließlich das mit Request/Response. Das hat Vorteile gebracht. Es ist in unserem Stammhirn. Aber nun sind wir an einem Punkt, wo wir die Nachteile zu spüren bekommen: schwierig einzudämmende Unwartbarkeit.

Deshalb bin ich dafür, einen anderen Weg einzuschlagen. Auf dem kann man wieder Fehler machen. Aber hoffentlich auf einem höheren Niveau. So wie mit VB oder .NET Remoting.

JakeJBlues hat gesagt…

Hallo Ralf,

bei Deiner Argumentation verwendest Du immer -> Funktionen produzieren "Berge von unwartbarem Code", deshalb keine Funktionen, sondern "FLOW-DESIGN" ....
Glaubst Du wirklich, dass mit FLOW-DESIGN das "Problem" vor dem Computer massenweise "Wartbaren Code" produziert .....

oder erscheint er Dir nur "wartbarer", weil man Code-Teile bei "Nicht-Gefallen" leichter eliminieren kann ....

Ich bin geneigt zu sagen, der "sinnvolle" Umgang mit Flow-Design kann zu wartbarem Code führen, wie auch der sinnvolle Umgang mit Funktionen....

Wie Golo es schon sagte, "sinnfrei" bleibt "sinnfrei" .....

Gruß JJR

Ralf Westphal - One Man Think Tank hat gesagt…

@JJR: Auch mit FD kann man Verhaue produzieren. Auch FD muss man lernen.

Mit Objekten und Funktionen habe ich mich nun lange redlich bemüht. 30 Jahre mache ich das nun schon. Und in den letzten Jahren noch bewusster.

Aber bei allem redlichen Bemühen sehe ich nicht, dass es grundsätzlich voran geht. Mit Prinzipien geht es besser als ohne. Nur stoße ich dennoch an eine gläserne Decke. Und ich sehe das auch bei anderen.

Meine Diagnose: Auch mit CCD sind wir gefangen in einem begrenzenden Weltbild. Vielleicht sollte ich es das der von-Neumann-Maschine nennen? Es ist jedenfalls fundamental, unterhalb unserer normalen Bewusstseinsschwelle.

Der Fisch sieht nicht das Wasser in dem er schwimmt. Und so merken wir nicht, worin wir dümpeln.

Wenn wir SOLID nur innerhalb dieses Paradigmas anwenden, kommen wir nicht wirklich raus aus dem Problem. Deshalb habe ich zu xSOLID bei G+ geschrieben. Wir müssen die Prinzipien wirklich, wirklich ernst nehmen und dehnen. Dann haben wir eine Chance, aus der unsichtbaren Eierschale unseres Paradigmas auszubrechen.

Dabei ist es doch so einfach zu sehen: Ich kann dir ein beliebiges OSS Projekt geben. Und dann frage ich dich, "Wie funktioniert das?" Du wirst mir nicht antworten können. Der Grund: Man kann das nur sehr, sehr schwer aus dem Code herauslesen. (Und ich setze schon voraus, dass du die Problemdomäne verstehst.)

Das kann doch aber nicht sein. Wir reden doch nicht über eine Pyramide, die tausende Jahre unverändert stehen soll. Wir reden über eine hochvolatile Maschine. Und deren Funktionalität soll man nicht ablesen können, wo man doch ständig daran rumschrauben muss? Die soll nicht einfach zu ändern sein, wo das doch ständig nötig ist?

Nein, nein, solange das nicht einfacher wird, ist etwas im Argen. Wir nutzen die Paradigmen und Werkzeuge quasi auf dem Niveau von Leuten der 1950er oder 1980er. Da macht auch Intellisense keinen Unterschied. Nur das Zeug, was wir damit bauen, ist um 10erpotenzen komplexer.

Damit es besser wird, reicht es nicht, wenn wir den Baum an der Krone stutzen. Wir müssen die Axt am Stamm anlegen. Und mit FD haben ich eine Ahnung davon bekommen, dass das etwas nützt.

Palin hat gesagt…

Ich finde die Tonnen an nicht wartbaren Code liegen zum Größten Teil daran das Prinzipien wie SOLID verletzt wurden.

Und guten Code kann ich nur bekommen wenn ich mich an Prinzipien wie SOLID, DRY usw halte, dazu gibt es halt noch Entwurfsmuster und TDD.

Ich denke FD baut auch auf Verständnis diesen Prinzipien und Techniken auf.

Und mit FD kann ich jetzt natürlich ein weiteres Prinzip einführen was die Leute auch wider nicht kennen, falsch verstehen oder machen was sie Möchten (OK die Ausnahme wird es dann richtig Anwenden, wie es ja mit den jetzigen Prinzipien auch schon läuft).

Für mich stellt sich hier einfach die Frage, brauche ich noch ein weiteres Prinzip.

Oder um es mal umgekehrt auszudrücken welche Prinzipien(Techniken) sollte ein Entwickler kennen um "guten" Code zu produzieren?

Und sollte man dann nicht genau daran arbeiten den Leuten, diese Prinzipien beizubringen?

Golo Roden hat gesagt…

"Nur all das und viel mehr gehört nicht in die Hände jedes Entwicklers."

So weit einverstanden.

"Und wir haben nicht die Ressourcen - wie inzw. offensichtlich sein sollte -, dass wir alle kompetent machen, diese Tools zu benutzen, ohne damit mittelfristig Probleme zu erzeugen."

Auch einverstanden.

Aber Du ziehst in meinen Augen den falschen Schluss:

Denn dass wir nicht alle kompetent machen können, liegt nicht nur an den Ressourcen. Das liegt auch an den "allen".

Das Problem ist meines Erachtens nicht die Technik, das Problem ist der Mensch.

Zu viele Entwickler, die sich nicht mit der Thematik beschäftigen WOLLEN. Die heute noch kein yield kennen, die heute noch nicht wissen, was ein Lambdaausdruck ist, die heute noch nicht wissen, was ein Delegate so genau eigentlich ist.

DESHALB funktioniert es nicht, alle kompetent zu machen: Weil nicht alle dafür geeignet sind.

Ich versuche auch nicht, filigrane Kunstwerke zu schnitzen, um davon leben zu können, weil mir die Kompetenz dazu fehlt, und ich auch nicht die geeigneten Voraussetzungen dazu mitbringe. Weder habe ich das notwendige Feinmotorik, noch genug Übung mit einem Schnitzmesser. Und ganz davon abgesehen interessiert es mich auch nicht.

Muss deswegen das Schnitzen vereinfacht und das Messer verbessert werden? Brauchen wir deshalb einen Nachfolger für ein Messer?

Nein. Ich muss mich damit abfinden, eben nicht filigrane Dinge schnitzen zu können, ob mir das nun passt oder nicht.

Und genau DAS müsste auch unsere Branche mal kapieren: Dass eben nicht jeder als Entwickler geeignet ist. Damit wäre viel gewonnen, und man bräuchte nicht so viel Zeit und Energie in Dinge zu investieren, die für diejenigen, die die entsprechenden Voraussetzungen mitbringen, kein oder kaum ein Problem darstellen.

Just my 2 cents ...

Golo Roden hat gesagt…

Um es in einem Satz zusammenzufassen:

Wenn all diejenigen, die keine Leidenschaft für die Softwareentwicklung und keine intrinsische Neugier und Motivation auf Neues mitbringen, die IT-Branche verlassen würden, hätten wir deutlich weniger Probleme.

Und falls Du mir das nicht glaubst: Erfolgreiche Startups bestehen NUR aus Leuten, die Leidenschaft und Neugier für das mitbringen, was sie machen. Die haben solche Probleme eher selten ...

Christian Götz hat gesagt…

@Golo:
Und falls Du mir das nicht glaubst: Erfolgreiche Startups bestehen NUR aus Leuten, die Leidenschaft und Neugier für das mitbringen, was sie machen. Die haben solche Probleme eher selten ...

Das liegt auch viel an deren mitgebrachter Unternehmensphilosophie und Offenheit für Neues.
Ich denke, die Leidenschaft kann sich da auch besser entfalten, weil alle im Selben Boot bzw. auf gleicher Augenhöhe sitzen.

Ich denke mir, in den meisten Unternehmen kann sich diese Leidenschaft gar nicht entfalten, weil da zu viele BWLer im Weg stehen. ("Kostet zu viel. Dauert zu lang. Wollen kein Risiko eingehen. Hilfe, schon wieder was Neues?! etc.")

BWLer und Softwareentwickler sind wie Feuer und Wasser. Die passen einfach nicht zusammen. BWLer haben einfach keine Vorstellung davon, was Softwareentwicklung bedeutet. Die wollen nur die fertige Software und setzen die Termine teils auf utopische Werte.

Bei sowas kann sich meine Leidenschaft leider nicht so entfalten, wie ich es gerne hätte. Also ordnet man sich meist dem BWLer unter und rotzt den Code eben so hin, auch wenn einem innerlich dabei alles weh tut, weil es nicht sauber ist. (Oder man beginnt sauber und wird zur Deadline hin immer schlampiger, weil keine Zeit mehr ist. Da bricht man dann mit den guten Vorsätzen, damit der Termin gehalten werden kann.)

Ich denke auch, dass dies das eigentliche Problem ist.

Ralf Westphal - One Man Think Tank hat gesagt…

@Palin: Ob du mehr Prinzipien oder sonstwas brauchst, kannst du nicht aus der Zahl der Prinzipien ableiten, die du schon hast. Du musst dir das Ergebnis des Umgangs mit den Prinzipien anschauen.

Dieses Ergebnis sieht für mich frustrierend aus.

Die Prinzipien mögen nicht falsch sein - aber ineffizient.

"Du musst mehr auf SRP und SLA und SoC und LoD usw. achten! Los!"

Das habe ich lange gepredigt. Mit mäßigem Erfolg.

Wenn ich aber heute Flow-Design unterrichte... dann geht es viel einfacher. Durch Hinzufügen eines weiteren Konzepts mache ich mir und den Teilnehmern leichter.

@Golo: "Problem Mensch"... ja, da war es wieder, das Menschenbild. Der Mensch ist das Problem. Der ist unmotiviert, störrisch, beschränkt... Ach, wenn der Mensch nur ein anderer wäre, dann wäre die Welt eine bessere.

Sicher gibt es auch immer Leute, die schwer von Begriff sind. Die Normalverteilung lauert überall. Aber wie erklärst du es dir, dass heute 10 Jährige Sachen können, die früher Erwachsene nicht konnten? Hat sich der Mensch verändert? Oder liegt es nicht vielmehr an den Unterrichtsmethoden oder den vereinfachten Werkzeugen?

Wir sollten nicht darauf hoffen, dass die Entwickler schlauer werden. Da wir mehr brauchen als wir haben, ist eher zu befürchten, dass wir in Zukunft mehr haben, die weniger gescheit und motiviert sind. Und trotzdem müssen wir unsere Softwareprobleme lösen. Wie das?

Wir müssen unsere Konzepte verbessern, auch unsere Tools, ja, auch unsere Ausbildung. Aber bitte wünsch dir doch keine anderen Menschen.

Technologien, Werkzeuge alles ist für den Menschen da, nicht umgekehrt. Autos sehen heute so aus wie sie aussehen, weil die Menschen nicht alle Automechaniker werden können und wollen, um Auto zu fahren. Herr Daimler hätte sich vielleicht auch lieber andere Autofahrer gewünscht damals...

Get real: Wenn eine Technologie oder ein Konzept über Jahrzehnte Schwierigkeiten macht, dann ist das kein Problem der Menschen, sondern ein Problem des Konzepts.

Das war bei RPC bzw. Remote Objects so, das ist bei der "normalen" OOP so, das ist bei Request/Response so :-)

JakeJBlues hat gesagt…

Hallo Ralf,

"Das war bei RPC bzw. Remote Objects so, das ist bei der "normalen" OOP so, das ist bei Request/Response so :-)"

Du hast vergessen zu erwähnen, dass es bei "FD" und jedem anderen Konzept auch so sein wird....

Aus diesem Grunde kann man zurecht behaupten, dass "Flow-Design considered harmful", wir sollten erst gar nicht anfangen... :-S

Obwohl ist nicht dieser Meinung bin!

Gruß JJR
P.S.: Es wird kein "Konzept" geben, welches von allen Menschen so umgesetzt werden wird, wie sich der Konzeptersteller es sich wünscht ;-) ...

Ralf Westphal - One Man Think Tank hat gesagt…

@JJR: Du hast völlig recht, dass jedes Konzept, jedes Tool missverstanden und falsch "im Sinne des Erfinders" eingesetzt wird.

Meine Hoffnung ist allerdings, dass all das mit der Zeit auf immer höherem Niveau geschieht.

Beispiel: Weil wir heute GC haben, muss über manches, worüber wir vor 10 Jahren uns noch die Köpfe heiß geredet haben, nicht mehr gesprochen werden. Wir können uns auf Wichtigeres konzentrieren.

Irgendwas ist immmer. Und trotzdem glauben wir an den Fortschritt. Nein, wir fühlen es, dass es Fortschritt gibt.

Jeden Tag mit dem Auto 90min zu pendeln und dabei im Stau zu stehen, ist doof. Aber jeden Tag 90min draußen an einem Zug zu hängen, wie es in Indien Millionen wohl tun, ist doofer.

In diesem Sinn: Mir über FD "Verhaue" die Haare zu raufen ist für mich ein wünschenswertes Problem im Vergleich zum Reverse Engineering von Abhängigkeitshierarchien :-)

Anonym hat gesagt…

Hallo Ralf!

Mal wieder viel zu verdauen und spät zur Party gekommen.

Spontan habe ich das Gefühl, dass wir traditionell im EVA-Prinzip das Hauptaugenmerk und damit den Fokus auf V legen. V ist für meine Begriffe ein Kürzel, welche den Weg "von E zu A" benennt.

An dieser Stelle gibt es mehrere Probleme:
- man hat nicht genügend atomare Operationen im Sprachschatz
- man hat genügend Operationen, findet diese aber nicht, weil Code Completion sie nicht liefert
- man schreibt eine Spike zum Verständnis einer API, die dann zum Legacy-code ausartet
- Code von zweifelhafter Wiederverwendbarkeit wird als firmeninterne API über eine API gelegt, um "Einarbeitungsaufwand zu sparen"; wie oft habe ich schon gehört "du kennst dich doch aus, schreib mal eine Standardfunktion, die das Problem erschlägt, der braucht das".

Flow Design ist für meine Begriffe ein Ansatz, eine Beschreibung von V nicht nur verbal sondern auch im Code zu bekommen. Dazu werden atomare AKA SRP Funktionseinheiten AKA Blätter aneinandergereiht, die die Eingabe in Zwischenergebnisse bis hin zum Endergebnis umwandeln. V ist hierbei ein Komposit, so wie jede nicht-atomare Unterfunktion ein Komposit ist.

Ich implementiere FD bis dato immer mit Extension-Methods. Nehmen wir mal dein Beispiel:

A
S
T
U
B
mit impliziter Eingabe [E] und Ausgabe [A] wird zu

[V] => [A] = [E].A.S.T.U.B

Wenn STU eine einen Standardfall oder eine Kategorie bildet, dann gruppiere ich die einfach als neue Extension, die genauso einfach wieder auffindbar ist.

[F] => [A] = [E].S.T.U
[V] => [A] = [E].A.F.B

Wenn ich an einer Stelle T nicht benötige, kann ich immer noch atomar

[V] => [A] = [E].A.S.U.B

verwenden.

Natürlich: die Blätter-Funktionen sind momentan nur durch Selbstbeschränkung un konsequentes Einhalten von SRP erreichbar. Der Blick auf die Komposite bleibt getrübt, so lange man sich nicht auf eine oder maximal zwei Ebenen beschränkt.

Aber hier geht es um Wartbarkeit. Eine atomare Funktion zu verstehen ist trivial, einen Komposit zu verstehen ist trivialer. Die atomare Funktion wird darüber hinaus per Unit-Test in ihrer Konsistenz garantiert.

Für mich ist das bis dato das Maximum an Wartbarkeit. Lasse mich aber gern immer eine Besseren belehren.


Gruß,

Markus

Ralf Westphal - One Man Think Tank hat gesagt…

@Markus: Deinen Ansatz mit den Extension Methods finde ich völlig ok. Ob das Funktionen sind oder nicht, ist nicht so wichtig. Denn du verwendest Methoden hier auf eine Weise, dass deutlich zwischen Operation (Logik) und Integration (Verdrahtung) unterschieden wird.

Extension Methods sind ein Mittel, um zu vermeiden, dass zwischen zwei Methodenaufrufe Logik schlüpft.

Frank hat gesagt…

Viele von den Kommentaren wäre uns wohl allen erspart geblieben, wenn der Titel gelautet hätte:

Nested Function Calls considered harmful

und/oder

Mixed-abstraction-level Functions considered harmful

Du kritisierst ja wohl nicht die Funktionen an sich, sondern einerseits ihre Schachtelung und anderseits die Mischung von Funktionsaufrufen und anderen Anweisungen innerhalb einer Funktion.

Davon abgesehen, stimme ich dir zwar darin zu, dass man nicht für alles, was man kritisiert, eine Lösung parat haben muss. Gerade in einer großen Community wie der Software-Entwicklungs-Community im Netz, kann es viel effizienter sein, eine kritische These ohne Lösungsansatz aufzustellen und dann zu sammeln, was an Lösungsvorschlägen kommt. Wenn man hunderte Gehirne auf das Problem ansetzt und aus dem Feedback die beste oder die besten Lösungen auswählt, bekommt man im Schnitt bessere Resultate, als wenn man gezwungen ist, sich alleine was auszudenken.

Allerdings ist es hier ja wohl so, dass du das, was du als Lösung ansiehst, doch schon ganz genau kanntest. Und dass andere sinnvolle Lösungsmöglichkeiten nicht wirklich genannt wurden. Dadurch geht dein Lösungsvorschlag "Flow-Design löst das Problem der gefährlichen Funktionen", den du im Grunde nur in diesem einen Satz in einem der mittleren Kommentare nennst, vollkommen unter. Ich weiß, du hast viele Artikel über Flow Design geschrieben und jeder interessierte kann das jederzeit nachlesen. Trotzdem erscheint es mir sinnvoll, dass du einen zweiten Teil zu diesem Blog-Artikel schreibst, in dem du konkret und im einzelnen aufzeigst, wie in denen Augen Flow-Design die angesprochenen Probleme löst.

Ich sehe z.B. nicht, wie FD verhindert, was du unter Punkt 2 kritisierst:

"Inhaltliche Sequenzen werden aufgelöst. Zur Entwicklungszeit jedoch wird es immer schwieriger zu verstehen, was da eigentlich passiert."

Im Gegenteil scheint mir dein Beispiel

"Client:
Vorbereitender Code erzeugt X
Y= f(X)
Nachbereitender Code verarbeitet Y

wird Producer-Consumer-Code:

Producer:
Vorbereitender Code erzeugt X
Versenden von X

f als Prosumer:
X nach Y transformieren
Versenden von Y

Consumer:
Nachbereiten von Y"

zu zeigen, dass durch FD alles in extrem kleine Einheiten zerrissen wird. Solche und andere denkbare Fragen könntest du in einem zweiten Teil des Blog-Artikels klären.

Wobei ich sagen muss, dass ich dir schon bei der Problembeschreibung nicht in allen Punkten zustimme. Du schreibst z.B.

"Die Einführung des Unterprogramms F zerstört diese verständliche Einheit. Was zwischendurch passiert, steht nun irgendwo. Wer nun liest A; CALL F; B; der ist darauf angewiesen, dass F ein ausdrucksstarker Name ist, um zu verstehen, was da passiert."

Das sehe ich allerdings weniger als Problem, als vielmehr als Chance. Denn

A
BerechneBruttoPreis
B

ist wohl einfacher zu verstehen als


A
if (kategorie == 0) {
preis *= 1.07;
} else {
preis *= 1.19;
}
B

selbst wenn oder gerade weil man im ersten Fall nicht im Detail weiß, wie der Bruttopreis berechnet wird. Die Abstraktion schärft den Blick für das Wesentliche.

McZ hat gesagt…

@Frank
"A
BerechneBruttoPreis
B

ist wohl einfacher zu verstehen als


A
if (kategorie == 0) {
preis *= 1.07;
} else {
preis *= 1.19;
}
B"

In diesem Fall ist BerechneBruttoPreis eine Blatt-Funktion und damit ist im konkreten hardcodierten Fall der Einsatz gerechtfertigt. Selbst in FD wäre dies eine Funktionseinheit.

Im Prinzip benennst du aber bereits durch die Konstanten deine Erweiterungspunkte.

Was, wenn eine oder beide Konstanten je nach Land unterschieden werden oder gar dynamisch sind? Dann würdest du entweder eine weitere Funktionsebene einfügen, die die voeher konstanten Werte ersetzt, oder gleich MwstSatz als eine gleichberechtigte Eigenschaft führen, die dann in einem separaten Schritt vorher ermittelt wird.

Der Unterschied ist: in ersterem Fall wird dein Code zu einem Komposit, im zweiten Fall bleibt es ein Blatt, nur die Implementierung ändert sich.

Hinzu kommt die Prozessebene. Ich muss MwstSatz nur ermitteln, wenn sich das Land in der Adresse des Auftrags ändert. BruttoPreis wird berechnet, wenn sich der NettoPreis oder der MwstSatz ändern. Ich würde also sogar soweit gehen zu sagen, dass dies zwei völlig getrennte Prozesse sind.

Gruß,


Markus

Frank hat gesagt…

@Markus:

Um Blatt oder nicht, Funktionseinheit oder nicht, Konstante oder nicht, sogar Flow Design oder nicht ging es doch bei meiner Aussage gar nicht. Sondern es ging nur darum, was von beidem verständlicher ist: Code direkt eingesetzt oder Code in Funktion ausgelagert. Ich habe mich auf ein konkretes Zitat von Ralf bezogen.

Je mehr von dem, was du bezüglich der Implementierung angesprochen hast (Änderung des Mehrwertsteuersatzes, andere Länder) in dieselbe einfließt, je länger und komplexer diese also wird, desto stärker wird mein Argument. Nochmal: Es ging um Gegenargument zu der Aussage von Ralf bezüglich der Verständlichkeit.

McZ hat gesagt…

@Frank
"Um Blatt oder nicht, Funktionseinheit oder nicht, Konstante oder nicht, sogar Flow Design oder nicht ging es doch bei meiner Aussage gar nicht. Sondern es ging nur darum, was von beidem verständlicher ist: Code direkt eingesetzt oder Code in Funktion ausgelagert. Ich habe mich auf ein konkretes Zitat von Ralf bezogen."

Schon verstanden. Wenn du aufmerksam gelesen hast wirst du bemerkt haben, dass ich in deinem dargelegten einfachen Fall das auch für absolut in Ordnung halte. Auf die kaufmännische Korrektheit gehe ich jetzt nicht ein.

Aber es geht hier um Wartbarkeit in evolvierendem Code. Deshalb habe ich auf eine der offensichtlichen Evolutionsrichtungen der Funktion hingewiesen.

Ich habe also in meiner Argumentation eine Code-Änderung vorgenommen um zu verdeutlichen, wo die Krux liegt, sollte der Code wachsen.

"Je mehr von dem, was du bezüglich der Implementierung angesprochen hast (Änderung des Mehrwertsteuersatzes, andere Länder) in dieselbe einfließt, je länger und komplexer diese also wird, desto stärker wird mein Argument."

Je mehr du dich mit dem, was ich ganz am Ende geschrieben habe, befasst, wirst du merken, dass dein Argument schwächer wird.

Ein sich verändernder MwstSatz würde per Definition zu einem völlig anderen Zeitpunkt ermittelt als der BruttoPreis. Die Änderung hat einen völlig anderen Trigger.

Die logischen Prozesse sind:
1) Änderung Land - Ermittlung
MwstSatz
2) Änderung Kategorie - Ermittlung MwstSatz
3) Änderung MwstSatz - Ermittlung BruttoPreis
4) Änderung NettoPreis - Ermittlung BruttoPreis
(usw...)

(Umgekehrt gelesen ergibt das quasi die Abhängigkeiten der Eigenschaften untereinander.)

Es gibt also vier separate Prozesse/Flows und zwei atomare Funktionen.

(1) Ermittle für das Land beide MwstSätze; je nach Kategorie gib - falls vorhanden - den niedrigen, ansonsten den höheren MwstSatz zurück (das Speichern in der Eigenschaft MwstSatz triggert Prozess 2)
(2) Nehme die Werte MwstSatz und NettoPreis, berechne den Bruttopreis per BP = NP * (1 + MWST / 100) und gib das Ergebnis zurück (das Speichern in der Eigenschaft BruttoPreis triggert wiederum andere Prozesse, z.B. die Rabattberechnung; liegt aber außerhalb des Scope)

Die Implementierung wird eben nicht komplexer und eben nicht länger, nur anders, man könnte auch sagen kompletter. Beide Funktionen sind gemäß SRP korrekt und einfach testbar.

Setze ich hier auf eine einzige Funktion die beides erledigt, habe ich tatsächlich eine komplexe Aktion, die noch dazu in 90% der Fälle zuviel tut (denn bei der statistisch häufigeren Änderung des NettoPreises brauche ich den MwstSatz nicht neu zu ermitteln, benötige also keinen Repository-Hit, spare somit Ressourcen).

Dies zu erkennen ist meines Erachtens essentiell.

Darauf wollte ich hinaus, weil schon dieses einfache Beispiel zeigt, wie wichtig das Denken in Prozessen aka Flows ist.

Gruß,


Markus

Frank hat gesagt…

@Markus:

Ich kann es nur noch mal sagen: Darum ging es überhaupt nicht. Statt der Geschichte mit der Mehrwertsteuer hätte ich auch ein beliebiges anders Beispiel verwenden können. Denk dir den Code mit der Mehrwertsteuer weg und Code deiner Wahl hin.

Auch Ralf war bei dem Punkt, auf den ich mich bezogen habe, überhaupt noch nicht bei Flow Design, sondern nur bei ganz normalen Funktionen. Es geht nur um die Frage: Führt das Auslagen von Code in eine extra Funktion zu mehr oder weniger Verständlichkeit an der Aufrufstelle.

Ralf hat seine Sichtweise beschrien, ich meine. Jeder Leser kann selbst entscheiden.

Um Mehrwertsteuer und Evolvierbarkeit ging es überhaupt nicht. Schade, dass du das nicht erkannt hast.