Design Frage



  • Gugi schrieb:

    Hab ich mir auch überlegt, ob ich die Read/Write Funktionen mit Hilfe von Templates implementieren sollte.

    Allerdings, müsste ich dann eben für int, bool, string, etc. spezielle Versionen anbieten. Allerdings doch dann auch für unsigned int, unsigned char, signed char, short, long,..., oder (wenn ich alle "normalen" Typen unterstützen möchte).

    Da wäre Überladung wohl eher was als Templatespezialisierung. Denn was bringt dir das allgemeine Template?
    Du kannst versuchen, es nach dem C++-Stream-Vorbild durchzuziehen. Ich würde allerdings gleich die Streams involvieren, nämlich std::stringstream für die Konvertierung von und zu Strings. Dann kannst du dir die Arbeit sparen.

    Gugi schrieb:

    Außerdem: Was mache mache ich dann in der allgemeinen Version? Ich muss ja irgendwie das Objekt in einen String (und zurück) casten: Sollte ich dann z.B. einfach T.ToString() schreiben?

    Eben, Templates sind hier ungünstig. ToString gibt es in C++ nicht für beliebige Typen und es gibt auch keine Konvention wie in Java, die zur Ausgabe einer Klasse eine solche Methode anbietet. Dafür kann man die Stream-Operatoren überladen. Für deinen Fall sollte es wie gesagt reichen, std::stringstream für die Konvertierung heranzuziehen.

    Gugi schrieb:

    Ach ja, da fällt mir in dem Zusammenhang noch eine kleine Frage ein: Diese Template Funktion muss doch dann im Header stehen, oder?

    Ja, grundsätzlich schon. Aber die Sache mit den Templates sollte sich eben erledigt haben...



  • Ich hätte in dem allgemeinen Template eine Konvertierung mittels stringstream vorgenommen. Das funktioniert schon mal für alle Elementaren Datentypen, ich kann mir also Spezialisierung (und auch Überladungen) für bit/byte/short/... und die ganzen unsigned Varianten schenken. Sind schon mal >10 Codedupletten, die wegfallen.Und für die Sachen die sich nicht via stringstream konvertieren lassen (oder auch string, was gar keine Konvertierung benötigt) eine Spezialisierung.



  • @Gugi: Irgendwie habe ich den Eindruck, dass du dein Design nur verteidigst, als ueber Alternativen nachzudenken. Wenn du nur willst, dass man dir hier auf die Schulter klopft, dann bitte: fein gemacht.

    Zu Bool etc.: Ich kann einen Bool als {"True","False"} speichern, hey aber warum nicht auch als {0 , 1} oder { 'b', 'f' } oder ... alles sinnvolle Alternativen. Was davon jetzt aber benutzt wird, ist durch die Anwendung bestimmt. Ob ein Wert vorhanden sein soll, wird auch durch die Anwendung bestimmt, welche Fehler mit Werten dabei auftreten koennen , wird durch die Anwendung bestimmt, wie ein Wert interpretiert werden soll, wird von der Anwendung bestimmt. Also liegt die Verantwortung fuer alle diese Sachen beim restlichen Programm und nicht bei der Klasse CIni.

    Auch braucht man keine separaten Umwandlungsfunktionen, da die Anwendung mittels Stringstream das in ein bis zwei Zeilen erledigt. Also warum extra der Aufwand?



  • knivil schrieb:

    Irgendwie habe ich den Eindruck, dass du dein Design nur verteidigst, als ueber Alternativen nachzudenken. Wenn du nur willst, dass man dir hier auf die Schulter klopft, dann bitte: fein gemacht.

    Das will ich nicht, aber ich will auch nicht einfach irgendetwas übernehmen, ohne verstanden zu haben, warum es jetzt besser ist als etwas anderes.
    Ich habe über die Alternativen nachgedacht. Allerdings bin ich halt einfach noch ein absoluter Noob in Sachen Verwendung der Sprache. So wusste ich dass mit dem stringstream z.B. nicht. Meine Gründe gegen eine Template Version waren:

    1. Zu viele Spezialisierungen
    2. Was passiert bei einem benutzerdefinierten Objekt?
    3. Mit direkten ReadBool, ReadInt, etc. Funktionen sieht man direkt, welche Typen unterstützt werden.

    Wobei ich das mit dem stringstream Operatore Überladen nicht kapiere (ich schau die Klasse mir gleich an). Ich nehme an, dass sowas gemeint war:

    MyStringStream << AnInt;
    

    Aber das hätte ich dann so geschrieben:

    template <class T>
    EResult CIniFileWriter::Write(const string& SectionName, const string& VariableName, const T& Value)
    {
      ...
      MyStringStream << T;
      string StrToWrite = MyStringStream.str();
      ...
    }
    

    knivil schrieb:

    Zu Bool etc.: Ich kann einen Bool als {"True","False"} speichern, hey aber warum nicht auch als {0 , 1} oder { 'b', 'f' } oder ... alles sinnvolle Alternativen. Was davon jetzt aber benutzt wird, ist durch die Anwendung bestimmt. Ob ein Wert vorhanden sein soll, wird auch durch die Anwendung bestimmt, welche Fehler mit Werten dabei auftreten koennen , wird durch die Anwendung bestimmt, wie ein Wert interpretiert werden soll, wird von der Anwendung bestimmt. Also liegt die Verantwortung fuer alle diese Sachen beim restlichen Programm und nicht bei der Klasse CIni.

    Das hier verstehe ich z.B. einfach nicht bzw. sehe ich nicht ein. Wenn ich ein Programm schreibe, und z.B. eben ein Bool in eine Ini Datei speichern möchte, dann ist es doch eigentlich dem Programm egal, wie es gespeichert wird. Es soll einfach ein Bool möglichst einfach geschrieben werden. Hierfür habe ich die Ini Klassen ja, damit ich mir die Konvertierungen sparen kann. Beim Lesen doch das gleiche: Ich möchte einfach z.B. ein Int aus einer Ini Datei lesen, ohne dass ich den string noch Konvertieren muss. Allerdings können natürlich nicht alle strings in Ints konvertiert werden und das Programm muss ja auch eine Möglichkeit haben, auf fehlende oder falsche Werte zu reagieren. Da Ini Dateien aufgrund ihrer Text-Art leicht von Benutztern editiert werden können, kann es gut sein, dass eine Variable nicht in der Ini steht bzw. ein falscher Wert.
    Für das benutzterdefinierte Speichern gibt es ja immer noch die String-Version.



  • dann ist es doch eigentlich dem Programm egal, wie es gespeichert wird

    Ach und der ini-Datei ist es nicht egal? Was in der Datei steht, ist doch der Datei egal. Es geht darum, wer welche Aufgaben/Verantwortung hat. Falls ein Wert fehlt, so kann die ini-Datei ja nichts dafuer. Also sollte sie nur sagen, dass der Wert nicht gefunden wurde. Wie damit umgegangen wird, entscheidet die Anwendung. Und welches Format fuer z.B. Bool gewaehlt wird, ist der Anwendung zwar egal, aber deswegen kann sie sich ja eins aussuchen.

    Hierfür habe ich die Ini Klassen ja, damit ich mir die Konvertierungen sparen kann.

    Ach, deine Ini-Klasse ist auch fuer Konvertierungen zustaendig? Du kannst Konvertierungen voellig unabhaengig von der ini-Klasse definieren. Und da es unabhaengig ist, hat sowas auch nichts in der Klasse zu suchen. Die Kopplung zwischen Lesen/Schreiben als auch Typumwandlung ist voellig unnoetig. Fuer fuer folgende Zeilen brauche ich keine Extrafunktion:

    std::stringstream str;
    str << a number;
    str >> a string;
    

    z.B. ein Int aus einer Ini Datei lesen, ohne dass ich den string noch Konvertieren muss.

    Du wirst ihn immer konvertieren muessen, aber ich wuerde es nicht in der ini-Klasse machen.



  • knivil schrieb:

    Und welches Format fuer z.B. Bool gewaehlt wird, ist der Anwendung zwar egal, aber deswegen kann sie sich ja eins aussuchen.

    Wieso sollte die Anwendung sich mit der Frage belasten, wie jetzt ein Bool gespeichert wird? Bzw., was verstehst du unter "Anwendung"? Ich würde es hier auf alles außerhalb der Ini-Klassen beziehen... (womit ich wsk falsch liege, da dies auch die ReadBool Funktion im 1. C++ Code unten mit in die "Anwendung" mit einschließen würde).

    Würdest du dann einfach nur jeweils eine Funktion zum Lesen bzw. Schreiben anbieten, die den Wert in Form eines Strings zurückgibt, und dann überall im Code, wo eine Konvertierung nötig ist, die 3 Zeilen mit dem stringstream schreiben? Oder in einer extra Funktion, welche so aussähe:

    EOpResult ReadBool(CIniFileReader& IniFile, const string& Section, const string& Variable);
    

    ?

    Irgendwie empfinde ich es aber als besser (bzw. natürlicher), dies in der Klasse zu haben, um die Art der Speicherung zu abstrahieren...
    Bzw. finde ich folgenden Code natürlicher:

    CIniFileReader IniFileObject;
    ...
    bool bReadBool;
    if (IniFileObject.ReadBool("MySection", "MyVariable", bReadBool) != RR_SUCCESS)
    {
      Do something.
    }
    

    als

    CIniFileReader IniFileObject;
    ...
    bool bReadBool;
    if (ReadBool(IniFileObject, "MySection", "MyVariable", bReadBool) != RR_SUCCESS)
    {
      Do something.
    }
    

    Das 2. sieht mehr nach C als C++ aus, oder?

    Aber das Argument, dass die Funktion auch außerhalb der Klasse realisierbar ist, ist wahrscheinlich das stärkere. Steht auch in Meyers Effective C++, so weit ich weiß.

    Aber sollte man dann alle Funktionen, welche auch außerhalb einer Klasse realisierbar sind, auch außerhalb definieren? (Public Funktionen, OK, aber was ist dann z.B. mit privaten Funktionen, welche nur "Hilfsfunktionen" sind, die auf keine Datenelemente/Methoden zugreifen und dadurch entstanden sind, dass aus 1 anderen Funktion aufgrund des Funktionsumfangs Code ausgelagert wurde?)

    Oh mann, ich komme mir bei solchen Entscheidungen immer dämlich vor, weil ich nie weiß, was ich jetzt machen soll. Das gleiche gilt für Variablen/Funktionsbenennung oder ähnliches xD



  • Ich wuerde eine Funktion anbieten fuers setzen von Strings. Dann vielleicht sowas wie string2bool oder int2string fuer Konvertierungen. Das hat den Vorteil, dass du die gleichen Funktionen auch fuer Logging, Usermessages, Eingabe und Ausgabe verwenden kannst. Ich favorisiere natuerlich noch eine einzelne Ini-Klasse mit load, save, getValue und setValue, die nur mit Strings arbeiten.
    Signaturbeispiel fuer eine losgeloeste Konvertierungsfunktion:

    bool int2string( int, std::& string )
    

    std::string ist ein In/Out-Parameter und bool zeigt Fehler an. Kann man auch mit Exceptions machen, aber da scheiden sich die Geister.

    Ich halte mich meist an die Richlinien: lose Kopplung, keine Aufgaben vermischen, keine Codeduplizierung, RAII. Das ist natuerlich der Idealzustand und es sind nur Richtlinien. Auch entstehen durch diese Richtlinien Gefahren wie: premature generalisation, overengineering und solving the wrong problem.

    Oh mann, ich komme mir bei solchen Entscheidungen immer dämlich vor, weil ich nie weiß, was ich jetzt machen soll.

    Quatsch. Das meiste sind nur Vorlieben und die entstehen aus Erfahrung. Auch was ich hier zum besten gebe. Mach einfach deine eigenen. Ich mag z.B. nicht diesen Reader/Writer-Kram, da das irgendwie immer nach Java aussieht. 🙂



  • Also, dann ziehe ich jetzt sämtliche Read/Write Funktionen (bis auf die string Version, evtl. noch umbenennen) aus den Klassen raus und wandle sie in Templates mit Spezialisierungen um. Mehr oder weniger so:

    template <class T>
    EOpResult ReadIniVar(const CIniFileReader& IniFile, const string& Section, const string& Variable, T& Output)
    {
      string ReadValue;
      const EOpResult Result = IniFile.GetValue(Section, Variable, ReadValue);
      if (Result != RR_SUCCESS)
        return Result;
    
      stringstream StringStream;
      StringStream << ReadValue;
      StringStream >> Output;
      //... Error checking
      return Success or Failure (falls Konvertierung fehlschlug).
    }
    
    template <class T>
    EOpResult WriteIniVar(const CIniFileWriter& IniFile, const string& Section, const string& Variable, const T& Input)
    {
      stringstream StringStream;
      StringStream << Input;
      return IniFile.SetValue(Section, Variable, StringStream.str());
    }
    

    Aufjedenfall viele Dank für die geduldigen Antworten 🙂

    (Mh, eine Frage hätte ich dann da noch xD :

    Gugi schrieb:

    Aber sollte man dann alle Funktionen, welche auch außerhalb einer Klasse realisierbar sind, auch außerhalb definieren? (Public Funktionen, OK, aber was ist dann z.B. mit privaten Funktionen, welche nur "Hilfsfunktionen" sind, die auf keine Datenelemente/Methoden zugreifen und dadurch entstanden sind, dass aus 1 anderen Funktion aufgrund des Funktionsumfangs Code ausgelagert wurde?)

    )



  • Gugi schrieb:

    Aber sollte man dann alle Funktionen, welche auch außerhalb einer Klasse realisierbar sind, auch außerhalb definieren? (Public Funktionen, OK, aber was ist dann z.B. mit privaten Funktionen, welche nur "Hilfsfunktionen" sind, die auf keine Datenelemente/Methoden zugreifen und dadurch entstanden sind, dass aus 1 anderen Funktion aufgrund des Funktionsumfangs Code ausgelagert wurde?)

    Wenn die Funktion nur im Zusammenhang mit der Klasse Sinn macht, und auch nicht in die öffentliche Schnittstelle passt, würde ich eine der folgenden Varianten vorziehen:

    a) Private Klassenmethode
    b) Deklaration/Implementation ausschließlich in der cpp (ggf. noch in einen anonymen Namensraum)

    Inzwischen tendiere ich eher zu b, da ich im Header eine Codedokumentation (Doxygen) mache, und keine unnötigen Methoden/Funktionen aufführen will.



  • k, danke 🙂


Anmelden zum Antworten