Design-Problem



  • Hi,

    folgendes:
    Ich habe eine abstrakte Klasse StreamInterface:

    class StreamInterface
    {
        public:
            virtual void connect( ... ) = 0;
            virtual void disconnect( ... ) = 0;
    
            virtual void send( ... ) = 0;
            virtual void recv( ... ) = 0;
    };
    

    Diese stellt mir einfache Streamfunktionalitäten zur Verfügung.

    So nun habe ich zB folgende Klasse:

    class ComPort : public StreamInterface { ... };
    class File : public StreamInterface { ... };
    class Socket : public StreamInterface { ... };
    
    // etc ...
    

    Nun werden je nach Anwenung die empfangen/gesendeten Bytes anders intepretiert.

    template<typename Stream>
    class BlaStream : public Singleton<BlaStream<Stream> >
    {
        private:
            Stream stream;
        pivate:
            size sendBla(...);
            size_t receiveBla(...);
    };
    
    template<typename Stream>
    class BlubStream : public Singleton<BlubStream<Stream> >
    {
        private:
            Stream stream;
        pivate:
            size sendBlub(...);
            size_t receiveBlub(...);
    };
    

    Der C'tor vom BlaStream sieht folgendermaßen aus:

    BlaStream()
    {
        stream.connect(...);
    }
    

    Instantiiere ich mit ComPort, connecte ich mich zu einem COM-Port. Instantiiere mich File, dann zu einer Datei.

    Das Problem ist nun mein connect. Zu einem Port connecte ich anders als zu einer Datei.
    Ein Port bekommt zB PortNr, Baud etc.. Ein File irgendwas anders.

    Jetzt kann ich aber in StreamInterface nur ein connect() mit bestimmten Parametern liefern, die aber nicht jeder zwangsläufig auch haben muss.
    Jeder braucht sein eigenes connect() und BlaStream C'tor muss dann das richtige je nach TemplateTyp aufgerufen werden.

    Spezialisierung sind mir momentan nicht lieb, es sei denn es gibt keine andere Lösung. Irgendwelche Ideen, um das Problem zu lösen bzw. das Design zu verbessern ?

    Edit: Ein connect(std::string) gefällt mir auch nicht 🙂



  • Was man oft sieht sind connect-Strings.

    zB übergibst du ein "FILE://C:\test.dat" wenn du C:\test.dat öffnen willst, zB "COM://port=1&baud=9600" für COM port, etc.

    oft ist es aber sinnvoll spezifische konstruktoren zu definieren die eben genau die notwendigen werte verlangen und nicht generisch sind und über eine factory sie dann erstellen lassen. und die factory nimmt dann eben zB solche connect-strings entgegen.

    Edit:
    da dir das ja nicht gefällt, das ganze geht auch als config-objekt und laufzeit polymorphie. die klasse macht einen downcast auf das config objekt um das konkrete objekt zu bekommen mit dem es etwas anfangen kann.

    ich persönlich mag die connect strings aber am liebsten - da man sie auch 1:1 so in config files schreiben kann...



  • Was mich wundert ist, warum du ComPort & Co von einerm Interface ableitest und mit virtuellen Funktionen arbeitest, wenn du danach eh alles in ein template packst. Die Stream-Argumente der template-klassen müssen ja nicht unbedingt von StreamInterface ableiten, sie müssen nur die entsprechenden Methoden haben.

    Was mich dann noch wundert ist, dass du für die verschiedenen Anwendungen/byte-Interpretationen jeweils das komplette template wieder von vorn schreibst.

    So aus dem Stehgreif würde mir folgendes Desgin einfallen:

    template <class ConnectorPolicy, class IntrepretationPolicy>
    class Stream : private ConnectorPolicy, private InterpretationPolicy
    {
    public:
      Stream(ConnectorPolicy cp = ConnectionPolicy(), 
             InterpretationPolicy ip = InterpretationPolicy())
            : ConnectionPolicy(cp), InterpretationPolicy(ip)
      {
        init(); //from ConnectionPolicy
      }
      Stream(InterpretationPolicy ip) 
            : ConnectionPolicy(), InterpretaionPolicy(ip)
      {
        init();
      }
    
      ~Stream()
      {
        endInterpretation(); //from InterpretationPolicy
        disconnect();  //from ConnectionPolicy
      }
    
    private:
      void init()
      {
        connect();  //from ConnectionPolicy
        startInterpretation(); //from InterpretationPolicy
      }
    };
    
    class SocketConnectionPolicy;  //weiß wie man Socketverbindungen herstellt
    class HTTPInterpretaionPolicy; //weiß wie man HTTP spricht ;)
    class FileConnectionPolicy;    //weiß wie man Dateien öffnet und schließt
    class PlainTextInterpretationPolicy; 
    
    int main()
    {
      Stream<FileConnectionPolicy, PlainTextInterpretationPolicy>
        textfileStream("C:\data\foo.txt"); //FileConnectionP hat Ctor(const char*)
    
      std::string URL;
      int portno;
      Stream<SocketConnectionPolicy, HTTPConnectionPolicy>
        websiteStream(SocketConnectionPolicy(URL, portno));
    }
    

    Dem Stream-Template müssten dann noch send- und recv- oder ähnliche Methoden (z.B. die std::iostream-operatoren) verpasst werden die einfach an die InterpretationPolicy weiterleiten und der das (De-)Serialisieren überlassen.
    Wenn du die Streams selber noch polymorph behandeln willst dann leite das Stream-template von einer Basisklasse ab die die ein- und Ausgabe virtuell deklariert.
    Die verschiedenen Arten von Konstruktoren für die verschiedenen Verbindungen lässt du bei der ConnectionPolicy, die speichert intern die nötigen Werte um dann beim connect-Aufruf zu wissen wie wo und weshalb sie sich verbinden muss.



  • Ich verstehe nicht, warum man hierfür überhaupt Templates braucht.

    Vielleicht stehe ich ja auf dem Schlauch, aber für mich sieht das einfach nach einer ganz normalen Klassenhierarchie mit StreamInterface als Basisklasse aus. Der Ctor jeder Unterklasse (oder meinetwegen auch Funktionen wie File::open() oder Socket::connect()) definieren dann Parameter, wie sie gerade gebraucht werden.

    Was wäre der Grund für die Verwendung von Templates??

    Stefan.



  • Hmmmmm schrieb:

    Der Ctor jeder Unterklasse (oder meinetwegen auch Funktionen wie File::open() oder Socket::connect()) definieren dann Parameter, wie sie gerade gebraucht werden.

    Womit dann das Ableiten von einer Basisklasse unnötig wäre, weil durch die verschiedenen Signaturen Polymorphie nicht mehr möglich ist.

    Hmmmmm schrieb:

    Was wäre der Grund für die Verwendung von Templates??

    Die Art der Verbindung auf der einen und die Iterpretation der einzelnen Bytes auf der anderen Seite sind zwei orthogonale Verhaltensweisen, die jedes für sich nach Belieben ausgetauscht und verändert werden können. Sowas wird meistens durch policy-basiertes Design gelöst, was wiederum mit Templates realisiert wird.



  • pumuckl schrieb:

    Hmmmmm schrieb:

    Was wäre der Grund für die Verwendung von Templates??

    Die Art der Verbindung auf der einen und die Iterpretation der einzelnen Bytes auf der anderen Seite sind zwei orthogonale Verhaltensweisen, die jedes für sich nach Belieben ausgetauscht und verändert werden können. Sowas wird meistens durch policy-basiertes Design gelöst, was wiederum mit Templates realisiert wird.

    In C++ wird das oft so gemacht, aber auch oft wo es nicht nötig/sinnvoll ist. Die beiden Dinge kann man auch wunderbar über mehrere Klassen die ein Interface implementieren + mehrere Klassen die dieses Interface konsumieren können implementieren.

    ------

    Ein Problem was ich bei dem Design (bezogen auf das Kopfposting jetzt wieder) sehe, ist die Vermischung von Zuständigkeiten.
    Das Stream Interface sollte IMO nicht Funktionen zum "Herstellen" (connect) und "Betreiben" (recv/send) eines Streams haben, dies sollte getrennt werden. In der Einfachsten Ausführung haben dann die Klassen die das Stream-Interface implementieren Konstruktoren mit irgendwelchen Parametern - was sie eben brauchen.

    Weiters ist es meist nicht nötig dass die Klasse welche den Stream "konsumiert" diesen auch erstellt.

    Beispiel:

    class Stream
    {
    public:
        virtual size_t read_some(...) = 0;
        virtual size_t write_some(...) = 0;
    };
    
    class File : public Stream, private noncopyable
    {
    public:
        explicit File(std::wstring const& path);
        // ...
    };
    
    class CommPort : public Stream, private noncopyable
    {
    public:
        explicit File(size_t portNumber);
        // ...
    };
    
    class Foo : private noncopyable
    {
    public:
        explicit Foo(boost::shared_ptr<Stream> const& stream);
        // ...
    };
    


  • Shade Of Mine schrieb:

    connect-Strings

    Einige Projekte laufen anscheinend schon über solche ConnectionStrings. Müsste nur mal in Erfahrung bringen, ob diese vllt von irgendwo eingelesen werden und mein Projekt das dann auch machen müsste. Zur not bastele ich nicht Zwischenstelle dafür.

    pumuckl schrieb:

    Was mich wundert ist, warum du ComPort & Co von einerm Interface ableitest und mit virtuellen Funktionen arbeitest, wenn du danach eh alles in ein template packst. Die Stream-Argumente der template-klassen müssen ja nicht unbedingt von StreamInterface ableiten, sie müssen nur die entsprechenden Methoden haben.

    volkard schrieb:

    großteil stl-verblendet, tendieren zum übermäßogen konsum unpassender abgefreakter sprachmittel, können schnittstellen und funktionsumfang einfach nicht schlank halten, haben angst vor zeigern und werfen zu viele exceptions.

    🙂

    Ich weiß zwar einiges über C++, aber trotzdem mangelts mir noch an Praxiserfahrung. Schnappe mal hier und da was auf, aber das gelbe vom Ei isses immer noch nicht. Naja, dass ändert sich gerade auch langsam.

    pumuckl schrieb:

    Was mich dann noch wundert ist, dass du für die verschiedenen Anwendungen/byte-Interpretationen jeweils das komplette template wieder von vorn schreibst.

    Da wollte ich ehe noch ansätzen und hatte vor etwas mit nem Strategy-Pattern reinzubassteln.

    pumuckl schrieb:

    Stream(ConnectorPolicy cp = ConnectionPolicy(), 
             InterpretationPolicy ip = InterpretationPolicy())
            : ConnectionPolicy(cp), InterpretationPolicy(ip)
    

    Perfekt! Die richtigen C'tor über nen Template zu generieren, war genau das was ich gesucht habe. Insgesamt eine schöne Lösung, so spare ich mir auch mein S-Pattern.

    hustbaer schrieb:

    Ein Problem was ich bei dem Design (bezogen auf das Kopfposting jetzt wieder) sehe, ist die Vermischung von Zuständigkeiten.
    Das Stream Interface sollte IMO nicht Funktionen zum "Herstellen" (connect) und "Betreiben" (recv/send) eines Streams haben, dies sollte getrennt werden. In der Einfachsten Ausführung haben dann die Klassen die das Stream-Interface implementieren Konstruktoren mit irgendwelchen Parametern - was sie eben brauchen.

    I think about it 😉

    Danke für alle Tipps ...


Anmelden zum Antworten