Klassenarchitektonische Frage



  • Hi Leute!

    Habe folgende Situation. Ich habe eine Klasse, nennen wir sie Spektrum, welche ein bestimmtes mathematisch-physikalisches Objekt repräsentiert (was genau, ist erst mal egal). Nun gibt es noch eine Klasse SpektrumController. Diese Klasse kümmert sich um die Kommunikation zwischen dem jeweiligen Spektrum und dem User bzw. dem Filesystem durch Methoden wie ReadFromFile(), WriteToFile(), Plot(), PrintStatistics(). Warum mache ich das? Ich möchte eine strikte Trennung zwischen den "eigentlichen" mathematischen Objekten und dem "User-Interface". Die mathematischen Objekte sollen später evtl. mal in eine wissenschaftliche Analyse-Library eingebunden werden, da möchte ich sie nicht durch Methoden wie WriteToFile() etc. verseuchen.

    Nun allerdings der Haken: Die Controller-Klasse muss alle Attribute des jeweiligen Spektrum-Objekts kennen. Meine Frage an euch wäre nun, durch welche Art der Beziehung diese Bedingung am saubersten vollzogen werden kann. Einige bisherige Überlegungen von mir:

    - Hat-Beziehung mit Get-Methoden:
    Ich implementiere SpektrumController eine(n Pointer auf eine) Instanz von Spektrum und in Spektrum für jedes Attribut eine Get-Methode: GetFieldMap(), GetSpektrometerLength() etc.. womit SpektrumController auf die Attribute zugreifen kann. Problem: umständlich...

    - Hat-Beziehung mit Friend-Class:
    Ich mache SpektrumController zur Friend-Class von Spektrum. Problem: "being fried with a father doesn't mean being friend with his son". Will ich von SpektrumController weitere Spezialisierungen ableiten, muss ich diese wieder explizit zur Friend Class machen. Außerdem widerspricht das der strikten Trennung, da ich die Friend Classes in der Spektrum.h benenne.

    - Vererbung:
    Ich lasse SpektrumController von Spektrum erben. Das hätte den Vorteil, dass automatisch auf alle Attribute zugegriffen werden darf. Problem: möchte ich noch weitere Spezialisierungen von Spektrum ableiten und dazu zugehörige Controllers entsprechend von SpektrumController (was der Fall ist), tritt das Diamond-Problem auf. Ferner kann ich Basisklassen nicht zur Laufzeit per Konstruktor initialisieren (also erst den Controller und dann das zugehörige Spektrum), müsste also noch zusätzliche Methoden für die nachträgliche Initialisierung schreiben.

    Was wäre eurer Meinung nach die sauberste Lösung?



  • Zwei oder Drei... ich würd sagen, zwei, kann mir aber vorstellen, das Diamond Problem zu umgehen.

    Edit: Was genau meinst du mit "Strikte Trennung"?
    Und explizit zu friend Class machen ist nicht explizit mehrere Initialisierungs oder Getter(/Setter)-Methoden schreiben.
    Deswegen würd ich dir zu 2 raten.



  • drölf



  • Erste Variante. Wenn Du wirklich eine saubere Trennung möchtest, macht das einfach Sinn. friend funktioniert auch, hat aber den klaren Nachteil, dass Spektrum eine Klasse SpektrumController kennen muss (zumindest vom Namen her). Vererbung ist eine ziemlich starke Bindung, die die Vererbungshierarchie blockiert, sprich: möchtest Du später wirklich eine Hierarchie diverser Controller haben mit speziellen Controllern usw., so hast Du Dir das verwährt. Hat-Beziehung ist einfach loser gekoppelt und bei dem, was Du vorhast, daher meiner Erfahrung nach üblicher und sinnvoller.



  • Vererbung bedeutet entweder:
    "Erbendes ist ein..." bei public-Vererbung
    oder
    "Erbendes ist implementiert als ein" bei protected- oder private-Vererbung.

    Beide Fälle scheinen mir hier nicht vorzuliegen.

    Stattdessen ist Deine Spektrum-Klasse weitestgehend unabhängig vom Lesen und Schreiben von Daten.

    Betrachten wir mal Aggregation- bzw. Komposition:
    Komposition: Die Komponenten leben genauso lange, wie das Kompositum, also in Deinem Fall: Das Spektrum-Objekt lebt so lange wie der "Controller", oder besser gesagt: Die ReaderWriter-Klasse. Statt "Hat-ein..." passt hier besser "Besteht aus...". Das ist es wohl auch nicht.

    Aggregation: Die Komponenten sind in der Lebensdauer unabängig vom Kompositum, aber: Das Kompositum besteht aus den Komponenten. Das könnte was sein. Aber: Solange Du eine Reader-Writer-Klasse hast, sollte die nicht das zu lesende Element enthalten, sondern IMHO lediglich als Referenz, die mit Daten gefüllt wird übergeben bekommen. Denn der ReaderWriter kann unabhängig vom zu Lesenden oder Schreibenden existieren. Ob Du dann in "Spektrum" die Daten mit setter Methoden füllst ist erst mal noch offen.

    Interessant bei der folgenden Lösung ist, dass die Interface-Klasse für den ReaderWriter nichts von den Innereien der Spektrum Klasse kennen muss. Konkrete Implementierungen von ReaderWriter brauchen nur die öffentliche Schnittstelle von Spektrum. Das lässt Dir im Gegenzug viel Gestaltungsspielraum beim Entwerfen von Spektrum.

    class Spectrum
    {
    public:
    	typedef binType float;
    
    	void resize(unsigned int newSize); // nötig für Lesen
    	void setBin(unsigned int index, binType value);
    	binType getBin(unsinged int index) const;
    private:
    	std::vector<binType> bins;
    };
    
    // Interface Klasse. Konkrete Implementierungen lesen dann aus Files, vom Netz, etc.
    class SpectrumReaderWriter
    {
    public: 
    	void readSpectrum(Spectrum& spectrumToBeRead) = 0;
    	void writeSpectrum(const Spectrum& spectrumToBeWritten) = 0;
    };
    


  • Interessant bei der folgenden Lösung ist, dass die Interface-Klasse für den ReaderWriter nichts von den Innereien der Spektrum Klasse kennen muss. Konkrete Implementierungen von ReaderWriter brauchen nur die öffentliche Schnittstelle von Spektrum.

    Ich verstehe den Vorteil so formuliert nicht. Konkrete Implementierungen müssen doch sehr wohl auf die Schnittstelle zugreifen und auch die Implementierung von Spektrum dann zur Verfügung haben. Du machst noch ein Interface dazu, dass OP gar nicht unbedingt gebraucht hatte. Fängt er zeitgleich mit der Implementierung von einem FileReader an, hat er doch genau so viel/wenig Flexibilität bei der Gestaltung von Spectrum?

    Und bzgl. der Schnittstellen von Spectrum gibt es sowieso keine Flexibilität, sobald die erste Subklasse deines Interfaces gebastelt ist. Der einzige Vorteil, der mir bzgl. des Interfaces einleuchtet, ist, dass man auf UI-Ebene irgendwem einen fertigen Controller als Interface übergeben kann, sodass das jeweilige UI-Element nicht die Implementierung der konkreten Reader kennen muss. Aber wieso ist das ein Vorteil? Wenn jemand wirklich etwas am Controller ändern möchte, dann muss er das Interface und alle anderen Klassen auch noch ändern. Das einzig Positive ist doch, dass er durch Gestaltung des Interfaces von vornerein schon weiß, dass das fix ist und er bloß ganz stark überlegen sollte, ob er das so haben möchte, oder?

    Ansonsten wirkt die Vorgehensweise für mich sehr java-mäßig einfach alles, was in irgendeiner Weise erweiterbar sein könnte, in Interfaces zu stecken. Das bläht dann alles wunderschön auf, schafft oft weitere Dateien, sorgt für zusätzliche Indirektionen, erschwert das Debuggen und hat oft nur den Vorteil einer irgendwo erweiterten Flexibilität, die am Ende dann doch durch die Anforderungen der Software gar nicht abgedeckt wurde. Geht nicht gegen Dich, würde das nur gerne ein wenig ausgeführt wissen, finde die Idee durchaus interessant. 🙂



  • @Eisflamme:

    Du hast natürlich recht, ein Interface macht nur dann Sinn, wenn man mehrere Verschiedene ReaderWriter-Implementierungen haben will (File, Netz, etc. wie oben erwähnt). Ansonsten eben ohne separate Interface-Klasse direkt einen Reader, wobei die Schnittstelle gleich beleibt.

    Da yeodelloyd aber bereits erwähnt hat, dass er eine "Controller"-Hierarchie haben wird, scheint mit eine Interface-Klasse geeignet. Dann können Nutzer von ReaderWriter mit Basis-(oder Interface-)-Klassen Referenzen arbeiten.

    Ungeachtet dessen, halte ich meine Überlegungen, keine enge Kopplung zwischen den Klassen (auch nicht per friend) zu haben, für sinnvoll. Mit dem von mir vorgeschlagenen Design, kann die Spektrum-Klasse komplett unabhängig von der SpektrumIO realisiert und getestet werden. Gleiches gilt für die Reader-Writer-Klasse, die nur einen Stub braucht, der die öffentliche Schnittstelle von Spektrum zur Verfügung stellt. Grundsätzlich ginge das auch mit friends und Aggregation, dann wäre aber die Kopplung unnötig eng.



  • Hey, danke für die Antworten! Von allen genannten finde ich die von ogni am interessantesten. Allerdings verstehe ich noch nicht, wieso die Controller-Klasse dann nichts von den Innereien des Spektrums kennen muss. Die Sache ist bei mir (jetzt gehe ich mal ein wenig mehr ins Detail meiner Spektrums-Klasse), dass nicht nur Bins ausgelesen und geschrieben werden, sondern die Berechnung der Bin-Inhalte auf bestimmten physikalischen Parametern beruht. Um das Objekt also wieder zu 100 % so zu erzeugen, wie es vorher war, müssen Reader und Writer nicht nur auf Bin-Inhalte, sondern auch auf alle Parameter zugreifen können. (Zumal ich auch noch eine Print-Methode möchte, die mir alle Parameter in der Konsole ausgibt.)

    Ich suche also jetzt mehr oder weniger aus Faulheit einen eleganten Weg, für alle diese Parameter (so um die 20 Stück...) eine Get-Methode zu schreiben. 😉 Wahrscheinlich werde ich aber wohl nicht drum rum kommen.



  • Aber die physikalischen Parameter sind doch von außen vorgegeben ("Versuchsraum") und nicht Teil (Eigenschaft!) des jeweiligen Spektrum-Objekt, oder? Dann kannst du deine ganzen Parameter in ein eigenes struct auslagern. Zur Berechnung übergibst du dann eine Instanz dieses PhysParms an das Spektrum. Das Speichern von Parm-Spekt-Paaren würde ich dem Benutzer überlassen (oder eben direkt ein Hilfskonstrukt mitliefern).

    Wenn du denkst dass das so nicht geht, wäre es nicht schlecht, wenn du ein wenig mehr Details preisgeben könntest.



  • Wie Spicker bereits erwähnt hat, ist die Berechnung des Spektrums keine seiner Eigenschaften, sondern eine Klasse für sich (von der es unterschiedlichste Arten geben kann).

    Oder im Trivialfall sind das nur Methoden der Art

    void makePhaseSpectrum(Spectrum& spectrumToBeFilled);
    void makeEqualizedSpectrum(Spectrum& spectrumToBeFilled);
    

    Ein Spectrum "weiss" nicht, bzw. muss nicht wissen, wie es erzeugt wird.



  • Hey, danke! Die Parameter in eine Struct auszulagern, halte ich für eine elegante Maßnahme. Das werde ich wohl so umsetzen.


Anmelden zum Antworten