IRC-Bot Klassendesign



  • Ich habe mir hier einmal das Interface des event_managers überlegt.

    #ifndef EVENT_MANAGER_HPP
    #define EVENT_MANAGER_HPP
    
    #include "socket.hpp"
    
    #include <string>
    #include <functional>
    
    #include <boost/noncopyable.hpp>
    
    namespace irc
    {
    	enum event_type
    	{
    		USER_JOIN,
    		USER_PART,
    		USER_KICK,
    		USER_BAN,
    		USER_QUIT,
    		...
    	};
    
    	// alle funktionen thread-safe
    	class event_manager : public boost::noncopyable
    	{
    	public:
    		typedef std::function<void(std::vector<std::string> args, event_manager& instance)> event_handler;
    
    		event_manager(irc::socket& socket);
    
    		void register_handler(event_type type, const std::string& chan, event_handler handler); // channel-spezifischer handler
    		void register_handler(event_type type, event_handler handler); // globaler handler
    
    		void unregister_handler(event_handler handler);
    
    		void write_line();
    		std::string read_line();
    
    		std::string nick() const;
    		std::string server() const;
    		unsigned short port() const;
    	};
    }
    
    #endif
    


  • void unregister_handler(event_handler handler);
    

    Da bin ich aber echt gespannt, wie die Implementierung aussieht 🙂

    Eventuell ist auch Boost.Signal was für dich...



  • Nexus schrieb:

    void unregister_handler(event_handler handler);
    

    Da bin ich aber echt gespannt, wie die Implementierung aussieht 🙂

    Eventuell ist auch Boost.Signal was für dich...

    Ja ich habe daran gedacht, das mit boost::signal zu implementieren, genauer gesagt mit boost::signal2 🙂

    Allerdings ist mir gerade ein anderes Konzept eingefallen, das mir auch recht gut gefällt. Und zwar zwei abstrakte Basisklassen "packet" und "event" von denen dann weitere packet- und event-Klassen für jeden Event-Typ abgeleitet werden. Dadurch kann man anstatt nur die Argumente am Handler zu bekommen direkt mit den geparsten Daten arbeiten und muss sich die Strings nicht selbst zusammenbauen.



  • Ich denke an sowas:

    #ifndef DINGENS_HPP
    #define DINGENS_HPP
    
    #include "socket.hpp"
    #include <string>
    #include <boost/noncopyable.hpp>
    
    namespace irc
    {
    	class dingens : public boost::noncopyable
    	{
    	public:
    		dingens(irc::socket& socket);
    
    		void register_handler(event_handler* handler);
    		void write_line();
    		std::string read_line();
    
    		std::string nick() const;
    		std::string server() const;
    		unsigned short port() const;
    	};
    }
    #endif
    
    class event_handler
    {
       virtual void receive_line(std::string line)=0;
       //oder
       virtual void join/leave/msg/whisper/kick/ban/motd/...
    


  • Angst vorm inner platform effect hab.



  • volkard schrieb:

    Angst vorm inner platform effect hab.

    ?



  • 314159265358979 schrieb:

    volkard schrieb:

    Angst vorm inner platform effect hab.

    ?

    Ich baue Methoden und nenne sie join und leave und so.
    Du baust Dir ein Framework, setzt event_type/lamda-Paare ab, und so Sachen.
    Ich kann sogar Vererbung.
    Du kannst events an Default-Eventhandler weiterreichen lassen.
    Ich will nicht sagen, daß Dein Entwurf schlecht wäre.
    Mußt nur irgendwo zwischen
    http://en.wikipedia.org/wiki/God_object
    und
    http://en.wikipedia.org/wiki/Inner-platform_effect
    landen und am besten nicht zu weit am Rand.



  • Sorry, ich verstehe nicht was du mir damit sagen möchtest. Wo versuche ich denn so eine "God-Klasse" zu machen?



  • 314159265358979 schrieb:

    Sorry, ich verstehe nicht was du mir damit sagen möchtest. Wo versuche ich denn so eine "God-Klasse" zu machen?

    Nicht Du. Mir könnte es passieren, wenn ich aus Angst vor dem inner platform effect zu wenig trickse.



  • volkard schrieb:

    Ich will nicht sagen, daß Dein Entwurf schlecht wäre.

    Sags ruhig 😃
    Genau deshalb habe ich ja diesen Thread eröffnet.



  • 314159265358979 schrieb:

    Sags ruhig 😃

    Mein Bot sah so aus wie Deiner. Weiß nicht, ob das Design vielleicht doch gut ist. Also damals hatte ich mich ziemlich darüber gefreut. Oder ob es an der Implemetierungssprache (Perl) lag. Er konnte viele Kunststücke. ~Allerdings habe ich ihn wegen schlechter Wartbarkeit aufgegeben.~



  • Also du würdest für jedes Event einen Handler-Typen einführen?



  • Man muss eben abstrahieren...

    Eine Klasse für die eigentliche Verbindung und Funktionalitäten zum Lesen einer Zeile bzw. Schreiben einer Zeile.

    Dann eine Klasse für die eigentliche Sitzung, welche gebrauch von der Verbindungs-Klasse macht.

    Dann eine Klasse welche alle Sitzungen verwaltet.

    An dieser Stelle muss man sich überlegen, wo man das PING/PONG betreibt.

    In der Verbindungs-Klasse? Eher nicht, dafür ist sie zu allgemein.

    In der Session-Klasse? Ja, das klingt vernünftiger. Allerdings sollte man sich hier auch eine Hilfs-Klasse basteln, welche allgemein Funktionen parst.

    Also entsteht eine weitere Klasse IrcIO (IRC-InputOutput), welche Funktionen zum Lesen und Schreiben im IRC-Protokoll implementiert. Diese Klasse wird dann zur Hilfe genommen.

    Das Aufrufen eines Handlers könnte man dann dort auch mit einbauen.

    Ob das aber alles so funktioniert zeigt sich erst in der Praxis 🙂



  • 314159265358979 schrieb:

    Also du würdest für jedes Event einen Handler-Typen einführen?

    Nein, im Gegenteil. Ich dachte eher an einen Observer, der praktisch nur hereinkommende Zeilen observt und selber zerlegt und dispatscht. Observer? Nur, weil ich nicht ervben wollte. Fühlt sich auch an wie ein Dilemma.
    Aber ich bin auch nicht völlig sicher. Müßte die Klassen und Methodenprototypen mal eintippen und ein Bierchen trinken.



  • Mach das 😃



  • volkard schrieb:

    314159265358979 schrieb:

    Also du würdest für jedes Event einen Handler-Typen einführen?

    Nein, im Gegenteil. Ich dachte eher an einen Observer, der praktisch nur hereinkommende Zeilen observt und selber zerlegt und dispatscht. Observer? Nur, weil ich nicht ervben wollte. Fühlt sich auch an wie ein Dilemma.
    Aber ich bin auch nicht völlig sicher. Müßte die Klassen und Methodenprototypen mal eintippen und ein Bierchen trinken.

    Ich finde die Idee recht gut und lässt sich auch gut implementieren. Die Frage bei der Zerlegung ist allerdings, ob du die Zerlegung der Nachricht durch die Subscriber erfolgt oder ob dies bereits vom Observer erledigt wird. Ich sehe Vorteile in beiden Varianten.

    Vorteil Zerlegung im Observer:
    - Alle Subscriber können auf die zerlegten Daten zugreifen
    - Kein Overhead durch ständige Neu-Zerlegung

    Nachteil
    - Unflexibel bei unterschiedlichen Nachrichtenaufbau

    ---

    Vorteile Zerlegung im Subscriber
    - Kann auf jede Nachricht reagieren, da Kontrolle darüber behalten wird

    Nachteil
    - Viel Overhead durch ständige Neu-Zerlegung

    Was haltet ihr davon?

    #include <iostream>
    #include <vector>
    #include <string>
    #include <sstream>
    
    class Subscriber
    {
    	public:
    		virtual void Update(const std::string& line, class Observer& observer) = 0;
    };
    
    class Observer
    {
    	typedef std::vector<Subscriber *> Subscribers;
    
    	Subscribers subscribers;
    
    public:
    	~Observer()
    	{
    		for (Subscribers::iterator it = subscribers.begin(); it != subscribers.end(); ++it)
    		{
    			delete *it;
    		}
    	}
    
    	void Notify(const std::string& line)
    	{
    		for (Subscribers::iterator it = subscribers.begin(); it != subscribers.end(); ++it)
    		{
    			(*it)->Update(line, *this);
    		}
    	}
    
    	void Add(Subscriber *sub)
    	{
    		subscribers.push_back(sub);
    	}
    };
    
    // 
    
    class PingSubscriber : public Subscriber
    {
    	virtual void Update(const std::string& line, class Observer& observer)
    	{
    		std::string buf = line;
    
    		if (buf[1] == 'I')	
    		{
    			buf[1] = 'O';
    
    			std::cout << "Got PING!\nResponse: " << buf << "\n" << std::endl;
    		}
    	}
    };
    
    class PrivmsgSubscriber : public Subscriber
    {
    	// :Macha!~macha@unaffiliated/macha PRIVMSG #botwar :Test response
    	virtual void Update(const std::string& line, class Observer& observer)
    	{
    		std::stringstream strm(line);
    
    		std::string buf;
    
    		std::vector<std::string> tmp;
    
    		while (strm >> buf) 
    			tmp.push_back(buf);
    
    		if (tmp.size() >= 3 && tmp[1] == "PRIVMSG")
    		{
    			std::cout << "User: " << tmp[0] << std::endl;
    
    			std::cout << "Channel: " << tmp[2] << std::endl;
    
    			std::cout << "Message: ";
    			if (tmp[3][0] == ':')
    			{
    				tmp[3].erase(0, 1);
    			}
    
    			for (std::size_t i = 3; i < tmp.size(); ++i)
    			{
    				std::cout << tmp[i] << " ";
    			}
    
    			std::cout << "\n" << std::endl;
    		}
    	}
    };
    
    int main()
    {
    	Observer observer;
    
    	observer.Add(new PingSubscriber);
    	observer.Add(new PrivmsgSubscriber);
    
    	observer.Notify("PING bla");
    	observer.Notify("PING bla");
    	observer.Notify("PING :bla");
    	observer.Notify(":User PRIVMSG #Channel :Response mit mehreren Woertern");
    	observer.Notify(":User PRIVMSG #Channel2 :Response mit noch mehrer Woertern");
    	observer.Notify(":User PRIVMSG #Channel3 Test!");
    }
    


  • Ich kenne dieses Pattern leider überhaupt nicht, daher fange ich damit nicht viel an. Gibts dazu irgendwelche hilfreichen Links?



  • Das Pattern siehst du doch hier im Forum als Code.

    Ansonsten: http://de.wikipedia.org/wiki/Observer_(Entwurfsmuster)



  • Ich würde es auch besser finden, wenn die Zeile nicht ständig neu geparst werden muss. Dann schleichen sich nämlich schnell solche Nachlässigkeiten wie bei jhkhjkhjk ein, wo statt nur auf PING auch auf DING, BINGO oder bIber reagiert wird und die Privmsg-Funktion doppelte Leerzeichen in der Nachricht verschluckt.

    Möglicherweise bekommt man mit ein bisschen aufwendiger Metaprogrammierung so etwas hin (ich zeig mal nur die ungefähre Verwendung):

    void ban(session& s, string nick, string channel, int seconds, string reason);
    
    int main()
    {
        session s;
    
        cmdprocessor cmd(s);
        cmd.register_cmd("!ban", ban);    // "!ban ipsec #c-plusplus.net 12345 ohne grund" -> ban(s, "ipsec", "#c-plusplus.net", "ohne grund");
    }
    


  • Nach euren Anregungen habe ich mir nun folgendes Design überlegt:

    irc::socket
    - Ein gekapselter Socket von boost ohne viel weitere Funktionalität
    - Bietet 2 Funktionen read_line() und write_line(), die \r\n entfernen bzw hinzufügen.
    - Kennt keine Informationen über die session, also keinen nickname, auth, channel o.ä.

    irc::session
    - Bekommt im Konstruktor einen irc::socket, sowie für die Session relevante Daten (Nickname, Auth, Realname, evtl Passwörter, etc..)
    - Hat alle Daten gespeichert über Channels, User, etc..
    - Beantwortet Ping-Anfragen
    - Bietet Funktionen read_line(), write_line(), read_line(const std::string& chan) und write_line(const std::string& chan) = blockierende, thread-safe Funktionen

    irc::protocol
    - Ist für die irc-spezifischen Befehle verantwortlich
    - Ist eine Ansammlung von statischen Funktionen, wie z.B. make_nick_string(const std::string&)

    irc::event
    - (Abstrakte) Basisklasse für alle Arten von Events
    - Hat keine Funktionen

    irc::*_event
    - Konkrete Eventklassen
    - Im Konstruktor wird ein string übergeben und geparst
    - Bietet Zugriffsfunktionen auf die Daten, wie .new_nick(), .old_nick(), etc.

    irc::event_manager
    - Verwaltet die events
    - Verantwortlich für die event-Handler
    - Kann Handler sowohl channel-lokal, als auch global hinzufügen
    - Enthält enum event_type, das den Typ des Events angibt.
    - Ebenfalls thread-safe

    irc::event_handler
    - Basisklasse für event-Handler
    - nicht abstrakt
    - enthält für jedes Event bereits eine Funktion, z.B. on_nick_change()
    - Vordefinierte Funktionen enthalten nur ein throw method_not_overwritten_error("some text"), falls ein Handler mit einem Event registriert wurde, dessen Funktion er nicht überschreibt

    Sagt mir mal, ob das so brauchbar ist. 🙂


Anmelden zum Antworten