Falls das jemand nachvollziehen will: Unten ist eine nsf mit Agenten angehängt. Bitte den Agenten in die schon bestehende Datenbank kopieren und den laufen lassen. Am besten auch im Debugger.
So was ist nicht so gut mit dem jetzigen Code?
1. Jede Testklasse erzeugt ihr EIGENES Logger-Objekt, dass wiederum mehrere kostpielige Notes-Operationen durchführt, die eigentlich nur einmal nötig sind. Lägen die Notesdatenbanken auf dem Server gingen sie über das Netzwerk (noch kostspieliger). Im Prinzip ist das egal.
Performance? Egal? Ach du heilige Kaffeetasse.
Dieser Code ist ausreichend schnell, für was er tut.
Zu frühes Optimieren kann schnell zu unübersichtlichen Code führen.
Wobei man meiner Meinung nach schon ein bischen über Performance nachdenken sollte.
Oft verursachen aber nur kleine Stellen im Code wirklich Performance-Probleme. Um diese aufzuspüren helfen sogenannte Profiler.
Würden wir die Datenbank auf einen Server legen wäre die Initialisierung eines Loggers schon kostspieliger, weil die Lookups gegen das Konfigurationsdokument Remote Calls sind.
2. Ausserdem und schlimmer haben wir bei der Vererbung das Problem, dass die Superklasse ihr eigenes setIdentifier aufruft, so dass Log-Nachrichten von 1 call durch den Client in 2 Kategorien landen (Duck und MallardDuck bzw. RedHeadDuck).
Wir müssen das irgendwie umschreiben.
Leider hat Igor wie gesagt meinen Larman, d.h. ich muss das später noch mal überarbeiten.
Aber im Grunde sehe ich hier ein echtes Problem von HighCoupling. Wir wissen schon, dass Klassen kohäsiv sein soll, d.h. sie sollen 1 Thema aus der realen Welt behandeln.
Deshalb haben wir 1 Klasse für das Konfigurationsdokument, 1 für den Logger und jeweils eine für jeden Typ Ente und die Superklasse, die lieber abstrakt wäre, aber IBM bastelt lieber an Workplace rum.
Das war cohesion. Jetzt gibt es noch coupling. Es heisst high cohesion. Low coupling. Coupling heisst die Abhängigkeit zwischen den Klassen. Natürlich muss es da Abhängigkeiten geben, weil die Objekte ja irgendwie zusammenarbeiten sollen. Nur sollte jede einen gewissen Freiraum haben. Möglichst viel sogar. Damit sie unabhängig von den anderen eingesetzt werden kann und ausserdem haben wir eben durch den doppelten-setIdentifier-in-den Konstruktoren einen ärgerlichen Seiteneffekt von Vererbung.
Es gibt verschiedene Stufen von Kopplung und Larman hat das gut beschrieben.
Die höchste Stufe ist, wenn 1 Objekt Creator eines anderen Objekts ist. Duck creates Logger, weil es new Logger() aufruft.
Was können wir machen?
Wir können Logger ausserhalb von Duck erstellen und diesen Logger dann jeder Ente übergeben. Dann können wir den Logger auch wiederverwenden. Der Logger ist unabhängiger von der Ente.
Ist das umsonst?
Nein.
Der Client-Code wird ein wenig komplizierter. Client code ist das, was im Initialize des Agenten steht. Die Enten sind also ein bischen schwerer zu bedienen.
Rem gibt’s auch im beigefügten .nsf
Dim theLogger As Logger
' client muss Logger erstellen.
Set theLogger = New Logger()
Dim myRHDuck As RedHeadDuck
Dim myMDuck As MallardDuck
Dim myShouldNotBeInitializedDuck As Duck
'Logger setzt pro Objekt eine neue Kategorie
Call theLogger.setIdentifier("DecoyDucksDontFly_StartBetter::RedHeadDuck")
' Operationen mit RedHeadDuck
Set myRHDuck = New RedHeadDuck(theLogger)
Call myRHDuck.swim()
Call myRHDuck.quack()
Call myRHDuck.display()
‘das gleiche Logger-Objekt manipulieren, um es für die MallardDuck zu benutzen
Call theLogger.setIdentifier("DecoyDucksDontFly_StartBetter::MallardDuck")
' Operationen mit RedHeadDuck
Set myMDuck = New MallardDuck(theLogger)
Call myMDuck.swim()
Call myMDuck.quack()
Call myMDuck.display()
‘das gleiche Logger-Objekt manipulieren, um es für die Duck zu benutzen
Call theLogger.setIdentifier("DecoyDucksDontFly_StartBetter::DuckBetter")
Set myShouldNotBeInitializedDuck = New Duck(theLogger)
Call myShouldNotBeInitializedDuck.swim()
Call myShouldNotBeInitializedDuck.quack()
Call myShouldNotBeInitializedDuck.display()
'PASS BY REFERENCE!!!
‘ VORSICHT: WILD
theLogger.setIdentifier("DecoyDucksDontFly_StartBetter::BigDuckMassacer")
Logger ist wie gesagt deutlich unabhängiger von den Enten, wenn wir das Objekt dem Konstruktor übergeben und es nicht mehr im Konstruktor erzeugen!
Lasst das erst mal laufen und schaut euch das Ergebnis in der Log-DB an.
Interessant ist v.a. die letzte Zeile und die Auswirkungen in der Log-DB:
theLogger.setIdentifier("DecoyDucksDontFly_StartBetter::BigDuckMassacer")
Der Speicher der Objekte wird dann freigegeben, wenn der Agent endet. Kurz bevor der Speicher freigegeben wird, ruft ein LotusScript automatisch den Destruktor delete() auf. Die delete-Methoden schreiben ins Log. z.B:
Call myLogger.log("Info:MallardDuck::Delete starts")
Das Logger Objekt wird wie bei LotusScript defaultmässig üblich by-reference übergeben. Wir stellen das vor Ende des Agentenlaufs in der letzten Zeile um. Xtreme Geek fun und DAS IST ÜBERHAUTPT KEIN DESIGNPATTERN!
So landen aber alle LogNachrichten rund um das Thema Destruktor in einer eigenen Sektion „BigDuckMassacer“. Das ist der Effekt.
Ich bin mir nicht sicher, ob das hier jeder versteht, warum das so ist..
Warum ist das so?
Ich übergebe im initialize des Agenten EIN WIEDERVERWENDETES Objekt mit Namen theLogger an drei DuckKlassen, das heisst als Parameter im Konstruktor wird das Objekt erst mal aLogger und wird dann dem Member myLogger zugewiesen.
AUFRUF:
Set myMDuck = New MallardDuck(theLogger)
OBJEKT-KONSTRUKTOR::
Public Sub new(aLogger As Logger)
Set myLogger = aLogger
myLogger.log("Info:MallardDuck.New called")
End Sub
Das gleiche mache ich mit dem selben theLogger in Initialize mit allen 3 Enten-Objekten!
Dann verändere ich theLogger und das hat Auswirkungen auf gleich drei, in Zahlen: 3, Instanzvariablen, die völlig anders heissen?
Qué cosa más eficiente.
Aber eben auch unübersichtlich.
Mit prozeduraler Programmierung geht das so nicht, weil ein Skript sequentiell abgearbeitet wird. Ein Objekt bleibt aber im Speicher, bis es zerstört wird (etwa am Ende des Agenten). Der Code im Initialize läuft weiter, aber das Objekt ist noch DA.
SELBSTVERSTÄNDLICH GEHT DAS NICHT. Solche Konstrukte sind Zeitbomben für zukünftig schwer zu findende bugs. Ich werde deshalb diesen Teil noch mal refaktorieren müssen. Jetzt soll den Enten eine eigene KOPIE des Logger Objekts übergeben werden. Am besten ginge das mit pass-by-value, wobei zu prüfen ist, ob das mit Klassen überhaupt geht. Aus Performance Gesichtspunkten sollte das auch ok sein, weil die Initialisierung des Logger-Objekts als die eigentlich kostpielige Operation erscheint (Zugriff auf config-Dokument). Ohne Profiling wissen wir das zwar nicht, ist aber sehr wahrscheinlich. Mit der oben skizzierten Lösung, das Enten Kopien des Logger-Objekts übergeben werden, wäre die Initialisierung des Loggers nach wie vor zentralisiert. D.h. es findet nur ein call gegen Logger.new statt.
NEUER GEDANKE: Objekte haben nicht nur Methoden und Eigenschaften. Nein. Sie haben auch einen Identifier, der auf sie verweist. theLogger ist so ein Identifier oder myMDuck.myLogger.
Die Objekte selbst sind ein Bereich im Adressraum des Speichers deines Rechners. Dort selbst sind eine Menge 1en und 0en. Wenn ich – wie in LotusScript defaultmässig üblich – by Reference übergebe, steht in dem Objekt also genauer:dem_Verweis_auf_die_Speicheradresse_mit_Bezeichner_theLogger_wo_die_ganzen_0en_und_1en_für_das_Logger_Objekt_stehen die, gut, Speicheradresse dieses Objekts.
Diese gleiche Speicheradresse_des_Objekts weise ich nun in den Parametern des Konstruktors der lokalen Variable aLogger zu. Im Konstruktor-Body weise ich dann der Instanzvariable myLogger die gleiche Speicheradresse im_Rechner zu. Eine Menge Variablen zeigen auf die gleiche Speicheradresse.
Wenn ich nun in der letzten Zeile von initialize sage:
theLogger.setIdentifier("DecoyDucksDontFly_StartBetter::BigDuckMassacer")
, dann verändert das die 0en und 1en des Adressbereichs auf den die ganzen Logger Identifier (myRHDuck.myLogger, myRHDuck.myLogger; myShouldNotBeInitialized.myLogger, theLogger) zeigen.
Wenn nun der Agent endet, werden die Objekte von Lotus Notes automatisch aus dem Speicher entfernt. Als Rückrufmethode vor dem entfernen wird bei jedem Objekt automatisch die public sub delete aufgerufen, wo Dinge drinstehen wie
Call myLogger.log("Info:MallardDuck::Delete starts")
myLogger zeigt auf eine bestimmte Speicheradresse. Da sind 0en und 1en. Diese wurden in der letzten Zeile des initialize geändert. Und das hat Auswirkungen.
Das mag am Anfang verwirrend sein. Wer das nicht verstanden hat, bitte melden. Jens oder Bernhard erklären das sicher gerne
Was haben wir getan?
Der Programmablauf macht eigentlich – mit ein paar Änderungen – nach wie vor dasselbe. Ohne die kleinen Änderungen in den sichtbaren Auswirkungen in der LogDB, hätten wir die Königsdisziplin der Javaprogrammierung durchgeführt: Refactoring.
Sichtbar ändert sich nichts, aber das Programm ist besser strukturiert.
Es ist wie gesagt nicht richtig Refactoring, aber ziemlich Refactoring-ähnlich. D.h. der Kern der Arbeit war Refactoring. Wir erzeugen die Instanzvariable myLogger nicht mehr im Konstruktor, sondern wir übergeben den Enten-Konstruktoren ein im initialize erzeugtes und dann von allen gemeinsam genutztes Logger-Objekt. Im Konstruktor wird der Instanzvariable der Enten myLogger nur dieses Logger Objekt zu gewiesen und nicht jedes Mal neu per new erzeugt.
Das ist eine Änderung der internen Struktur des Programms, die lediglich die vorher vorhandenen Nebenwirkungen bereinigt hat.
Refactoring hört sich vielleicht erst mal nicht besonders produktiv an, ist aber höchst produktiv. Wir verwenden glaub ich alle – ich auf jeden Fall – mehr Zeit damit, bestehenden Code zu verstehen als neuen Code hinzuzufügen. Wodurch werden debugging-sessions manchmal lang?
Durch das Ändern des codes oder durch das Auffinden des codes, der geändert werden muss?
Für uns ist das klar: Das Auffinden der Stelle, die den Fehler verursacht. Das Ändern ist meist kein Problem.
Für unsere Kunden ist das leider oft nicht klar.
Wieso ist das so?
Es gibt so was wie Software-Entrophie. Je mehr Funktion einem Programm hinzugefügt wird, desto unübersichtlicher wird es. Tendentiell und Reell. Kein Mensch kann wirklich perfekten Code schreiben. Deshalb ist es sinnvoll, zwischendurch die interne Struktur des Programms auf einen übersichtlichen Stand zu bringen. Das ist die Idee von Refactoring.
Zum Thema Refactoring in OO und v.a. Java hat Martin Fowler ein tolles Buch geschrieben, was irgendein Idiot auf amazon.de als OO-Anfänger-Buch bezeichnet hat. Das sind natürlich sehr kleine Schritte beschrieben und man weiss das eigentlich alles, aber der Typ hat irgendwie die Idee der Systematisierung nicht ganz begriffen. Systematisiertes Refactoring ist ein ziemlich monotoner low-level Prozess. Das Extrahieren eines Konstruktor-Aufrufs (myLogger = new Logger() aus einem anderen Konstruktor (Duck, MallardDuck, RedHeadDuck) nach aussen in den Client-Code und dann Übergabe dieses Objekts an die Konstruktoren der Ducks ist immer gleich und nicht besonders kompliziert. Wir haben das auch in prozeduralen Notesfunktionen 5 bis 80 mal gemacht.
Nein. Die Funktion benötigt einen weiteren Parameter und wir brauchen hier kein neues NotesDatabase-Objekt erzeugen, weil wir das im Initialize schon haben. Wir übergeben es mit dem Konstruktor.
Die Wiederholbarkeit des Prozesses macht Refactorings natürlich zu einem guten Thema für IDEs und tatsächlich gibt es in Eclipse 15 bis 20 per wizzard automatisierte Refactorings, die ich btw zum Teil echt nutze und die wirklich Sinn machen.
Erstaunlicherweise habe ich unter den ca. 50 Refactorings in Fowlers Buch KEIN EINZIGES GEFUNDEN, DAS AUF UNSEREN FALL PASST.
Die Erzeugung eines Objekts nach aussen zu verschieben, gibt es dort scheinbar nicht. Oder ich bin blind, weil ich eine ganze Weile nachgeschaut habe. Dann habe ich in in Kerievskys, Refactoring to Patterns etwas ähnliches gefunden. Sehr interessant. Es macht für Java aus verschiedenen Gründen nicht viel Sinn. Erstmal wäre Ducks in Java sowieso abstract. Zweitens könnte man das über Singletons lösen. Weil wir in Java static haben, können wir regeln, dass von 1 Klasse nur genau 1 Objekt existiert (später dazu mehr). Interessant ist, dass Kerievsky in Refactoring to Patterns ein „Inline Singleton“ hat. Ein Zeichen dafür, dass das Buch auch „Refactoring to and from Patterns“ heissen könnte. Manchmal sollte man Patterns auch vernichten, weil sie das Design komplizierter machen können. „Inline Singleton“ heisst: Wir machen den Singleton code aus der Singleton Klasse raus. Kerievsky konstatiert einen gewissen Hang zur „Singletonitis“ unter Java Programmierern. Er war ein bischen unsicher und hat das explizit mit anderen Experten besprochen und Ward Cunningham und Kent Beck finden das auch mit der Singletonitis. „Singleton pattern usage has grown out of propoertion“. „A singleton is unnecesary when its simpler to pass an object ressource as a reference to the object that needs it (S. 145f., Kerievski, Refactoring to Patterns)”. Unser Refactoring ist natürlich ein bischen anders. Es ist „Convert Creator To Global Object Receiver“ und das erste Marinero-Notes-OO-Refactoring und ein bischen problematisch..
Das Problem ist, dass das im Client erzeugte Objekt ein globales Objekt ist.
Dieser ganze Trick mit dem Ausnutzen von pass-by-reference zeigt, dass es eine gefährliche Geschichte ist, die zu schwer zu debuggenden Fehlern führen kann.
In Notes, wo wir eine sehr strikte Parameterübergabe in Konstruktoren, keine abstrakten Klassen und kein static haben, ist es vielleicht keine schlechte Idee.
Fowler nennt in einem anderen Buch (Patterns of Enterprise Architecture) ein sogenanntes Registry-Objekt als Singleton Ersatz. Darüber habe ich schon nachgedacht. Fowler meint aber zurecht, dass man mit solchen globalen Objekte, die man an andere Objekte ausgibt, sehr sparsam umgehen sollte und ich find er hat Recht, weil das wirklich unübersichtlich werden kann.