Einzelnen Beitrag anzeigen

Der_Unwissende

Registriert seit: 13. Dez 2003
Ort: Berlin
1.756 Beiträge
 
#15

Re: OOP Problem: änderungen werden nicht übernommen

  Alt 26. Dez 2005, 12:44
Ich seh schon, langsam nähern wir uns der OOP, aber es sind glaube ich noch ein paar Dinge nicht ganz klar. Heißt hier weder das du es nicht verstanden noch ich es schlecht erklärt habe oder so, sondern ein paar Dinge wurden (denke ich) einfach noch nicht gesagt und selbst dann ist die OOP schon etwas umfassender als das man mal ebend ein perfektes OO-Programm hinbekommt (falls das je der Fall ist).

Aber du befindest dich (imho) auf einem guten Weg.
Eine wirklich wichtige Sache an der OOP ist es und die ist grundlegender als alle Anderen, zu abstrahieren. Ich glaube du betrachtest deine Probleme noch ein wenig zu konkret. Z.B. die Idee, dass bei einem Add ein Create aufgerufen werden muss, stimmt so nicht ganz.
Du darfst dir immer nur einzelne Objekte anschauen, die kennen die Welt nicht sondern nur sich selbst. Sie kennen ihre Eigenschaften und können auf Kommunikation von Aussen reagieren, mehr nicht. Sie interessieren sich aber auch garnicht für die Welt.
Dein Panel ist auch so ein Objekt. Es kennt erstmal nur die eigenen Eigenschaften. Sagen wir mal (da sind wir uns ja einig), eine Eigenschaft ist, dass ein Panel andere Objekte aufnehmen kann (sie verwalten kann). Das weiß das Panel, das ist auch gut so. Aber wo die anderen Objekte herkommen und was für Objekte auf der Welt leben, dass braucht das Panel doch gar nicht zu wissen, also weiß es das auch nicht.
Dein eigentliches Programm ist wie gesagt die Welt. Da gibt es auch eine höhere Macht (den User), der die Welt beeinflusst und auf diese Beeinflussung reagiert deine Welt. Du definierst mit deinem Programmieren nur, wie sie worauf reagiert.

Also quasi üblich wäre es für ein Panel, dass ein Panel folgendes kann:
  • sich um eigene Eigenschaften (Farbe, Größe, Titel, ...) kümmern
  • Fremde Objekte speichern
  • aber nicht fremde Objekte erzeugen

Also könnte ein Panel folgende Form haben

Delphi-Quellcode:
type
  TDeinPanel
    private
      // alle privaten Eigenschaften / Felder
    protected
      // setter und getter für diese Felder
      // alles zur eigenen Verwaltung
    public
      // Methoden die von anderen angesprochen werden können
      procedure draw; // damit es sich selbst aktualisiert
      procedure add(Obj); // fügt irgendein Objekt ein
      procedure remove(Obj); // löscht irgendeinobjekt
  end;
Natürlich ist dies hier kein echter und vollständiger Code, aber von aussen gesehen sehe ich eigentlich doch nur, dass es ein draw, ein add und ein remove gibt. Es ist die Sache des TPanels was beim Aufruf gemacht wird. Sagen wir mal wir machen es etwas konkreter, wir kreieren eine Liste in Delphi (ohne Pointer, sondern mit Klassen). Ich denke es reicht sich hier auf das Add zu beschränken, daran kann ich schon alles zeigen was ich zeigen möchte (sollte es Namen schon in Delphi geben, ist dies nur Zufall und nichts aus der VCL)

Delphi-Quellcode:
type
  // Ein Interface, legt nur Methoden fest
  // die von aussen zugänglich sind
  IObjectList = Interface
    procedure Add(Obj : TObject);
  end;
  
  // erbt von TInterfaceObject, da Delphi Interfaces 3 Standard-Methoden haben
  // die ich hier nur nicht implementieren möchte
  // Und diese Klasse implementiert IObjectList (hat also alle Methoden des Interfaces)
  TObjectList_Array = class(TInterfacedObject, IObjectList)
    private
      Objects : Array of TObject;
    public
      procedure Add(Obj : TObject); // muss hier rein, da IObjectList implementiert wird!
  end;
 
  TObjectList_List = class(TInterfacedObject, IObjectList)
    private
      Objects : TObjectList;
    public
      procedure Add(Obj : TObject); // muss hier rein, da IObjectList implementiert wird!
  end;
Ok, soweit sind mal die Deklarationen der Klassen fertig. Wie du siehst haben beide Klassen die Methode Add und beide müssen Add haben, da sie IObjectList implementieren. Von Aussen sehe ich nur dieses Add. Intern hat eine Klasse noch ein dyn. Array, die Andere eine TObjectList.
Schauen wir uns nun das Add an.

Delphi-Quellcode:
procedure TObjectList_Array.Add(Obj : TObject);
begin
  setLength(self.Objects, length(self.Objects) + 1);
  self.Objects[length(self.Objects) - 1] := Obj;
end;

procedure TObjectList_List.Add(Obj : TObject);
begin
  self.Objects.Add(Obj);
end;
Wie du siehst machen beide nicht das gleiche. Eine Methode speichert das übergebene Obj in ein Array, das andere in eine Liste. Wenn du aber davon abstrahierst, würdest du sagen, beide speichern Obj. Gut, hier fehlen jetzt Methoden um ein Element auch wieder raus zu holen, aber denk dir die einfach. Auch diese sähen für ein Array anders aus als für eine Liste, aber beide würden dir ein Element rausholen.
Und dieses Abstraktere siehst du nur von aussen.

Wenn du jetzt eine Variable vom Typ IObjectList hast, dann weißt du, dass Add ein TObject aufnehmen kann. Wie das intern gespeichert wird weißt du nicht, aber musst du auch nicht.
Es handelt sich dabei um das so genannte Black-Box-Prinzip. Du hast eine schwarze Kiste. Die kannst du nicht öffnen, du weißt was du reinwerfen kannst und du weißt was rauskommt. Mehr nicht, aber das reicht dir.

Und auch das nochmal als Programm:

Delphi-Quellcode:
// Fügt Element in die Liste ein
procedure AddToList(Liste : IObjectList; Element : TObject);
begin
  // klappt, da jedes IObjectList eine Methode Add hat, der man ein TObject geben kann
  Liste.Add(Element);
end;

procedure Test;
var L1 : TObjectList_Array;
    L2 : TObjectList_List;
begin
  L1 := TObjectList_Array.Create;
  L2 := TObjectList_List.Create;
  
  AddToList(L1, TObject.Create);

  AddToList(L2, TObject.Create);
end;
Schau dir den Code mal gut an. Wie du siehst, kannst du Variablen vom Typ IObjectList übergeben. Die beiden TObjectList_xxx haben dieses Interface implementiert. AddToList weiß zu keinem Zeitpunkt ob Liste gerade eine TObjectList_Array oder TObjectList_List bekommen hat. Es weiß nur, dass es eine Methode Add gibt und die wird aufgerufen.
Soweit klar? Hier bitte wirklich an diesem Beispiel nachfragen wenn etwas unklar ist!

Also alles was geerbt wurde (irgendwann auf dem Weg von TObject -> TIrgendwas) bleibt erhalten. Das heißt wenn du ein Objekt T1 von TObject erben lässt, kannst du es auch überall dort verwenden wo ein TObject verlangt wird. Dann kannst du ein T2 ableiten (TObject -> T1 -> T2) und T2 kannst du überall dort benutzen wo ein TObject oder T1 gebraucht wird.
Dann kannst du ein T3 ableiten...

Wichtig ist, dass du immer nur die Eigenschaften hast, die der Typ (variable : TIrgendwas) festlegt.
Delphi-Quellcode:
type
  T1 = class(TObject)
    public
      color : TColor;
  end;
  
  T2 = class(T1)
    public
      count : Integer;
  end;

procedure setColor(t : T1);
begin
  t.Color := clRed;
end;

procedure resetCount(t : T2);
begin
  t.Count := 0;
end;

procedure resetCount2(t : T1);
begin
  t.Count := 0;
end;

procedure Test;
var inst1 : T1;
    inst2 : T2;
begin
  inst1 := T1.Create;
  inst2 := T2.Create;
  
  setColor(inst1); // klappt super
  setColor(inst2); // klappt auch

  resetCount(inst1); // geht nicht, da inst1 nicht vom Typ T2
  resetCount(inst2); // klappt super

  resetCount2(inst2); // klappt nicht
end;
So, interessant ist natürlich der Letzte Fall. Du übergibst ein T2, dass die Eigenschaft Count hat. Aber die Methode resetCount2 erwartet ein T1. Also werder von inst2 nur die Eigenschaften genommen, die auch ein T1 hat, count gehört nicht dazu. In der Methode siehst du also auch wirklich nur das, was der Variablentyp (T1) beschreibt.

Ok, hoffe das war soweit verständlich.
Was dein Problem mit der Verwaltung angeht, so sollte es eigentlich recht nahe an dem Beispiel der Listen dran sein. Modellier (am besten einfach auf Papier oder so) erstmal die Beziehung der Objekte untereinander.
Also wer hat welche Eigenschaften. Das macht vieles einfacher. Ich denke du würdest erstmal mit den zwei Interfaces auskommen, die ich schon genannt hatte. Wie sie funktionieren (also wie verwaltet wird) kannst du dir später überlegen, erstmal musst du dir überlegen was du alles brauchst.
Ein häufiger Fehler (hab ihn sehr sehr häufig gemacht) ist es, einfach los zu legen. Dann kommt irgendwann der Punkt wo einem klar wird, man hat etwas vergessen und man kann mehr oder weniger von Vorne anfangen (macht sich immer schlecht, ist immer teurer als erwartet und lässt sich fast immer vermeiden).
Deshalb modellier erstmal im Kopf / auf Papier in Ruhe durch. Der Hauptvorteil der OOP ist, dass du dir über die Details zu keinem Zeitpunkt Gedanken machen musst (ganz übertrieben gesagt). Wenn du in deiner schwarzen Kiste die Objekte statt in einem Array in einer TObjectList speicherst, dann ist das dem Programm egal. Es wird halt intern anders gespeichert, aber du wirfst noch das gleiche rein und bekommst noch das gleiche raus.
Hier liegt dann auch der Vorteil von Interfaces. Wenn du als Argument nur ein IObjectList eingetragen hast, ist es dem Programm egal ob die Instanz nun ein TObjectList_Array, eine TObjectList_List oder gar eine TObjectList_FibonacciHalde ist. Die Methode Add gibt es, das reicht.

Und nochmal konkret auf deinen Fall bezogen, ja es ist schon richtig, du trägst in die Interfaces ein Add und Remove (und was auch immer dazu gehört) ein. Hier musst du nur drauf achten, dass jedes Interface genau das beinhaltet, was zusammen gehört. Ein Interface mit nur Add macht keinen Sinn (siehe oben ), aber genauso wenig macht ein Interface mit nur einem Remove Sinn. Eines das Hinzufügen und Entfernen kann, ist hingegen schon geeigneter, oder?
Und so kannst du weiter machen, erstmal die Grundlegenden Eigenschaften in Interfaces (also nur in Hinblick darauf, dass es irgendwann mal irgendein Objekt geben wird, dass diese Methoden braucht).
Dann geht es mit abstrakten Klassen weiter. Hier kommen dann Methoden, Ereignisse und Variablen rein, die spätere konkrete Klassen gemeinsam haben (z.B. wird alles was Sichtbar ist auch eine Position haben, ...). Aber auch hier nur dass was auch zusammen gehört. Wir hatten ja schon, dass nicht jedes Objekt mal einen Titel (Caption) haben muss (aber die einen haben kann man wieder von einer gemeinsamen Klasse ableiten).

Das alles sind natürlich nur Tipps, ich würde sagen es ist immer einer guter Weg so an ein Problem ran zu gehen. Du bleibst flexibel. Es ist gerade bei am Anfang kleineren Projekten schon ein gewisser Mehraufwand hinter und man fragt sich immer mal lohnt sich das? Aber ehrlich, wenn du später was neues einfügst und gut modelliert wurde, ein Traum! Und wenn du mal Code umschmeißen musst, weil nicht gut modelliert wurde... (und das umschmeißen kostet deutlich mehr Zeit/Geld als das bisschen mehr Planung am Anfang).

Falls du dich für die Modellierung entscheidest, würde ich die gerne sehen, dann kann ich dir da sicher weiterhelfen (falls ich dir bisher helfen konnte )
Lass ruhig Dinge bei denen du dir unsicher bist noch etwas aussen vor (wäre hier eigentlich nur das Hauptformular) oder bau es so ein wie du denkst. Muss auch nicht vollständig sein, geht ja nur ums Prinzip.
Und denk noch nicht zu sehr über alle speziellen Eigenschaften und Beziehungen nach. Was man später in ein oder zwei Variablen, mit Listen oder ohne, ... macht ist für die schwarze Kiste erstmal egal. Die guckt man sich später immer schön einzeln an und dann hat man schon den Vorteil dass man weiß was reinkommt und was rauskommt.

Ach ja, wie du später an ein bestimmtes Objekt kommst, ist viel leichter als du vielleicht gerade glaubst, aber ich habe es erstmal aussen vor gelassen, da wie gesagt die Modellierung vorher dran kommen sollte und du nie über Dinge nachdenken brauchst (in der OOP) die noch nicht aktuell sind. Du kannst schließlich sehr sehr leicht erweitern!

Gruß
  Mit Zitat antworten Zitat