Dieses Blog durchsuchen

Sonntag, 11. Juli 2010

Ansichten ändern sich!

Es ist nun schon eine Weile her, als ich hier einen Beitrag geschrieben hatte. Ich nutzte die Zeit, um mit der MPT ein wenig Erfahrung zu sammeln. Dies tat ich mit einem Projekt auf Arbeit.

Naja, es funktionierte schon alles so, wie ich mir das gedacht hatte. Jedes Objekt war im Grunde unabhängig voneinander und der Datenfluss geschah eher passiv. Eigentlich genau das, was ich wollte, oder?
Nun ja, nicht ganz.
So bereitete mir z.B. die Idee des Transmitters in einem Anwendungsfall ziemliche Probleme:
Nehmen wir an Objekt A ist ein Transmitter für irgendeinen Wert X.
Objekt B und Objekt C empfangen diesen Wert X.
Objekt B fordert den Wert X an. Diesen Wert bekommt Objekt B von Objekt A. Aber auch Objekt C bekommt diesen Wert X, weil es ja ebenfalls als Empfänger für diesen Wert registriert ist.

Das scheint soweit OK zu sein, oder? In meinem Fall war dies aber nicht gewollt, weil das unaufgeforderte Empfangen des Wertes X eine Ereigniskette ausgelöst hat, die nicht hätte ausgelöst werden dürfen.
Also, wenn Objekt A den Wert X anfordert, darf ihn nicht Objekt B bekommen. Und wenn Objekt B den Wert X anfordert, darf ihn nicht Objekt A bekommen.
Das ist ein ziemliches Dilemma, welches einen schon mal verzweifeln lässt.
Meine erste Idee war es ja, bei den Objekten A und B eine Flag-Variable einzuführen, welche in der Empfangsroutine prüft, ob der Wert X überhaupt angefordert wurde.
Doch das schlug ich mir schnell wieder aus den Kopf, denn so eine "Prüfung" verschmutzt die Businesslogik. Zudem wissen die Objekte gar nicht, was um sie herum geschieht. Diese Flag-Variable würde aber indirekt andeuten, dass dieses Objekt etwas über seine Umgebung weiß. Und das wäre schlecht!

Ich überlegte lange, wie ich das Problem lösen könnte. Dann kam mir die Idee! Man könnte ja einfach ein Objekt dazwischen schalten, welches wie ein Ventil funktioniert. Man schaltet es z.B. einfach zwischen Objekt A und Objekt B. Die Anforderung von Objekt B geht durch das "Ventil". Dabei wird in dem "Ventil" ein Flag gesetzt, welches den Empfang von Wert X erlaubt. Es ist somit "offen". Wenn dann der Wert kommt, wird der Wert an Objekt B durchgereicht und das Ventil wird wieder geschlossen. Bei einem unaufgeforderten Empfang wird der Wert einfach abgewiesen und Objekt B bekommt davon gar nix mit.

Nachdem ich das so eingebaut hatte, funktionierte alles wieder problemlos. Die Ventile sorgten für den gewünschten Datenfluss und die Businesslogik hatte sich absolut nicht geändert. Genial!

Unzufrieden, trotz entkoppelter Objekte
Die Anwendung funktionierte, aber mir war unwohl bei dem Gedanke, dass noch mehr Funktionen in die Software fließen sollten.
Wie kann das sein? Die Entkopplung sollte ja eigentlich das Gegenteil bewirken und Sicherheit bei der Implementation neuer Funktionen erzeugen. Ein Grund für die Unsicherheit war wohl auch das oben geschilderte Szenario.
Viel schlimmer war aber gerade die totale Isolierung der einzelnen Businessobjekte zueinander. Das heißt, ich konnte aus einem Objekt heraus nicht die Kommunikationsstrecke verfolgen.
Wollte ich also wissen, was passiert, wenn ich auf einen bestimmten Button klicke, so kam ich zwar zur entsprechenden Aufrufmethode, doch dann war da eine Sackgasse. Der Isolation sei "Dank", stand da in den jeweiligen Methoden nur die Floskel "Say.Carefully(MacheIrgendwasSinnvolles, this)".
"Ach ja, toll! Und welches Objekt ist jetzt nochmal dafür zuständig, diese Sache auszuführen?" fragte ich mich des öfteren.
Denn leider steht man hier wirklich in der Sackgasse. Man kann nicht einfach zur Definition von "MacheIrgendwasSinnvolles" springen, um den Programmfluss nachzuvollziehen. Das ist der Preis, den man für die Isolation bezahlt. Und der ist teuer!
Um also zu wissen, welches Objekt auf dieses Ereignis reagiert, muss man sich die Verdrahtung angucken. Gut, jetzt hat man das verantwortliche Objekt lokalisiert. Man geht in die Klasse und sucht sich die entsprechende Methode. Die Freude hält nicht lange an, denn in der Methode steht auf einmal der Aufruf "Say.Carefully(INeedEinenBestimmtenWert, this)". "OMG!"
Aber zum Glück ist es an der Stelle tatsächlich nicht wichtig, wo der Wert herkommt. Ich suche in der Klasse schnell die passende Receive-Methode und fahre mit der Programmverfolgung fort. In der Receive-Methode wird Business-Logik ausgeführt. Ein neuer Wert wird darin berechnet. (Um Missverständnissen vorzubeugen: Diese Werte sind natürlich auch Objekte. Es sind aber eben spezielle Value-Objekte und keine primitiven Datentypen! Ich bin auch dabei, mir abzugewöhnen, primitive Datentypen direkt zu transportieren, weil diese einfach nichts aussagen und zudem nicht erweiterbar sind.)
Weiter geht's: Am Ende dieser Methode steht dann also wieder "Say.Carefully(SendNeuerWert, this)".
Jetzt geht das Geeier wieder los. "Wer nimmt den Wert entgegen und was passiert damit?". Also wieder zurück und von "oben" drauf gucken, weil ja das Objekt für sich isoliert gewesen ist.
So geht das dann immer weiter. Besonders "lustig" wird es dann, wenn man wirklich einen Bug lokalisieren muss und die Zeit drängt. Da springt man hin und her ... hoch und runter. Wenn man Glück hat, findet man den Bug.

Eine gewisse Ordnung, und trotzdem Chaos
Alles hat im Grunde eine ordentliche Struktur, aber für mich ergibt sich ein Gefühl von Chaos, dass ich auf Dauer nicht zu bändigen vermag. Ursache dessen ist gerade diese Isolation der Objekte! Aus einem Objekt heraus kann ich nicht einfach auf den Programmfluss schließen. Und weil die Objekte äußerlich von nix abhängig sind (fehlende Konstruktorargumente), weiß ich ja nicht mal, ob diese Objekte überhaupt benötigt werden. Ist ein bestimmtes Objekt für einen bestimmten Vorgang unerlässlich? Vielleicht ja? Vielleicht nein? Welche Events von dem Objekt müssen verdrahtet werden, damit die Software funktioniert? Welche davon sind optional?
All das sagt das Objekt, von außen betrachtet, nicht aus.
Ich sehe mir bei einem Softwareprojekt nicht zuerst an, wie die Objekte verdrahtet sind. Der naive Ansatz, den ich immer verfolge, wenn ich wieder mal wissen will, wie etwas funktioniert, ist Folgender:

Im Grunde haben alle meine Anwendungen ein quasi statisches Hauptformular. Dieses nehme ich mir her und im Entwurfsfenster sehe ich es dann mit all seinen Buttons usw. Wenn ich wissen will, welche Funktion sich hinter einem Button verbirgt, klicke ich doppelt drauf und springe in die entsprechende Routine. Bei der MPT beginnt dann jenes Szenario, welches ich oben dargestellt hatte.
Und DAS macht keinen Spaß! Das Formular ist zwar dadurch wunderbar entkoppelt, aber für sich gesehen ist es absolut sinnlos, weil es nix über die Funktionalität der Software aussagt. Nun kann man sich darüber streiten, ob man so beginnen sollte, eine Software zu lesen. Aber, hey! Eine User-Story beginnt schließlich so: "Wenn ich auf den Button klicke, dann ...!" Und dann beginnt die Business-Logik zu fließen. Aber bei der MPT fließt erstmal nix, sondern man kommt gerade an dieser Stelle zum Stillstand, wo es interessant wird. In meinen früheren Projekten ist an der Stelle tatsächlich die Story geflossen. (Auch wenn da in der Anfängerphase(!) eine furchtbare Struktur zum Vorschein kam und die Domänen häufig gemischt waren, wirkte diese weit weniger chaotisch, als die MPT! Denn man konnte wenigstens hintereinander weg dem Programmfluss folgen! Und das ist absolut wichtig, wenn man Software verstehen will!)

Zurück zu den Wurzeln
Eine totale(!) Entkopplung, also Isolation von Objekten, mag ja auch ihre Vorteile haben. Aber mir hat sie in der Praxis das Verstehen meiner eigenen Software nur erschwert. Denn wie gesagt: Ich gucke in erster Linie nicht auf den "Schaltplan", also danach, wie die Objekte miteinander verknüpft sind. Man hat schließlich immer noch Quellcode vor sich, den man wie eine Geschichte lesen können soll.
Das geht aber NICHT, wenn man hier nur isolierte Klassen vor sich hat.
So konnte das nicht weiter gehen! Immer dieses "von Sackgasse zu Sackgasse" springen machte absolut keinen Spaß!
Was würde also dagegen sprechen, Objekten wieder die gewohnte Abhängigkeit zu verleihen? Sie also miteinander lose(!) zu koppeln? Nix würde dagegen sprechen! Die lose Kopplung würde einen aber wieder zu einem lesbaren Programm verhelfen! Und das ist mir wichtiger, als auf biegen und brechen meine Klassen total zu entkoppeln. Das nützt mir nix, wenn ich die Software am Ende nicht vernünftig lesen kann. Schlimmer noch, wenn sich der Lesefluss so anfühlt, als ob ich von einer "goto-Anweisung" zur nächsten "goto-Anweisung" springe. Man nennt das "Spaghetti-Code". Genauso fühlt sich aber auch das Nachvollziehen des MPT-Codes an!

Dependency Injection neu entdeckt: DIY-DI
Dependency Injection (DI) ist mir nicht unbekannt. Durch Misko Hevery, einem regelrechten DI Enthusiasten, hatte ich die Thematik so richtig vertieft. Warum hatte ich aber das Prinzip der DI nicht weiter verfolgt? Im Grunde hatte mich die manuelle Objekterzeugung abgeschreckt. Sowas kann dann schon mal unübersichtlich und komplex werden. Natürlich gibt es auch DI-Frameworks, welche einem die Arbeit abnehmen sollen.
Wenn ich aber eines hasse, dann sind es irgendwelche Frameworks, von denen man sich wieder abhängig macht. Frameworks, wo im Hintergrund irgendwas "Magisches" passiert, kann ich nicht leiden. Ich möchte in meinem Quellcode jede Kleinigkeit nachvollziehen können. Dass ich da beim .Net-Framework mit einem relativ hohen Abstraktionsgrad leben muss, ist mir da schon genug "Magie". Da brauche ich nicht noch irgendein mysteriöses Framework, wo ich nicht weiß, was da im Detail passiert.

Nun bin ich da aber vor kurzem bei Misko Heverys Blog auf einen Artikel gestoßen, der in mir wieder das Interesse für DI geweckt hat: Do it Yourself – Dependency Injection (DIY-DI). (Der eigentliche "Erfinder" dieser Idee ist aber Chad Parry und sein originaler Beitrag befindet sich dort.)
Als ich mir die Präsentation und das PDF dazu durchgelesen hatte, war mir schlagartig klar, dass ich meine Software zukünftig SO und nicht anders entwickeln möchte! Das ist genau das, was ich unter sauberen Quellcode verstehe.
DI ist im objektorientierten Bereich nun mal ein probates Mittel, um Ordnung in seine Struktur zu bekommen. Wenn ein Objekt auf andere Objekte angewiesen ist, damit es funktionieren kann, sollte es dies doch äußerlich kenntlich machen. Das gibt eine gewisse Transparenz und zugleich eine enorme Sicherheit für den Programmierer. Man sieht sofort, mit welchen Objekten das Zielobjekt unmittelbar interagiert und man weiß, dass das Objekt voll funktionsfähig ist, wenn es die geforderten Objekte injiziert bekommt. (Zumindest sollte das so sein. Stichwort: "Law of Demeter")

So habe ich auf Arbeit damit begonnen, mein derzeitiges Projekt nach DIY-DI zu migrieren. Und mir bereitet es erstmals wieder Freude, Funktionalität hinzuzufügen.
So begann meine Überlegung: "Das Business-Model benötigt kein User Interface, um zu funktionieren. Aber ein User Interface benötigt ein Business-Model, um zu funktionieren."
Das erscheint auch logisch. Spinnt man das noch eine Ebene weiter, so benötige ich als Benutzer das User Interface, um die Software bedienen zu können! Das ist Fakt!
Ich, als Mensch, bin ja auch nicht isoliert von meiner Umwelt. Die Umwelt ist zwar nicht abhängig von mir, aber ich von ihr. So brauche ich z.B. die Luft zum Atmen, usw.

Um zur Software zurück zu kommen, injizierte ich also einen "MainPresenter" in meine "MainForm". Und über den "MainPresenter" fordere ich nur die Dinge an, die für das User Interface von Belang sind. Der "MainPresenter" ist von diversen Service-Objekten abhängig, die er benötigt, um die Anforderungen vom User Interface erfüllen zu können.
So hatte ich schon einige Funktionalitäten von der MPT-Architektur umgestellt. Alles ist auf einmal so einfach nachvollziehbar geworden. Der Aufruf, "Gehe zu Definition" in Visual Studio, ist mir endlich wieder von Nutzen, weil ich mit dessen Hilfe wirklich hintereinander weg die komplette Aufrufhierarchie nachvollziehen kann.

Das soll es erstmal soweit gewesen sein. Die totale Entkopplung ist für mich einfach nicht mehr attraktiv.

Keine Kommentare:

Kommentar veröffentlichen