Strukturierung von Klassen



  • Hallo,

    das Thema, worauf sich das bezieht ist schon etwas älter, deshalb vielleicht zur Erläuterung den letzten Ratschlag von

    @seppj sagte in Methoden einer anderen Klasse benutzen:

    Ratschlag: Was immer gut hilft, um recht einfach bei gut definierten und einsetzbaren Klassen zu landen, ist, dass jede Klasse genau eine Art von Ding aus der realen Welt beschreibt (oder auch aus der mathematischen Idealwelt). Damit vermeidest du solchen Quatsch, wie dass deine mathematischen Formen Zeichenmethoden haben. Hast du schon einmal einen (mathematischen) Kreis gesehen, der sich selber malt? Nein, ein Zeichner malt eine Näherung des mathematischen Idealkreises. Wahrscheinlich löst das auch ganz natürlich die komische Wahl deiner Member, die schon mehrmals angesprochen wurde. Was soll denn überhaupt eine AffineGraphic sein? Soll das eine mathematische Figur plus eine affine Transformation darauf sein? Dann mach das doch auch so!

    Ich habe das jetzt so gemacht:

    Je eine Klasse für die Art der Berechnung

    class PPAttributes
    {
        double move_x = 0;
        double move_y = 0;
        double move_z = 0;
        int alpha = 0;
        int beta  = 0;
        int gamma = 0;
        double scale_x = 1.0;
        double scale_y = 1.0;
        double scale_z = 1.0;
    
        void print() const;
        void reset();
    
        friend class PointProjection;
    };
    
    class PointProjection
    {
        const std::vector<Point>& scale( double x, double y, double z );
        const std::vector<Point>& move( double x, double y, double z );
        const std::vector<Point>& rotate( int alpha, int beta, int gamma );
        const std::vector<Point>& reset();
        void print() const;
    
        PPAttributes g;
        std::vector<Point> base_points, points;
        void calculate();
        friend class GraphObject;
    };
    
    class AffineAttributes
    {
        double mov_length = 1.0;
        int mov_phi = 0;
        double scale_x = 1.0;
        double scale_y = 1.0;
        int rot_phi = 0;
    
        double a = 1.0;
        double b = 0;
        double c = 0;
        double d = 1.0;
        double e = 0;
        double f = 0;
    
        void print() const;
        void reset();
    
        friend class AffineTransformation;
    };
    
    class AffineTransformation
    {
        const std::vector<Point>& affineScale( double x, double y );
        const std::vector<Point>& move( double length, int phi );
        const std::vector<Point>& rotate( int phi );
        const std::vector<Point>& reset();
        void print() const;
    
        AffineAttributes g;
        std::vector<Point> points;
        void calculate();
        friend class GraphObject;
    };
    

    Eine Klasse, um alles zu verwalten

    struct SideLength
    {
        double x = 0;
        double y = 0;
        double z = 0;
    };
    
    class GraphObject
    {
    
    public:
    
        GraphObject( const WireFrame& wf ) :
            name( wf.name ),
            side_length { wf.side_length, wf.side_length, wf.side_length },
            base_points( wf.points ), points( base_points ), edges( wf.edges ),
            wired( true )
        {
            reset();
        }
        GraphObject( const Shape& shape ) :
            name( shape.name ),
            side_length { static_cast<double>( shape.swidth ), static_cast<double> ( shape.sheight ), 0 },
            base_points( shape.points ), points( base_points ),
            wired( false )
        {
            reset();
        }
    
        void scale( double x, double y, double z );
        void move( double x, double y, double z );
        void rotate( int alpha, int beta, int gamma );
    
        void affineMove( double length, int phi );
        void affineScale( double x, double y );
        void affineRotate( int phi );
    
        void reset();
        void print( int amount = 10 ) const;
        static void setDistance( int dist = 100, int d = 2 );
    
        void draw( const DrawArea& draw,
                   int x, int y,
                   const Char& chr,
                   int char_mix_mode = BACKGROUND,
                   bool show = false ) const;
    
        void plot( const DrawArea& draw,
                   int x, int y, 
                   int char_mix_mode = BACKGROUND ) const;
    
    private:
    
        PointProjection      pp;
        AffineTransformation af;
    
        std::string name;
        SideLength side_length;
        std::vector<Point> base_points, points;
        std::vector<Edge> edges;
        bool wired;
    
        inline static int distance = 100;
        inline static int d_large  = 2;
        void map();
    };
    

    Je eine Klasse für die Art des Objekt

    class Shape
    {
    public:
    
        static Shape peek( int X, int Y, int swidth, int sheight );
        static Shape snip( const DrawArea& draw, int sx, int sy, int swidth, int sheight );
        static Shape load( const std::string& name );
    
        int width() const;
        int height() const;
        void save( const std::string& name ) const;
        void poke( int X, int Y, int char_mix_mode = NONE ) const;
    
    private:
    
        std::string name = "Shape";
        int swidth  = 0;
        int sheight = 0;
        std::vector<Point> points;
        friend class GraphObject;
    };
    
    class WireFrame
    {
    public:
    
        template<typename wireFrame> WireFrame( const wireFrame& wf ) :
            name( wf.name ), side_length( wf.side_length ), points( wf.points ), edges( wf.edges ) {}
    
    private:
    
        std::string name;
        double side_length = 0;
        std::vector<Point> points;
        std::vector<Edge> edges;
        friend class GraphObject;
    };
    

    Und für letzteres den alten Kram wie zB

    struct Rect
    {
        Rect( double length = 1.0 ) : side_length( length )
        {
            points = getPoints( side_length, x, y, z );
            edges  = getEdges( a, b );
        }
    
        const std::string name = "Rect";
        double side_length = 0;
    
        const std::vector<double> x      = { -0.50,  0.50,  0.50, -0.50 };
        const std::vector<double> y      = {  0.50,  0.50, -0.50, -0.50 };
        const std::vector<double> z      = {     0,     0,     0,     0 };
    
        const std::vector<std::size_t> a = {  0,  1,  2,  3 };
        const std::vector<std::size_t> b = {  1,  2,  3,  0 };
    
        std::vector<Point> points;
        std::vector<Edge> edges;
    };
    

    Ohne den alten Code zu kennen, ist es schwierig zu fragen, ich habe aber versucht nachzuvollziehen, wie SeppJ es gemeint haben könnte. Ich wollte schon die Manipulation und die Ausgabe an einem Objekt haben.
    Wenn dies immer noch nicht zutrifft, sollte man mir es an einem zB Diagramm zeigen. Danke.



  • Dein "struct Rect" ist in meiner Vorstellungswelt schon eine Klasse und keine Struktur mehr. Aber das ist Geschmackssache.

    Meine "struct"s sind ausschließlich reine Datenhalter, ohne jede Funktionalität oder selbst-definierte Konstruktoren/Destruktoren, im Idealfall PODs.
    Alles was komplizierter ist, wird bei mir ne Klasse.
    Muss aber jeder selber entscheiden, wie strikt er da bei der Trennung ist. Ist ja auch nur ne Stilfrage.



  • Ja ne, Du hast Recht. Das sollte ich noch ändern. Hatte ich gar nicht mehr richtig auf dem Schirm.

    Wichtig war mir die Trennung von Art des Objekt, die Verwaltung, und die Berechnungen. Zwar habe ich immer noch eine Klasse, die äußerlich alles beinhaltet, aber sie beherbergt nur die Daten und führt sie zur Berechnung weiter.

    Wen ich Operation und Ausgabe an einem Objekt haben möchte, kann ich mir das nicht anders vorstellen?


  • Mod

    @lemon03 sagte in Strukturierung von Klassen:

    Wen ich Operation und Ausgabe an einem Objekt haben möchte, kann ich mir das nicht anders vorstellen?

    Ostream ist doch auch eine Klasse, die alles ausgeben kann, ohne mit dem, was sie ausgibt, etwas zu tun zu haben.



  • @lemon03 sagte in Strukturierung von Klassen:

    Wen ich Operation und Ausgabe an einem Objekt haben möchte, kann ich mir das nicht anders vorstellen?

    Spricht aus meiner Sicht nix dagegen. Ich habe auch öfter Klassen, die Daten halten und verändern und zusätzlich noch eine oder mehrere Ausgabemöglichkeiten haben:

    • saveToFile( ... )
    • toString()
    • ostream-Operator
      Je nachdem was man braucht.


  • @seppj sagte in Strukturierung von Klassen:

    @lemon03 sagte in Strukturierung von Klassen:

    Wen ich Operation und Ausgabe an einem Objekt haben möchte, kann ich mir das nicht anders vorstellen?

    Ostream ist doch auch eine Klasse, die alles ausgeben kann, ohne mit dem, was sie ausgibt, etwas zu tun zu haben.

    Und GraphObject ist doch eine Klasse, die mit dem was die ausgibt, nichts zu tun hat? Sie hat nur einen vector<Point>, den sie weiterreicht und ausgibt.



  • Und wohin ausgibt? Direkt mittels cout?

    Und was mir noch aufgefallen ist:

    • bei deinen (erstgezeigten) Klassen fehlen die public:-Bereiche. Oder sollen diese nur über die friend-Klassen ansprechbar sein (also lesend und schreibend)?
    • du hast doch die Klasse SideLength (wenn mich auch der Name irritiert)? Warum hast du in deinen Klassen (besonders PPAttributes) dann noch einzelne _x, _y, _z -Member?
    • den Aufbau (bzw. die Schnittstelle) deiner beiden Klassen PointProjectionund AffineTransformation finde ich auch eigenartig (keinen Konstruktor, Rückgabewert der Funktionen - auf jeweiligen Member points ???).


  • Zwar in die Konsole, aber rein über eine WinAPI-Funktion. Die ist aber ganz unten, das sie im restlichen Code nicht mehr auftaucht. Die beiden Ausgaben schauen so aus

    void GraphObject::draw( const DrawArea& draw,
                            const int x, const int y,
                            const Char& chr,
                            const int char_mix_mode,
                            const bool show ) const
    {
        if ( wired )
        {
            if ( edges.size() > 0 )
            {
                for ( std::size_t i = 0; i < edges.size(); ++i )
                {
                    const std::size_t a = edges[ i ].a;
                    const std::size_t b = edges[ i ].b;
                    drawLine( draw,
                              points[ a ].cx +x, points[ a ].cy +y,
                              points[ b ].cx +x, points[ b ].cy +y,
                              chr, char_mix_mode );
                }
            }
            else
            {
                drawDot( draw, points.front().cx +x, points.front().cy +y, chr, char_mix_mode, show );
            }
        }
        else
        {
            std::cerr << "GraphObject::draw(): wired warning\n";
            std::cerr << "no WireFrame";
            ready();
        }
    }
    
    void GraphObject::plot( const DrawArea& draw,
                            const int x, const int y,
                            const int char_mix_mode ) const
    {
        for ( const auto& point : points )
        {
            drawDot( draw, point.cx +x, point.cy +y, point.chr, char_mix_mode );
        }
    }
    
    • Ja, die Attribute-Klassen ( vielleicht hätte ich eher private-structs nehmen sollen) sollen vollkommen private sein und nur für die Friend-Klasssen nutzbar.

    • Habe ich das? Das muss dann ein Fehler sein. Die Seitenlängen sind wie die Namen nur zur besseren Übersicht beim printen eines Objekt. Die kommen in den Berechnungen nicht vor. Zwar sind die bei WireFrame alle gleich, aber bei Shape sind es zwei verschiedene. Da die Objekte dreidimensional dargestellt werden, habe ich auch drei benannt.
      Jedenfalls haben die mit den Attribute-Klassen rein gar nichts zu tun.

    • Ja, das war mit besten Wissen und Gewissen. Ich habe da lange überlegt, wie ich das hinbekomme. Irgendwann hat sich dies als die beste Lösung rauskristallisiert.
      Ich hatte bestimmt auch eine Konstruktor-Übergabe überlegt, das es die nicht gibt, war wahrscheinlich das es damit nicht wie gewünscht lief.



  • Ich meinte eher die print()-Funktionen in den einzelnen Klassen, oder was machen diese?

    Ok, dann ist SideLengthnur zufällig genauso eine Datenstruktur, um drei Dimensionen zu speichern.
    Und bei PPAttributesmeinte ich daher so etwas:

    struct Vector // or whatever name you like
    {
         double x, y, z;
    };
    
    struct PPAttributes
    {
        Vector move;
        // ...
        Vector scale;
    };
    

    Und wie benutzt du dann die beiden Klassen PointProjection und AffineTransformation? Greifst du (z.B. beim Initialisieren oder beim Auslesen) direkt auf points zu?

    Das ist alles andere als gutes OOP.



  • Die print-Funktionen "printen" die Daten als reine Zahlenwerte

    void GraphObject::print( const int amount ) const
    {
        console.setCursorHome();
        std::cout << name << "  side lengths:\n";
        auto n = 8;
        const auto np = 3;
        std::cout << "\n x: " << std::setw( n ) << std::fixed << std::setprecision( np ) << side_length.x;
        std::cout << "  y: "  << std::setw( n ) << std::fixed << std::setprecision( np ) << side_length.y;
        std::cout << "  z: "  << std::setw( n ) << std::fixed << std::setprecision( np ) << side_length.z;
        pp.print();
        af.print();
    
        int l = 0;
        std::cout << "\nPoints:";
        for ( const auto& point : points )
        {
            std::cout << "\n x: " << std::setw( n ) << std::fixed << std::setprecision( np ) << point.x;
            std::cout << "  y: "  << std::setw( n ) << std::fixed << std::setprecision( np ) << point.y;
            std::cout << "  z: "  << std::setw( n ) << std::fixed << std::setprecision( np ) << point.z;
            ++l;
            if ( l > amount ) break;
        }
        l = 0;
        n = 4;
        std::cout << "\n\nMap:";
        for ( const auto& point : points )
        {
            std::cout << "\n  cx: " << std::setw( n ) << point.cx;
            std::cout << "  cy: "   << std::setw( n ) << point.cy;
            ++l;
            if ( l> amount ) break;
        }
        pressKey();
    }
    

    Was Du meinst, muss ich erst mal kurz überlegen, auf die Schnelle kann ich jetzt dazu nichts schreiben.

    Tja, und der letzte Punkt? Muss ich wahrscheinlich dann noch mal ransetzen.



  • Genau das meinte ich. Was wenn du dann die Daten in eine Datei schreiben wolltest?
    Dafür gibt es doch den Stream-Operator:

    std::ostream& operator <<(std::ostream& os, const GraphObject &graphObject);
    

    Übergebe einfach std::ostream& als Parameter an deine print-Funktionen und rufe dann diese im Stream-Operator auf.
    Natürlich passen dann console.setCursorHome() und pressKey() nicht mehr, aber diese gehören ja auch generell nicht in eine Logik-Klasse.



  • Aha, danke. Nachvollziehbar. Das schaue ich mir mal an.


Anmelden zum Antworten