Klassenmember in separaten Klassen kapseln oder primitiv lassen?



  • Hallo zusammen,

    ich hätte ne Frage, auf der ich hier im Forum und über Google keine Antwort gefunden habe. Nehmen wir als Beispiel die Klasse "Kunde".

    class Kunde
    {
        std::string Straße, Hausnummer, Vorname, Nachname;
        std::uint16_t Nr;
    };
    

    Nehmen wir an Straße, Hausnummer, Vorname und Nachname haben eine Begrenzung. Eine gewisse Anzahl von Zeichen und nicht leer.
    Die (Kunden)Nr muss größer als 0 sein, darf aber nicht größer als 16000 sein.

    Das sind die Grundbedingungen. Im Konstruktor der Klasse können wir jetzt jeden Wert validieren und prüfen, ob er diese Rahmenbedingungen entspricht. Oder wir können jedes Member in eine eigene Klasse kapseln, die natürlich weiß, wann sie gütig ist und wann nicht und im Konstruktor dann diese per Value und move übergeben.

    class Kunde
    {
        Straße straße;
        Hausnummer hausnummer;
        Name vorname, nachname;
        Id Nr;
    };
    

    Bei diesem Beispiel geht es mir darum, was eine gute Faustformel ist, wann man Member in eine eigenen Klasse kapselt und wann man diese so primitiv wie möglich hält und einfach nur als string oder int verwendet.

    Was sind eure Meinungen dazu?

    PS: Wie kann man Einrückungen machen ohne per Leertaste den Abstand abzuschätzen?



  • Hm... ich glaube: kommt darauf an...

    Ich schreibe in letzter Zeit fast nur Python. Dort gibt es das Modul pydantic, damit könnte man das so schreiben:

    class Kunde(BaseModel):
        strasse: Annotated[str, StringConstraints(min_length=1, max_length=100)]
        hausnumnemr: Annotated[str, StringConstraints(min_length=1, max_length=10)]
        # ... 
        id: Annotated[int, Field(gt=0, lt=16000)]
    

    Das ist ziemlich cool, weil dann gleich alles entsprechend validiert ist, wenn man das Objekt erzeugt, während die Typen aber "ganz normale" strings bzw. int sind.

    Wenn man so einfache Dinge hat wie "String mit bestimmter Länge(nrange)" oder ints mit einer Range, dann kann man halt schon sowas nehmen (bzw. 1x eine Klasse schreiben, die sowas tut). Nur wenn man speziell validieren muss, lohnt sich dann vielleicht eine spezielle Klasse oder man muss es im (oder nach dem) Konstruktor machen (insbesondere dann, wenn die Validität der Member voneinander abhängt -- im Adressbeispiel hängt der PLZ-Validator ja z.B. vom Land ab).

    Vielleich Sicher kann man in C++ sowas nachbauen. Habe gerade schnell rumgegooglet, daher keine Garantie auf die Qualität, aber das hier sieht gut aus: https://github.com/getml/reflect-cpp/ - du könntest via using Id = rfl::Validator<int, rfl::Minimum<1>, rfl::Maximum<16000>>; int-Grenzen setzen. Für Strings geht das ebenfalls - dann via rfl::Size.



  • Ich würde für extra Typen votieren. Ich bin leider häufig genug zu faul dafür selbst, aber extra Typen macht den Code leichter zu lesen und sicherer zu nutzen.

    Wenn wir uns die erste Klasse angucken, wie würde da ein Ctpr aussehen?

    ungefähr so:

    Kunde::Kunde(const std::string& straße, const std::string& hausnummer, const std::string& vorname, const std::string& name)
    
    Kunde kunde("teststraße", "42", "Luke", "Skywalker"); //funktioniert
    Kunde kunde2("42", "Luke", "Skywalker", "teststraße"); //kompiliert auch
    

    Wenn du eigene Klassen hast, hast du so was:

    class Straße{
    public:
    explicit Straße(const std::string &straße_):
    straße(straße_){};
    
    private:
    std::string straße;
    }
    
    //Für die anderen Parameter was ähnliches
    
    Kunde::Kunde(const Straße& straße, const Hausnummer& hausnummer, const Vorname& vorname, const Name& name)
    
    Kunde kunde(Straße("teststraße"), Hausnummer("42"), Vorname("Luke"), Nachname("Skywalker"));
    
    Kunde kunde2(Vorname("Luke"), Straße("teststraße"), Hausnummer("42"), Nachname("Skywalker")); //Compiler Fehler
    
    

    Wenn du eigene Typen hast, ist es viel schwerer Parameter zu vertauschen und beim lesen sieht man sofort was welcher Parameter ist. Auch wenn man später irgendwie Funktionen hat, wie zum Beispiel

    ValidateStraße(const Straße& straße)
    

    kann man die Funktion wirklich nur auf einer Straße aufrufen und nicht ausversehen auf einem Namen.

    Daher wäre ich für eigene Klassen 😉

    Das Stichwort für Google wäre "Type safe c++"



  • Was zusätzlich für eine Klasse für die einzelnen Werte spricht, dass ich bei Änderungen der Konditionen wie z.B. Zeichenanzahl oder Struktur der "Strasse" als solches, nicht jede Funktion suchen muss, wo ich diese als std::string übergebe und dort ggf. jedes mal Anpassungen vornehmen muss. Die Parameterübergabe bzw. Verarbeitung einer "Strasse" als Objekt findet sich schneller.



  • @Schlangenmensch sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    Ich würde für extra Typen votieren. Ich bin leider häufig genug zu faul dafür selbst, aber extra Typen macht den Code leichter zu lesen und sicherer zu nutzen.

    Generell ja, aber der Drawback ist eben manchmal, dass Code sehr aufbläht. Man wird bestimmt irgendwelche Funktionen nutzen wollen, die nur "normale" std::strings (oder gar const char *) als Parameter nehmen. Dann muss man jedes mal sowas wie strasse = Strasse(call_string_style(strasse.to_string())) aufrufen (den Constructor will man dann ja vermutlich explicit haben und keine automatischen casts erlauben). Einerseits ist dann ganz klar, was passiert, andererseits wird es auch manchmal mühsam und es macht nicht immer Sinn, 100% typesafe zu schreiben (insbesondere wenn der Nutzen gering ist - bzw. bei Vertauschungen nicht gleich ein riesiges Problem entsteht).

    In diesem Fall will man vielleicht gar nicht explizit Attribute wie "Straße" und "Hausnummer" am Kunden haben, denn das wären eigentlich Attribute von "Adresse". Ein Kunde könnte auch mehrere Adressen haben. Das so zu modellieren, wäre viel wichtiger. Dann übergibt man nämlich einfach immer Adressobjekte anstelle von Straße und Hausnummer separat. Damit ist die Verwechslung dann auch ausgeschlossen (außer beim Erstellen der Adresse). Andererseits wüsste ich aber sowieso nicht, wie man "Name" und "Straße" unterschiedlich validieren könnte. Beide Felder können "wilde Namen" enthalten.



  • @wob Ja, stimmt schon, Code kann dadurch recht verbose werden. Aber, meiner Meinung nach, übertrifft der Nutzen häufig die größere Schreibarbeit.
    Wenn man jetzt öfter Funktionen aufrufen will, die einen String erwartet, dann könnte man mit einem Conversion Operator arbeiten:

    class Strasse{
    public:
    operator std::string() const {return strasse;}
    }
    

    Man kann den auch explicit machen, dann muss man aber jedesmal nen static_cast aufrufen. Dafür kann man dann eine Strasse nicht ausversehen zu einem string werden lassen.



  • @Schlangenmensch sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    Man kann den auch explicit machen

    ja, habe ungefähr auf die Sekunde gleichzeitig zu deinem Kommentar das excplicit oben auch erwähnt. Ich finde immer noch "es kommt darauf an". Bei Datum (!!!), Zahlen, Einheiten etc bin ich voll bei dir. Bei so ein paar Adressstrings? Ich weiß nicht. Halte ich da für übertrieben. Lieber ein schönes Adressobjekt. Und diese automatischen Konvertierungsoperatoren / nicht-explicit Konstruktoren nehmen dann wieder einen Teil der Typsicherheit für Convenience weg.



  • @wob sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    Lieber ein schönes Adressobjekt. Und diese automatischen Konvertierungsoperatoren / nicht-explicit Konstruktoren nehmen dann wieder einen Teil der Typsicherheit für Convenience weg.

    Sehe ich genauso. Auf der Arbeit kenne ich selbst viele Stellen in unserem Quellcode, wo solche Cast Operation unbeabsichtigt bzw. teilweise sogar beabsichtigt und nicht sichtbar, passieren. Ich bin immer für Typsicherheit.

    @wob sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    In diesem Fall will man vielleicht gar nicht explizit Attribute wie "Straße" und "Hausnummer" am Kunden haben, denn das wären eigentlich Attribute von "Adresse". Ein Kunde könnte auch mehrere Adressen haben. Das so zu modellieren, wäre viel wichtiger. Dann übergibt man nämlich einfach immer Adressobjekte anstelle von Straße und Hausnummer separat. Damit ist die Verwechslung dann auch ausgeschlossen (außer beim Erstellen der Adresse). Andererseits wüsste ich aber sowieso nicht, wie man "Name" und "Straße" unterschiedlich validieren könnte. Beide Felder können "wilde Namen" enthalten.

    Sehe ich genauso, dass ein Kunde natürlich mehrere Adressen haben kann. Das war nur ein einfaches Beispiel. Wenn man es sehr genau nehmen möchte, könnte man natürlich ein komplexes Adressobjekt haben, dass Zugriff auf alle Straßennamen im echten Leben hat und diese validiert.

    @Helmut-Jakoby sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    Was zusätzlich für eine Klasse für die einzelnen Werte spricht, dass ich bei Änderungen der Konditionen wie z.B. Zeichenanzahl oder Struktur der "Strasse" als solches, nicht jede Funktion suchen muss, wo ich diese als std::string übergebe und dort ggf. jedes mal Anpassungen vornehmen muss. Die Parameterübergabe bzw. Verarbeitung einer "Strasse" als Objekt findet sich schneller.

    Das ist ein Faktor, den ich noch gar nicht betrachtet und beachtet habe. Ein sehr gut Hinweis.



  • Ich würde einfach die member privat machen und die constraints in settern prüfen.



  • @Tyrdal sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    Ich würde einfach die member privat machen und die constraints in settern prüfen.

    In diesem Fall ja, bei komplexeren Validierungen die man auch an anderen Stellen braucht, kann aber eine eigene Klasse schonmal Sinn machen. Spontan denke ich da z.B. an Validierung gültiger IPv4/v6 Adressen, was man vielleicht an etlichen Stellen im Code benötigt.

    P.S.: Wäre das eine Java-Geschäftsanwendung, käme man natürlich nicht um Hilfskonstrukte wie u.a. eine AbstractHausnummerValidationServiceClientFactory herum - da gälte dann eher "eine Bibliothek je Member". SCNR 😛



  • @Tyrdal sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    Ich würde einfach die member privat machen und die constraints in settern prüfen.

    Das die Member nicht public sein sollten, ist klar. Du meinst das im Konstruktor die Setter Funktionen aufgerufen werden, die dann gleich validieren?



  • @KK27 sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    @Tyrdal sagte in Klassenmember in separaten Klassen kapseln oder primitiv lassen?:

    Ich würde einfach die member privat machen und die constraints in settern prüfen.

    Das die Member nicht public sein sollten, ist klar. Du meinst das im Konstruktor die Setter Funktionen aufgerufen werden, die dann gleich validieren?

    Ja, das ist so ein Fall wo setter echt Sinn machen, weil sie eben den Wertebereich prüfen können.


Anmelden zum Antworten