Autor Thema: Gang Of Four (GoF) Design Patterns für LotusScript on OO nachprogrammieren?  (Gelesen 34473 mal)

Marinero Atlántico

  • Gast
Mit der großen roten 1 im Bild gekennzeichnet fällt auf, dass Duck:New 3 mal aufgerufen wird. RedHeadDuck.New und MallardDuck.New() werden aber nur 1 mal aufgerufen. Komisch. Komisch. 
Wer sich das nicht direkt erklären kann, schaut sich den Ablauf des codes am besten direkt mal im Debugger an.
Direkt bei den ersten steps!
Und was passiert dort im Debugger?
Richtig. Wir rufen.
Code
Set myRHDuck = New RedHeadDuck()
auf und es wird erst der Konstruktor von Duck aufgerufen und dann erst der Konstruktor von RedHeadDuck.
Das gleiche Phänomen ist bei dem Aufruf von MallardDuck zu beobachten.
Deshalb gibt es im Logger 3 Einträge von Duck.New pro Durchlauf. Schliesslich wird er – wie zu erwarten – noch mal bei new Duck() aufgerufen (letztes Code-Segment im Agenten.Initialize).

Die Methode new ist, wie bereits erwähnt, eine spezielle Methode. Ein Konstruktor, um genau zu sein. Konstruktoren von Klassen in einer Vererbungshierarchie werden also scheinbar bei der Objektintiierung in einer Art Hierarchie aufgerufen.
Und zwar als erstes der Konstruktor der oberste Hierarchie und zum Schluss der unterste in der Vererbungshierarchie (der initiierte Typ).
Konstruktoren dienen der Initiierung von Instanzvariablen. MallardDuck is-a Duck und deshalb muss auch der Konstruktor von Duck aufgerufen werden (oberste in der Hierarchie) und dann RedHeadDuck (eigentlicher Typ der erzeugt wird und erbt direkt von Duck).
Würde Ducks eine Oberklasse Birds haben und RedheadDuck von Duck erben, würde zunächst der Konstruktor von Birds aufgerufen, dann Duck und dann RedHeadDuck.   

Eine Schwäche von LotusScript OO gegenüber anderen OO-Sprachen ist, dass die Konstruktoren von Klassen in einer Vererbungshierarchie die gleichen Parameter übergeben werden m.ü.s.s.e.n. Bei dem Entenbeispiel fällt das nicht weiter auf, weil alle new Methoden (Konstruktoren) keine Parameter haben.
Sobald aber z.B. das new von MallardDuck ein Parameter übergeben wird (z.B.
Code
New (name as String) 
muss auch Duck einen Konstruktor mit einem String-Parameter haben.
Den Konstruktor auf keine Parameter belassen würde dazu führen, dass die Klasse nicht kompiliert mit einer entsprechenden aussagekräftigen Fehlermeldung im Designer.

In Java ist man da ein bischen freier.

Als erste Zeile im Konstruktor kann man das Schlüsselwort super benutzen, um dem unmittelbar aufgerufenen Parent-Konstruktor andere Parameter zu übergeben (In Java heissen Konstruktoren nicht new sondern haben immer den Namen der Klasse. Der Konstruktor von Duck heisst also in Java nicht new sondern Duck. Das ist aber nur syntaktischer Zucker, heisst: unwichtig:

Code
class Duck {
   public Duck () {}
}


class MallardDuck extends Duck {
  public MallardDuck(String name) {
    super();
 }
}
Dieses super muss aber in der ersten Zeile des Konstruktors der Kind-Klasse stehen.
Sieht aus wie ein normaler Aufruf, muss aber an erster Stelle stehen. Das kann Anfänger verwirren. 

C#.NET ist vielleicht in seiner erst mal merkwürdig aussehenden Syntax ein bischen deutlicher: (Vorsicht ungetester Code)
Code
class Duck {
 Duck () {}
}

public class MallardDuck : Duck {  
// der Doppelpunkt steht für Vererbung wie „extends“ in Java oder „as“ in LotusScript
 public MallardDuck (String name): base() {

 }
}
: base() steht für die Parameter-Übergabe an den  Parent-Constructor.

Wir sehen, dass die is-a Beziehung (Vererbung) eine sehr enge Kopplung von 2 Klassen verursacht. Tatsächlich ist RedHeadDuck eine Duck, weil eben der Konstruktor von Duck aufgerufen wird, wenn man RedHeadDuck erzeugt.

Was ist eigentlich, wenn man keine Lust hat oder vergisst einen Konstruktor zu schreiben?
Dann erzeugt der Compiler in LotusScript, Java und .NET einen leeren Default-Konstruktor ohne Parameter. Ihr könnt spasseshalber den new() Konstruktor von MallardDuck ausremen (mit %rem %end rem) und sehen was passiert, wenn ihr dann den Agenten laufen lässt. Dort ist auf jeden Fall ein Konstruktor. Dieser Compiler-generierte Konstruktor sieht so aus:
Code
Public sub new

End sub
Er steht nur nicht im code, wohl aber in der kompilierten Klasse.
Schwerwiegendere Auswirgungen hat das ausremen des Konstruktors von Duck. Die Klassen kompilieren zwar, dafür terminiert aber eine Runtime Exception „Object Variable Not Set“ den Programmablauf. Das ist so, weil der Konstruktor von Duck die wichtige Aufgabe hat, die Variable myLogger zu initialisieren. Remt man den vorhandenen Konstruktor aus, wo diese Variable inititalisiert wird und benutzt statt dessen den Default-Konstruktor, wo diese Variable nicht initialisiert wird, weil da keine Anweisungen wie die wichtige
Code
		Set myLogger = New Logger()
fehlt, ist diese Instanzvariable im weiteren Programmverlauf nicht gesetzt und es passiert in der Zeile
Code
Call myLogger.setIdentifier("DecoyDucksDontFly_Start::RedHeadDuck")
genau diese „Object Variable not Exception“.


Kommen wir zum nächsten Punkt. Das heisst Moment.
Warum stehen eigentlich in der Log-Datenbank die beiden Duck:new(), die über den Aufruf new MallardDuck() bzw. new RedHeadDuck aufgerufen worden sind unter der Sektion:
DecoyDucksDontFly_Start::Duck
und nicht unter DecoyDucksDontFly_Start::MallardDuck bzw. DecoyDucksDontFly_Start::RedHeadDuck?
Das ist nicht gut.
Nochmal. Was passiert genau bei dem Aufruf von new RedHeadDuck?
1.   RedHeadDuck.new() wird aufgerufen.
2.   Notes sieht, dass diese Klasse eine Superklasse Duck hat.
3.   Notes leitet den Aufruf auf Duck.new() weiter
4.   Im new() von Duck wird ein neues Logger-Objekt wird initialisiert und der Instanzvariable myLogger der Klasse Duck wird dieses Logger Objekt zugewiesen Set myLogger = New Logger()
5.   Die Methode .setIdentifier der Klasse Logger wird mit dem Parameter "DecoyDucksDontFly_Start::Duck" aufgerufen.
6.   In der Methode setIdentifier von Logger wird die Eigenschaft „Programname“ unseres NotesLog-Objekts geändert. Diese manipuliert die „Sektion“ in der Ansicht der Log-Datenbank (nähere Details zu Programname s. Noteshilfe)
7.   Es wird die Methode log("Info:Duck::New terminates") von Logger aufgerufen. Die Sektion steht auf "DecoyDucksDontFly_Start::Duck".
8.   Der Konstruktor new() der Superklasse Duck ist abgearbeitet und nun wird der Konstruktor der eigentlichen Klasse abgearbeitet (RedHeadDuck.new().
9.   In dem Konstruktor new() von RedHeadDuck wird wieder die Methode setIdentifier der Klasse Logger aufgerufen. Nun wird dieser Methode aber ein anderer Parameter übergeben. ("DecoyDucksDontFly_Start::RedHeadDuck").
10.   Wieder wird die Eigenschaft Programname unseres NotesLog-Objekts geändert in myLogger.setIdentifier. Der übergebene Parameter heisst aber nun "DecoyDucksDontFly_Start::RedHeadDuck" und nicht wie im Konstruktor von Duck "DecoyDucksDontFly_Start::Duck".
11.   Die Methode log von myLogger wird mit dem Parameter ("Info:RedHeadDuck.New called" aufgerufen. Das steht nun aber unter der neuen Sektion (Auswirkung von Punkt 10).

Mir gefällt das Verhalten nicht, dass die Logging-Section sich während eines new Aufrufs aus initialize (dem Client code) ändert. Welcher poofy wanker hat das programmiert? Ich weiss, dass auch ich einmal angefangen habe mit Object Orientation, aber muss man so code veröffentlichen? Ich werde das bald so umschreiben, dass das Logger Objekt im Agenten erzeugt wird und den Konstruktoren der Klasse als Parameter übergeben wird.
Oder irgendwie anders. Jedenfalls ist das eine ganz gute Diskussionsgrundlage für OO-Designpraktiken. 
Das initialisieren von myLogger im Konstruktoren in der Basis-Klasse war keine gute Idee!
To strong a coupling. Leider hat Igor z.Zt. wegen IBM.486 Vorbereitung meinen Larman, aber ich bin mir ziemlich sicher, dass sich dort klarere theoretische Fundierungen finden, warum das jetzt dazu führte, dass die Klassen redesigned werden müssen.
Code
« Letzte Änderung: 11.01.05 - 10:51:45 von Marinero Atlántico »

Marinero Atlántico

  • Gast
Themenwechsel: Ein bischen seltsam mag vielleicht auch erscheinen, warum in der Klasse RedHeadDuck überhaupt Methoden der Eigenschaft myLogger aufgerufen werden können. Schliesslich hat diese Klasse überhaupt keine Eigenschaft myLogger.  Nur die SuperKlasse hat eine solche Eigenschaft:
Code
Private myLogger As Logger
Offenbar kann aber die Kind-Klasse RedHeadDuck diese Eigenschaft der Elternklasse Duck mitbenutzen. Genau das ist ein Aspekt der Vererbung. Kindklassen tragen die Eigenschaften und Methoden ihrer Elternklasse in sich. Oft zumindest.
Mich hat das an LotusScript erst ein bischen verwirrt. In Java werden Eigenschaften mit dem Zugriffs-Modifier private nicht an child-Klassen vererbt.

In Java sieht es so aus:
Code
public class Parent {
	Parent() {
		System.out.println("****In Parent Konstruktor *******");
		System.out.println(publicMember);
		System.out.println(protectedMember);
		System.out.println(packagePrivateMember);
		System.out.println(privMember); // hier kann im Gegensatz von Child auf privMember zugegriffen werden!!!
		
		 

	}
	// nur innerhalb der Klasse sichtbar. Von Child aus kein Zugriff. 
	private String privMember = "privMember ist da.";
	// vor der nächsten Eigenschaft oder Membervariable steht kein Access-Modifier
	// aber auch der leere Access Modifier setzt bestimmte Regeln. 
	// Solche Variablen sind nur innerhalb des Pakets sichtbar. 
	// wir haben hier oben in dieser Klasse kein package Statement.
	// Deshalb landet die Klasse im default package. Ausserhalb von solchen 
	// mini-Demo Anwendungen schreibe ich keine Klassen ohne package. 
	String packagePrivateMember = "packagePrivateMember ist da.";
	// protected: wie packagePrivate (also sichtbar innerhalb des gleichen Pakets) und zusätzlich
	// alle Klassen, die von dieser Klasse erben und in einem anderen Paket sind. 
	protected String protectedMember = "protectedMember ist da.";
	// public: Vollzugriff. Aus allen Klassen kann auf diese Instanzvariable zugegriffen werden. 
	public String publicMember = "publicMember ist da.";
}

public class Child extends Parent {

	Child() {
		System.out.println("****In Child Konstruktor *******");
		System.out.println(publicMember);
		System.out.println(protectedMember);
		System.out.println(packagePrivateMember);
		//System.out.println(privMember); 
		//Lässt compiler nicht zu. 
		//Auf private members kann nur in der Klasse selbst zugegriffen werden. 
	}
	public static void main(String[] args) {
		new Child();
	}
}

Zwar kennen sowohl Java als auch LS Zugriffs-Modifier, jedoch heisst private in beiden Sprachen offenbar etwas leicht anderes. Das ist völlig normal. Auch bei C++/C# und Java gibt es jeweils leicht andere Zugriffs-Modifier. Die grundsätzliche Idee von private ist aber überall gleich. Es ist der Zugriffs-Modifier mit der höchsten Kappselungsstufe. Also der, der am restriktivsten Zugriffe von aussen einschränkt.
Wenn aber nun private Eigenschaften in Lotus-Script fröhlich von anderen Klassen aufgerufen werden können, vielleicht haben die LotusScript Programmierer einfach vergessen oder keine Zeit mehr gehabt das zu programmieren.
Doch es gibt einen Schutz.
Wenn ich am Ende des codes in Initialize des Agenten die Zeile:
Code
myShouldNotBeInitializedDuck.myLogger
Erhalte ich die Meldung: Not a public member MyLogger.
Auf private Member habe ich also aus anderen Modulen keinen Zugriff.
Wenn ich die Doku richtig verstanden habe, dann heisst private in LotusScript:
private für das gleiche Modul. Ich werde das aber noch genauer prüfen.
Wenn die Klassen in derselben Declaration-Section stehen können sie gegenseitig auf private Members zugreifen. Sobald sie in anderen Declaration Sections stehen oder der aufrufende Code im Initialize gibt es keinen Zugriff (ungeprüfte Hypothese, mein Verständnis der doku).

Auf jeden Fall bleibt die Tatsache, dass der code ausserhalb des Moduls, nämlich im Initialize des Agenten etwas anderes sieht als der code innerhalb der Klasse.
Genau dieses Phänomen ist der Kern von Kappselung. Aus OO-Sicht bezeichnet man alle Methdoden und Eigenschaften eines Objekts, auf die im jeweiligen Kontext zugegriffen werden kann als Interface. Dieses Interface aus OO-Sicht hat nur über 2 Ecken etwas mit dem Java-Schlüsselwort „Interface“ zu tun. Insgesamt ist es ein Synonym (je nach Kontext eine unterschiedliche Bedeutung). Auch LotusScript Klassen haben ein Interface. Sogar – wie alle OO-Klassen – direkt mehrere. Nämlich je nach Kontext. Für Aufrufe innerhalb der Klasse oder des Moduls gibt es ein private-Interface (Methoden und Eigenschaften mit public oder private Modifier) und ein public Interface (für Zugriffe von aussen, etwa aus dem Initialize eines Agenten wie im Beispiel). Dieses public Interface besteht nur aus den Methoden und Eigenschaften mit public modifier. Die mit private sind verborgen (hidden).
Dies hat extrem wichtige Auswirkungen, die noch später näher erläutert werden sollen.

Frage: Es gibt aber auch Eigenschaften und Methoden ohne public oder private Zugriffs Modifier. Hat das vielleicht in LotusScript auch eine spezielle Bedeutung wie in Java?
Nein. Hat es nicht. Der Compiler fügt den Eigenschaften ohne Zugriffs-Modifier automatisch ein private hinzu und den Methoden automatisch ein public.

Frage: Warum ist das für beide verschieden? Wäre es nicht einfacher, wenn sowohl Methoden als auch Eigenschaften public wären oder private. Zumindest alle einheitlich?
Eigentlich nicht. Nach normaler OO-Praxis sind Methoden eher, aber nicht immer public und Eigenschaften eher, aber nicht immer private.
Das ist mehr als eine Konvention. Aber das versuche ich später zu erklären. Da muss erst mal ein bischen über public und private Interfaces geredet werden.

Als nächstes wird
-   das Redesign durchgeführt (kann wegen mir auch wer anders machen, ist aber egal)
-   sich mal angeschaut was es mit dieser merkwürdigen delete() Methode auf sich hat, die es zwar in der Klasse gibt und gemäss dem Ergebnis in der Log-DB irgendwie aufgerufen wird. Wer sich den code genau anschaut, sieht das im code diese Aufrufe nicht stattfinden.
-   Der Frage nachgegangen, dass zwar der Konstruktor von RedHeadDuck den parent-Constructor aufruft, die in Duck definierte Methode swim() aus RedHeadDuck aufgerufen wird. Bei der Methode display(), die es sowohl in den Eltern als auch in den Kind Klassen gibt, aber nur die display() Methode des jeweiligen Objekts aufgerufen wird (beim Konstruktor werden ja beide aufgerufen). Alles ein bischen verwirrend, vielleicht.

Offline Semeaphoros

  • Gold Platin u.s.w. member:)
  • *****
  • Beiträge: 8.152
  • Geschlecht: Männlich
  • ho semeaphoros - agr.: der Notesträger
    • LIGONET GmbH
Axel, Du hast das mit Private schon richtig interpretiert, hat aber nix mit OO zu tun, sondern "übergeordnet" mit LS: die kleinste Scope-Einheit ist gemäss LS-Definition das Modul und nicht die Class oder whatever in einem Modul sonst noch granularer sein könnte .... irgendwo hatte ich das mal sogar ausdrücklich gelesen, weiss nur nicht mehr wo (wahrscheinlich im LS-Programmer Guide)
Jens-B. Augustiny

Beratung und Unterstützung für Notes und Domino Infrastruktur und Anwendungen

Homepage: http://www.ligonet.ch

IBM Certified Advanced Application Developer - Lotus Notes and Domino 7 und 6
IBM Certified Advanced System Administrator - Lotus Notes and Domino 7 und 6

Offline Mark³

  • Senior Mitglied
  • ****
  • Beiträge: 386
  • Geschlecht: Männlich
  • Nordisch by Nature
    • Das Leben aus der Sicht eines Menschen

-   Der Frage nachgegangen, dass zwar der Konstruktor von RedHeadDuck den parent-Constructor aufruft, die in Duck definierte Methode swim() aus RedHeadDuck aufgerufen wird. Bei der Methode display(), die es sowohl in den Eltern als auch in den Kind Klassen gibt, aber nur die display() Methode des jeweiligen Objekts aufgerufen wird (beim Konstruktor werden ja beide aufgerufen). Alles ein bischen verwirrend, vielleicht.


um mir das Gefühl des Mitmachens zu geben: Die display() Methode wird nur vom jeweiligen Objekt aufgerufen, weil sie überladen ist (geht in LS hier, weil alle display() die gleichen Parameter erwarten, nämlich keinen) . Wobei der Sinn der Überladung darin liegt dass man   mit dem gleichen Interface sowohl Eltern-Objekte als auch Kind-Objekte ansprechen kann  ::)    ???
sagt Mark.



slowfood.de

Marinero Atlántico

  • Gast
Axel, Du hast das mit Private schon richtig interpretiert, hat aber nix mit OO zu tun, sondern "übergeordnet" mit LS: die kleinste Scope-Einheit ist gemäss LS-Definition das Modul und nicht die Class oder whatever in einem Modul sonst noch granularer sein könnte .... irgendwo hatte ich das mal sogar ausdrücklich gelesen, weiss nur nicht mehr wo (wahrscheinlich im LS-Programmer Guide)

So etwas ähnliches schwirrte in meinem Kopf auch rum.
Und ja sicher: Private gibt es auch für Funktionen und Subs.
Ich finds nicht so schlimm. Das echte Problem für "modernes"-GoF OO sind die fehlenden abstrakten Klassen und das fehlende static.


Marinero Atlántico

  • Gast
um mir das Gefühl des Mitmachens zu geben: Die display() Methode wird nur vom jeweiligen Objekt aufgerufen, weil sie überladen ist (geht in LS hier, weil alle display() die gleichen Parameter erwarten, nämlich keinen) .
Typisch Microsoft-Programmierer  ;D(Scherz. Ich hab nix gegen Microsoft). s. dein codestore Kommentar zum neuesten Eintrag.  ;D
Du verwechselst gerade überschreiben mit überladen.
overwriting: beide Klassen haben eine display Methode mit gleicher Parameterliste. Man spricht hier auch von gleicher "Signatur" der Methode. Wenn ich mich recht erinnere gehört der Rückgabetyp zur Signatur. .
Overwriting funktioniert in LotusScript wie in Java oder C#.

overloading (überladen) ist, wenn 2 Methoden in einer Klasse den gleichen Namen aber unterschiedliche Parameterlisten haben. Das geht in LotusScript nicht. 
zb:
public display()
und
public display (name As String)
Overwriting hat ziemlich viel mit OO zu tun. Overloading gab es aber schon stark in prozeduralen Sprachen. In LotusScript aber nicht in den UserDefined Funktionen. In den von Lotus zur Verfügung gestellten Funktionen und Routinen gibt es - wie Jens oder Bernhard sehr richtig angemerkt hat - Funktionen/Routinen mit unterschiedlichen Parameterlisten, also overloading.
z.B. mir fällt dazu ein, dass messagebox sowohl:
messagebox "a message"
als auch:
messagebox "a message", intNumber, "aTitle"
aufgerufen werden kann.
Variierende Parameterlisten für Methoden mit gleichen Namen geht nicht für eigene Funktionen.

 
Hab mal so prep Fragen zu c# certi gesehen. Da wird auf diesen Begriffen (ähnlich wie bei Java) ziemlich drauf rumgeritten. Es gibt aber nur die 2: overloading und overwriting.   
Meine deutsche Begrifflichkeit ist aber auch manchmal nicht so toll.
Wobei der Sinn der Überladung darin liegt dass man   mit dem gleichen Interface sowohl Eltern-Objekte als auch Kind-Objekte ansprechen kann  ::)    ???
Im Prinzip ist das richtig (überschreiben nicht überladen). Das geht in Richtung Interface-Inheritance statt code Inheritance, Polymorphismus und Late Binding. Begriffe, die ich noch Versuche klar zu machen. Euer Autor Jesse Liberty kann das übrigens ziemlich gut darstellen.  8)

Gruß Axel
« Letzte Änderung: 11.01.05 - 20:16:18 von Marinero Atlántico »

Offline Semeaphoros

  • Gold Platin u.s.w. member:)
  • *****
  • Beiträge: 8.152
  • Geschlecht: Männlich
  • ho semeaphoros - agr.: der Notesträger
    • LIGONET GmbH
Private gibt es auch für Funktionen und Subs.
Ich finds nicht so schlimm. Das echte Problem für "modernes"-GoF OO sind die fehlenden abstrakten Klassen und das fehlende static.

Genau, daher, dass es Private und Public auch im sonstigen LS gibt, kommt diese Scope-"Problematik" auch zustande, und einverstanden, das ist nicht wirklich tragisch, da muss man einfach dran denken. Fehlende abstrakte Definitionen und fehlendes Static (bzw. eingeschränktes) ist wirklich eine echte Einschränkung.
Jens-B. Augustiny

Beratung und Unterstützung für Notes und Domino Infrastruktur und Anwendungen

Homepage: http://www.ligonet.ch

IBM Certified Advanced Application Developer - Lotus Notes and Domino 7 und 6
IBM Certified Advanced System Administrator - Lotus Notes and Domino 7 und 6

Marinero Atlántico

  • Gast
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.

Code
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:

Code
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:
Code
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.
Code
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:
Code
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
Code
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.
« Letzte Änderung: 20.01.05 - 19:18:36 von Marinero Atlántico »

 

Impressum Atnotes.de  -  Powered by Syslords Solutions  -  Datenschutz