Design Problem: Exceptions



  • Hallo,

    ich habe wieder mal ein Ordentliches Design-Problem:
    Wann, wie, warum und wo soll ich Exceptions einsetzen?

    Beispiel, wo ich's versaut habe:
    Ich habe eine ServerUser-Klasse, die eine Verbindung zu einem Server bereitstellt, in einer Methode, die jedes Frame aufgerufen wird, hole ich mir die Daten vom Server ab und schicke diese danach an alle Plugins.
    Schön und gut, hier der grausame Code:

    const std::string& Serveruser::GetAndProcessMessages()
    {
    	using namespace Exception;
    	// Eine Verbindung muss bestehen
    	if(!m_psClient)
    		throw Exception::SocketError("Keine Verbindung bestehen.", Exception::LW_ERROR);
    
    	std::string cMsg;
    	try
    	{
    		cMsg = m_psClient->SelectMainLoop();
    	}
    	catch(SocketError& eErr)
    	{
    		switch(eErr.GetHeavyness())
    		{
    		case LW_ERROR:
    			throw eErr;
    		case RS_ERROR:
    			delete m_psClient;
    			m_psClient = new Internet::SocketClnt(m_strIP, m_usPort);
    			throw eErr;
    		case BAD_ERROR:
    			throw eErr;
    		}
    	}
    
    	for(vPlugin::iterator it = m_vPlugins.begin(); it != m_vPlugins.end(); ++it)
    	{
    		try
    		{
    			it->m_pfFunc(cMsg, 0);
    		}
    		catch(PluginError& eErr)
    		{
    			switch(eErr.GetHeavyness())
    			{
    			case LW_ERROR:
    				throw PluginError(it->m_strPlugin + ":" + eErr.GetError(), LW_ERROR);
    			case RS_ERROR:
    				DelPlugin(it->m_strPlugin);
    				AddPlugin(it->m_strPlugin);
    				throw PluginError(it->m_strPlugin + ":" + eErr.GetError(), RS_ERROR);
    			case BAD_ERROR:
    				DelPlugin(it->m_strPlugin);
    				throw PluginError(it->m_strPlugin + ":" + eErr.GetError(), BAD_ERROR);
    			}
    		}
    		catch(SocketError& eErr)
    		{
    			switch(eErr.GetHeavyness())
    			{
    			case LW_ERROR:
    				throw eErr;
    			case RS_ERROR:
    				delete m_psClient;
    				m_psClient = new Internet::SocketClnt(m_strIP, m_usPort);
    				throw eErr;
    			case BAD_ERROR:
    				throw eErr;
    			}
    		}
    	}
    	return cMsg;
    }
    

    Argh, aber wie soll ich es anders machen?
    SelectMainLoop macht das ganze Zeugs mit select. Da dabei viele Socket-Fehler auftreten können, habe ich auch ein try genommen und natürlich muss ich die Fehler, falls welche kommen, catchen und behandeln.
    Auch die Wiederverbindung kann aber misslingen, sodass wieder eine Exception geworfen wird.
    Da ich einsah, dass das Design unbrauchbar ist, habe ich da jetzt erstmal keinen weiteren try-Block eingebaut.
    Im unteren Teil habe ich auch ein try, und zwar innerhalb der for-Schleife.
    Das ist sicherlich sehr performancelastig, aber ich hatte keine Wahl, für die Fehlermeldung muss ich nun einmal einen Teil aus it haben.
    Bei den ganzen Vorgängen, die zum Behandeln der Fehler genommen werden, können wiederum Fehler auftreten, sodass ich hier wieder try- und catch-Blöcke bräuchte, argh.

    Nun gut, das soll sich jetzt ändern, kann mir jemand helfen, wie man i.A. mit Exceptions arbeitet? Bisher habe ich nämlich noch nicht damit gearbeitet. 🙄

    MfG Mis2com



  • 1.du kannst innerhalb eines catch blocks weitere try blöcke definieren.
    2.exceptions sollten Zentral verwaltet werden, dann ist der code eher wiederzuverwenden,zb wenn du mehrmals SocketError oder ähnliches hast.
    3.vielleicht wär es auch besser, mittels vererbung mehrere verschiedene fehlerklassenarten zu verwenden, dann fallen auch die lästigen switch blöcke weg, und alles ist übersichtlicher..



    Ja, aber das ist doch kein ordentliches CodeDesign. 😞
    2.
    Serveruser ist die zentrale Klasse schlechthin.
    3.
    Das wäre eine Möglichkeit, aber dadurch vermindere ich nicht die Anzahl der try- und catch-Blöcke. 😞

    Danke schon mal :),

    MfG Eisflamme



  • GetAndProcessMessages - eine Funktion hat _eine_ Aufgabe und nicht 2
    die switch sind doof - mach mehrere Fehlerklassen.

    Warum erstellt GetAndProcMsg denn den Socket neu?
    Was wenn es einen Fehler gab, so dass der Socket nicht neu aufgebaut werden kann - dann fliegt ne 2. Exception und du hast einen terminate() aufruf -> bot ist tod

    Das wollen wir nicht.
    Wenn es einen Socket Error gab, dann wird der irgendwo zentral gehandhabt, aber sicher nicht in GetAndProcMsg.

    Bleibt also nur noch die Plugin Fehlerabfrage übrig.
    Das neuaufbauen des Plugin halte ich für fraglich - denn ein Fehler, auch wenn er kritisch ist, kann wohl kaum mit einem neu aufbauen des Plugins ausgebessert werden.

    Und musst du bei einem BAD_ERROR wirklich das Plugin killen? Das ist IMHO auch nicht so gut. Vielleicht ist das Plugin ja wichtig, aber es hab ne unerwartete Eingabe, die das Plugin gewaltig verwirrt hat - weiterlaufen tut es ja trotzdem.

    Also hätten wir die Sache auch schon verkürzt.

    Bleibt noch die Frage über, warum du eine Exception weiter wirfst, wenn noch andere Plugins weiterarbeiten wollen. Das hängt jetzt vom Design ab... Aber auf jedenfall müsste die Exception eine referenz auf das 'kaputte' Plugin beinhalten, sonst kannst du nix anderes machen als den Fehler loggen...

    Ich komme somit zu etwa folgendem resultat:

    //by value - wir wollen ja kein undefiniertes verhalten.
    std::string Serveruser::GetAndProcessMessages()
    {
        using namespace Exception;
        // Eine Verbindung muss bestehen, und sie muss verbunden sein.
        if(!m_psClient || !m_psClient->connected())
            throw Exception::SocketError("Keine Verbindung bestehen.");
    
        std::string cMsg = m_psClient->SelectMainLoop();
        for(vPlugin::iterator it = m_vPlugins.begin(); it != m_vPlugins.end(); ++it)
        {
            try
            {
                it->m_pfFunc(cMsg, 0);
            }
            catch(CriticalPluginError& eErr)
            {
                throw SomeException(*it);
            }
            catch(...)
            {} //normal weitermachen, nix kritisches passiert
            //uu auch loggen
        }
        return cMsg;
    }
    


  • jo,das meinte ich mit zentral ;).
    du solltest eindeutige Fehlerobjekte erstellen,und die nicht innerhalb der Funktion abarbeiten.
    der programmierer brauch nur zu wissen, dass es sein könnte, dass die connection flöten geht,und kann sich so drauf einstellen, und im notfall selber entscheiden, ob er ein exit(1) aufrufen will,oder ob er vielleicht eine notfall prozedur einleiten will("warning connection lost, try to get new connection in 10seconds" oder auch "warning connection lost,you play now alone 😃 ").



  • Danke für die Tipps. 🙂

    Und musst du bei einem BAD_ERROR wirklich das Plugin killen? Das ist IMHO auch nicht so gut. Vielleicht ist das Plugin ja wichtig, aber es hab ne unerwartete Eingabe, die das Plugin gewaltig verwirrt hat - weiterlaufen tut es ja trotzdem.

    Wenn es eine Eingabe ist, die das Plugin gewaltig verwirrt hat, und es kein schlimmer Fehler ist, wird nicht BAD_ERROR geworfen, BAD_ERROR wird nur geworfen, wenn es wirklich keine Möglichkeit gibt weiterzuarbeiten.
    Das richtig einzuschätzen liegt im Ermessen desjenigen, der die Plugins schreibt.

    Hm, zentral... Heißt das, ich baue einen try-Block um den Funktionsaufruf jener Funktion auf?
    Ich wollte es eigentlich so handhaben, dass die Klasse das selber korrigieren kann. Oder soll ich eine andere Funktion nehmen bzw. auch eine weitere Klasse darum herum?

    Das Weiterwerfen ist ausschließlich zum Loggen gedacht, d.h. das kann der Benutzer selber entscheiden, Anzeigen kann er's sicher auch.

    Jedenfalls danke für die Tipps, wenn ich Zeit habe, werde ich das mal gründlich überarbeiten.

    MfG Eisflamme 🙂



  • OT: Deine Plugins, sind das DLL-Dateien?
    Eine C++ Exception darf eine DLL nämlich niemals verlassen. Fehler lassen sich bei DLLs nur über Rückgabewerte oder Hilfsfunktionen (GetLastError) erkennen.



  • Eine C++ Exception darf eine DLL nämlich niemals verlassen

    Doch das geht wenn man die Laufzeitbibliothek als DLL linkt.



  • such schrieb:

    Eine C++ Exception darf eine DLL nämlich niemals verlassen

    Doch das geht wenn man die Laufzeitbibliothek als DLL linkt.

    Nein. DLLs haben ein vorgeschriebenes Interface. Diese Regeln legen unter anderem fest, dass eine DLL ausschließlich Funktionen exportieren darf, die einer bestimmten Aufrufkonvention folgen.
    Exceptions waren dort nie eingeplant und verletzen diese Aufrufkonvention. Deswegen wird es höchstens dann funktionieren, wenn DLL und EXE vom selben Compiler mit derselben Compilerversion stammen. Und selbst das ist nicht garantiert.



  • Ups, gut, dass du mir das sagst, ich wäre voll reingerannt. 😞



  • Hi,

    //by value - wir wollen ja kein undefiniertes verhalten.

    Es ist auch so nicht undefiniert, weil SelectMainLoop eine konstante Referenz zurückliefert, die durch den Aufbau ebenfalls gültig ist.

    std::string cMsg = m_psClient->SelectMainLoop();

    naja, SelectMainLoop kann auch Exceptions werfen und wo sollen die dann bitte aufgefangen werden?
    Im Übrigen:

    int main()
    {
    try
    {
    Main main;
    main.Execute();
    }
    catch(...)
    {
    ...
    }
    return 0;
    }
    

    Was bringt hier das catch? Was soll das außer Fehlerausgabe und Wertrückgabe denn bitte tun?

    Ich verstehe in der Tat überhaupt nicht, wann und wo es logisch ist Exceptions einzusetzen, wer sie behandeln sollte, inwiefern etc. anscheinend habe ich völlig falsche Vorstellung davon.
    Kennt jemand dazu gutes Material (kein Buch)?

    Danke jedenfalls schon mal,

    MfG Eisflamme



  • Mis2com schrieb:

    //by value - wir wollen ja kein undefiniertes verhalten.

    Es ist auch so nicht undefiniert, weil SelectMainLoop eine konstante Referenz zurückliefert, die durch den Aufbau ebenfalls gültig ist.

    Mag ja sein, aber du kopierst den Inhalt ja in die lokale variable cMsg - und gibst dann eine referenz darauf zurueck.

    [quote[naja, SelectMainLoop kann auch Exceptions werfen und wo sollen die dann bitte aufgefangen werden?[/quote]
    Was wirft sie denn fuer exception?
    Irgendwas wo du sinnvoll reagieren kannst?

    Was bringt hier das catch? Was soll das außer Fehlerausgabe und Wertrückgabe denn bitte tun?

    Dem User eine Nachricht ausgeben:
    "Es tut uns Leid aber wir haben voll Mist gebaut."
    und dann das programm beenden.
    bzw. anbieten das programm neu zustarten, oder im debug modus neu starten oder fehlerbericht drucken. ka, irgendwas halt.

    Das ist besser als wenn es einfach abstuertzt und eine standard meldung "unhandled exception bla bla" kommt.

    Ich verstehe in der Tat überhaupt nicht, wann und wo es logisch ist Exceptions einzusetzen, wer sie behandeln sollte, inwiefern etc. anscheinend habe ich völlig falsche Vorstellung davon.
    Kennt jemand dazu gutes Material (kein Buch)?

    Exceptional C++ hat mir sehr geholfen, wobei das eher nicht auf wann werfe ich exception eingeht, sondern exception sicherheit. ka ob dir das was bringt.

    uU fuehlst du dich aber in java wohler - dort werden exception so aehnlich behandelt wie du es tust.

    im prinzip gilt aber:
    sowenig try-catch bloecke wie moeglich (aber natuerlich nicht zu wenige).
    meistens erledigt RAII die ganze arbeit, ich habe deshalb auch quasi nie try catch bloecke...

    eine exception ist naemlich eine ausnahme - etwas das im prinzip nicht vorkommt wenn alles halbwegs ordentlich laeuft.



  • Hallo,

    uU fuehlst du dich aber in java wohler - dort werden exception so aehnlich behandelt wie du es tust.

    Nein, ich will nicht Java programmieren, die Sprache ziehe ich in Zukunft zwar auch in Betracht, aber das, was ich mache, gefällt mir selber nicht besonders gut, ich bin nur etwas hin- und hergerissen und weiß nicht, wie ich es tun soll...

    Dem User eine Nachricht ausgeben:
    "Es tut uns Leid aber wir haben voll Mist gebaut."
    und dann das programm beenden.
    bzw. anbieten das programm neu zustarten, oder im debug modus neu starten oder fehlerbericht drucken. ka, irgendwas halt.

    Das ist besser als wenn es einfach abstuertzt und eine standard meldung "unhandled exception bla bla" kommt.

    Gut, ich dachte erst, dass catch in der main() wirklich etwas behandeln würde, also nicht nur Ausgabe, so ist mir das dann auch klar.

    naja, SelectMainLoop kann auch Exceptions werfen und wo sollen die dann bitte aufgefangen werden?

    Was wirft sie denn fuer exception?
    Irgendwas wo du sinnvoll reagieren kannst?

    Ja, wenn der Server die Verbindung z.B. trennt o.Ä.
    Oder wie sollte ich darauf reagieren?
    Hier mal der weniger gute Code von SocketClnt:

    const std::string& SocketClnt::SelectMainLoop() 
    {
    	if(!m_bValid)
    		throw Exception::SocketError("Kein valides Socket.", Exception::RS_ERROR);
            // Falls kein Buffer vorhanden -> erstellen
    	if(!m_cstrBuffer)
    		m_cstrBuffer = new char[NBUFFERSIZE];
    
    	// Nachrichtenliste leeren
    	m_strRecv = "";
    
    	// fdset leeren
    	FD_ZERO(&m_fdsRead);
    	// Server fdset hinzufügen
    	FD_SET(m_sSocket, &m_fdsRead);
    
    	// Timeout zurücksetzen
    	m_tvTimeout.tv_sec = m_tvTimeout.tv_usec = 0;
    	// Schauen, ob was empfangen wurde
    	if((m_nRc = select(0, &m_fdsRead, 0, 0, &m_tvTimeout) > 0))
    	{
    		// Daten empfangen
    		m_nRc = recv(m_sSocket, m_cstrBuffer, NBUFFERSIZE-1, 0);
    
    		// Bei m_nRc == 0 hat der Server offensichtlich die Verbindung unterbrochen
    		if(m_nRc == 0)
    			throw Exception::SocketError("Der Server hat die Verbindung unterbrochen.", Exception::RS_ERROR);
    		// Wenn beim Empfangen der nachricht ein Fehler stattfindet, so kann man damit rechnen, dass die Verbindung
    		// nicht mehr valide ist
    		if(m_nRc == SOCKET_ERROR)
    		{
    			int m = WSAEFAULT;
    			int n = WSAGetLastError();
    			throw Exception::SocketError("Empfangen der Nachricht fehlgeschlagen.", Exception::RS_ERROR);
    		}
    		// Für die Nachrichtenauflistung mit Delimiter \n ist es hier sinnvoller, \n direkt zu setzen
    		m_cstrBuffer[m_nRc] = '\0';
    
    		m_strRecv += m_cstrBuffer;
    		// Nachricht hinzufügen
    	}//WSAEINVAL
    	// Empfangene Strings zurückliefern
    	return m_strRecv;
    }
    

    Ich werde das Konzept für die Sockets noch einmal überarbeiten, jedenfalls bin ich davon ausgegangen, dass ein nicht-verbundenes Socket auch keine Daseinsberechtigung hat und daher nicht valide ist. Ich hörte hier im Forum doch auch einmal davon, dass Methoden wie open() oder close() für basic_fstream kein gutes Design wären, daher übertrug ich dies auf Sockets, schließlich macht ein nicht-verbudenes Socket genauso wenig Sinn wie ein fstream ohne Dateiverbindung.
    Das mit dem Buffer sollte auch klar sein, der wird nur ein Einziges mal allokiert.
    Und ich hielt es eigentlich für eine gute Idee, das mit Exceptions zu machen, wenn getrennt wird...

    Also Exceptions sind Ausnahmen, klar, nur inwiefern?
    Anscheinend ist die Trennung vom Server, die am Ende jedes Programmes stattfinden sollte, keine Ausnahme, sondern praktisch die Regel...
    Mir ist klar, dass Exceptions nicht zur Programmsteuerung dienen sollten, in Java scheint dies genau anders zu sein?

    Jedenfalls will ich nicht die Sprache wechseln, sondern hier mal besser werden, danke vielmals. 🙂

    MfG Eisflamme



  • Mis2com schrieb:

    Ja, wenn der Server die Verbindung z.B. trennt o.Ä.
    Oder wie sollte ich darauf reagieren?

    Wie willst du denn darauf reagieren? Am besten garnicht. Lass den user entscheiden was er will.
    Normalerweise werde ich mir einen anderen server suchen, wenn mir dieser einen socket error liefert.
    vielleicht will ich es aber auch in 1 minute nochmal probieren...

    die socket klasse kann das nicht wissen. sie meldet den fehler und wartet ab.

    Ich werde das Konzept für die Sockets noch einmal überarbeiten, jedenfalls bin ich davon ausgegangen, dass ein nicht-verbundenes Socket auch keine Daseinsberechtigung hat und daher nicht valide ist.

    Sehe ich genauso.

    Ich hörte hier im Forum doch auch einmal davon, dass Methoden wie open() oder close() für basic_fstream kein gutes Design wären, daher übertrug ich dies auf Sockets, schließlich macht ein nicht-verbudenes Socket genauso wenig Sinn wie ein fstream ohne Dateiverbindung.

    ]
    exakt.

    bedenke aber: was passiert wenn ein lesefehler bei fstream auftritt? (OK, angenommen man setzt mit setexception fuer failbit eine exception) - es fliegt dir eine exception um die ohren. die datei ist aber weiterhin 'offen' - auch wenn jede lese oder schreiboperation vermutlich auch fehlschlagen wird.

    Mir ist klar, dass Exceptions nicht zur Programmsteuerung dienen sollten, in Java scheint dies genau anders zu sein?

    C++ arbeitet _sehr_ sparsam mit exception. zB ausser .at() wirft keine methode der STL defaultmaessig.

    das ist bei streams zwar doof, aber ist halt so 😞

    ich sehe es in etwa so: sobald eine exception in einem normalen durchlauf fliegen kann, dann ist es keine ausnahme.

    zB darf EOF beim lesen aus einer datei keine exception werfen. wenn es aber lesefehler gibt, oder gar die datei nicht gefunden wird - dann schon. denn dann ist etwas unvorhergesehenes passiert.

    bei sockets ist es aehnlich. der socket liest und liest, bis die verbindung beendet wird - soweit alles klar. allerdings muesste eine exception fliegen, wenn ich vom geschlossenen socket lesen will. das kann man natuerlcih auch immer etwas anders sehen - also zwing dich nicht dazu alles genauso zu sehen wie ich.



  • Hi,

    gut, genauso wie du muss ich es nicht sehen, aber so oder so... Mein Konzept ist momentan einfach nicht tragbar, soviele catch-Anweisungen... das finde ich ja selbst nicht schön.
    Aber z.B. kommt es auch vor, dass ein Socket die Verbindung verliert, weil der Provider mal kurz die Verbindung trennt und wiederverbindet (kommt vor, bei T-Online z.B.). Dies ist nicht sehr schlimm, aber ärgerlich, wenn keine automatische Wiederverbindung stattfindet.
    Und da es sich hierbei um eine Ausnahme handelt, finde ich zwei Exceptions doch schon angemessen.
    Allerdings, soll die nächsthöhere Instanz - hier GetAndProcessMessages - diese Ausnahme einfach ignorieren? In deinem Fall hättest du kein einziges catch darin und die Application-Klasse (also ich werde es demnächst wohl auch so mit Application machen)... sollte die überhaupt was davon mitbekommen?
    Nagut, Serveruser stellt eine Klasse dar, die einen Manager der Nachrichten und Antworten zu einem Server vertritt. Allerdings beinhaltet ServerUser auch eine Verbindung zum Server, jetzt gibt es 2 Möglichkeiten:

    1. ServerUser selbst stellt so eine Verbindung wieder her
    2. Application tut das über öffentliche Methoden von ServerUser

    Ich mag mich nicht so recht entscheiden, ServerUser würde naheliegen, weil es bei mehreren Servern lästig wird, wenn dem Fehler ein Zeiger auf die Klasse, die wiederverbunden werden soll mitgeliefert wird, obwohl das Serveruser doch eigentlich alleine könnte.
    Oder soll ich ServerUser eine Art Main-Methode geben, sodass dort GetAndProcessMessages aufgerufen wird, oder die Nachrichten verwaltet werden?

    Nochmals vielen Dank,

    MfG Eisflamme



  • wenn die connection verloren wurde->exception innerhalb werfen und abarbeiten, wenn das nich klappt->exception nach aussen.
    alternativ bool wert throwException. wenn true ->direkt exception nach aussen werfen, wenn false innerhalb abarbeiten



  • Äh, das widerspricht zu ziemlich dem, was Shade gerade sagte, weil eine verlorene Verbindung keine Ausnahme ist, da am Ende immer getrennt wird und bei dem Port 80 z.B. auch immer sofort eine Trennung erfolgt.
    In dem Sinne ist eine Exception hier unangebracht, weil sie dann zur Programmsteuerung dienen würde, wo kein Ausnahmefall vorliegt. oO

    Aber gut...



  • Woher weiss man warum die Verbindung geschlossen wurde? das kann man nur wissen, wenn man das verwendete Protokoll kennt - (da man sonst ja nicht weiss, ob dieses Ende der Verbindung beabsichtigt war oder nicht)

    Dieses Problem hat der Socket. Er weiss, die Verbindung ist zuende - warum weiss er allerdings nicht.
    Der Anwender der Klasse kann es aber sehr wohl wissen. Naemlich dann wenn noch Daten erwartet werden, aber der Socket zu ist - sprich, wenn ich von einem geschlossenen Socket lesen will.

    Ich sehe sockets als streams an, deshalb mal folgender pseudo code:

    while(getline(socket, str))
    {
      cout<<str;
    }
    

    Jetzt koennte man SocketCLOSE als eine Art EOF interpretieren. Dann waere obriger Code korrekt.

    Irgendwie gefaellt mir der obrige Code. Das lesen von einem Socket sollte sich wirklich nicht von dem lesen aus einer datei unterscheiden - denn genau das ist es ja in vielen faellen auch 🙂

    eine exception koennte dann fliegen, wenn wir folgenden code verwenden wollen wuerden:

    while(getline(socket, str))
    {
      cout<<str;
    }
    socket.get(c); //PENG denn socket ist bereits zu
    

    Wenn es dann noch eine reconnect() methode gibt, hat der Anwender alle moeglichkeiten richtig mit dem socket umzugehen.

    der socket alleine ist dumm - erst das darueberliegende protokoll macht ihn klug 🙂



  • OK, ich verstehe...

    Deine Idee ist wirklich interessant, was muss ich für die Verwendung von getline machen?

    > und << sind zu überladen, soll ich erben lassen?

    MfG Eisflamme



  • Mis2com schrieb:

    Deine Idee ist wirklich interessant, was muss ich für die Verwendung von getline machen?

    > und << sind zu überladen, soll ich erben lassen?

    Einfach einen eigenen stream implementieren.
    Das geht ueber einen streambuffer und uber das erben von ostream bzw. istream

    letztens gab es dazu eh eine frage hier. ich habe da gleich Josuttis fdstream zum abschauen empfohlen 🙂

    ansonsten schau dir mal an wie es socket++ macht - wird eh zeit dass boost sockets bekommt.


Anmelden zum Antworten