Softwarearchitektur: Abhängigkeiten in normalen Funktionen



  • Wow. Vielen Dank für die umfangreichen Antworten. Es ist wirklich gar nicht so einfach.

    Ich zeige mal was ich so habe...
    ...verzeiht den schlechten Aufbau 😞

    int getExtendedKeyUsageFlagCount(X509 *x509)
    	{
    		// function tracker for debugging
    		FUNCTION_TRACE;
    
    		int extIndex = 0;
    		X509_EXTENSION *ext = nullptr;
    		EXTENDED_KEY_USAGE_Guard eku(nullptr);
    		int ekuFlagsNum = 0;
    
    		if (x509 == nullptr)
    		{
    			NTS_EXCEPTION("A passed parameter is invalid.", "parameter 'x509' is a nullptr");
    		}
    
    		extIndex = X509_get_ext_by_NID(x509, NID_ext_key_usage, -1);
    		if (extIndex == -1)
    		{
    			NTS_EXCEPTION("The certificate extension 'Extended Key Usage' couldn't be found.", "external function failed");
    		}
    
    		ext = X509_get_ext(x509, extIndex);
    		if (ext == nullptr)
    		{
    			NTS_EXCEPTION("Failed to get the X.509 extension from the certificate.", "variable 'ext' is a nullptr");
    		}
    
    		eku.reset(static_cast<EXTENDED_KEY_USAGE*>(X509V3_EXT_d2i(ext)));
    		if (eku == nullptr)
    		{
    			NTS_EXCEPTION("Failed to decode the X.509 extension.", "variable 'eku' is a nullptr");
    		}
    
    		ekuFlagsNum = sk_ASN1_OBJECT_num(eku.get());
    		if (ekuFlagsNum < 0)
    		{
    			NTS_EXCEPTION("The number of flags in the stack couldn't be determined.", "external function failed");
    		}
    
    		return ekuFlagsNum;
    	}
    

    ...das Makro...

    #ifndef THIS_LINE
    #define THIS_LINE (std::string(__FILE__) + ";" + __FUNCTION__ + ";" + std::to_string(__LINE__)) ///< the error source in string form
    #endif
    
    #define NTS_EXCEPTION(description, cause)\
    {\
    	std::stringstream s;\
    	s << description << "   [" << cause << "]";\
    	NtsLogger().log("FATAL", s.str(), THIS_LINE);\
    	throw s.str();\
    }
    

    Der Code der oben zu sehen ist, entstand ziemlich am Anfang meines aktuellen Projekts. Zu diesem Zeitpunkt hatte ich erst 2-3 Monate Erfahrung mit C++ und dementsprechend ist auch der Aufbau nicht besonders. ...Zumindest hatte ich zu diesem Zeitpunkt das RAII-Idiom eingepflegt. Es ist jetzt eine von ca. 150 Funktionen, die alle die selbe Struktur verfolgen und zumindest ...naja... optisch konsistent sind. Dieses Gefrickel mit meiner NTS_EXCEPTION war eine Notlösung, da ich es zu diesem Zeitpunkt einfach nicht besser wusste. Die einzelnen Funktionen sind über namespaces gruppiert und werden von Klassen einer höheren Abstraktionsebene verwendet.

    Da ich an der Fachhochschule bin, habe ich die Möglichkeit ohne Termin- oder Kundendruck meine Software zu verbessern. Habe einige Bücher gelesen, aber oft sind es dann eben doch besondere Situationen, in denen mir auch Bücher nicht helfen und mir noch die Erfahrung fehlt. Im Code oben ist noch so einiges was ich ändern würde, aber nur die Exceptions bereiten mir erneut Probleme. Aus aktueller Sicht möchte ich feste Verbindungen im Code lösen und durch lose Kopplungen ersetzen. Das Makro würde ich an dieser Stelle entfernen und es durch eine Exception-Klasse ersetzen. Bin mir ehrlich gesagt noch nicht sicher ob es mir was bringt, aber ggf. kann ich implizit besser loggen oder die Ausgaben steuern. ...ich weiß es nicht 😕

    ...Anstatt der Exception hier, könnte es genau so gut ein Loggger sein, der eingebaut wird. Da hat man dann solche Überlegungen was man mit diesen Abhängigkeiten macht.

    hustbaer schrieb:

    Dem gegenüber steht der Mehraufwand die quasi omnipresente Dependency "Logger" überall mitzugeben -- bald benötigt jede noch so kleine freie Funktion nen Logger-Parameter. Das nervt natürlich.

    ...ja genau das ist der Punkt 😉
    Von Thread-Local-Storage habe ich bis jetzt noch nie gehört, aber ich werde mal stöbern.

    ...möglicherweise mache ich mich wieder verrückt und in simplen Funktionen ist es total unkritisch. Allerdings versuche ich auch mögliche Tests zu berücksichtigen. ...auch da bin ich zugegebenermaßen Amateur und habe noch nie eine Testumgebung von innen gesehen. Bis jetzt habe ich nur einfache quick-and-dirty unit-tests gemacht und einige memcheck-Überprüfungen mit Vallgrind.

    Auf jeden Fall nochmals Danke dass ihr euch Zeit für mich genommen habt 🙂
    Ich weiß jeden Rat zu schätzen und manchmal ist es ja nur ein Schlüsselwort, das die Entscheidung bringt 😉

    viele Grüße,
    SBond



  • SBond schrieb:

    hustbaer schrieb:

    Dem gegenüber steht der Mehraufwand die quasi omnipresente Dependency "Logger" überall mitzugeben -- bald benötigt jede noch so kleine freie Funktion nen Logger-Parameter. Das nervt natürlich.

    ...ja genau das ist der Punkt 😉
    Von Thread-Local-Storage habe ich bis jetzt noch nie gehört, aber ich werde mal stöbern.

    Das ordentlich zu machen ist halt auch *einiges* an Aufwand. Und man kann auch viele Fehler damit machen. Vermutlich viel mehr als wenn du der einfach nen Logger* mitgibst.



  • (ich kenne im Java/C#-Bereich keine einzige Software die den Logger nicht statisch bezieht, alle Log-Frameworks sind auch darauf ausgelegt, um das DI-Problem "switch the implementation" anzugehen wird meistens eine Fassade ala slf4j verwendet ... ich bin persönlich großer Fan von DI und IoC, aber auch ich beziehe alle Logger statisch via LogManager in ein statisches Klassenfeld)

    MfG SideWinder



  • hustbaer schrieb:

    Als "Zwischenlösung" bietet sich vielleicht Thread-Local-Storage an. D.h. man setzt den Logger in den Thread-Local-Storage, und wer ihn braucht, der holt ihn sich von dort. Hat aber natürlich auch Nachteile. Wenn man beim Erzeugen von Threads nicht immer den Logger neu setzen möchte, muss man sich 'was einfallen lassen mit dem man den Logger des Parent-Thread automatisch an den Child-Thread "vererben" kann. Ebenso muss man sich u.U. 'was basteln um in einem bestimmten Abschnitt, wo man z.B. in einem "geborgten" Thread läuft den Logger auf den TLS drauf und möglichst automatisch bei Verlassen des Scopes wieder runter nimmt. (Gut, das ist bloss ne kleine RAII Helper-Klasse, keine Tragik.)

    Ja, ich bin neulich auch über ein ähnliches Problem gestolpert. Nicht mit Loggern, aber andere Objekte, die so ähnlich überall benutzt werden. Hab mir zuerst gedacht, ganz einfach, steck die Teile ins TLS. Und dann nach paar Minuten Nachdenken gemerkt, dass es in dem Fall viel komplizierter war. Grad sowas wie Parent Thread muss es an die erzeugten Threads weitergeben. Und dann gibts auch noch Cross-Thread Calls, was will ich in dem Fall überhaupt haben? Und dann könnte es noch passieren, dass der ursürüngliche Thread früher beendet wird, als irgendwelche Threads, die noch diese Objekte benutzen, was mache ich damit? Hab im Endeffekt keine brauchbare Lösung gefunden.



  • Ich halte es etwas fraglich einen stringstream mittels throw zu werfen. Nach diversen Coding-Guidelines sollte nur std::exception oder abgeleitete Typen geworfen werden. Ausserdem bietet std einige vorgefertigte an, z.B. runtime_error oder logic_error. Und wenn es sich hier um die Authentifizierung handlet, dann kann diese schonmal fehlschlagen, und es ist zu ueberlegen, ob eine Exception gerechtfertigt ist.

    Ansonsten bin ich dazu uebergegangen nicht nur zu loggen, sondern meine Applikationen zu ueberwachen. D.h. Schluesselindikatoren fuer Performance sind in shared memory abgelegt und werden durch einen anderen Prozess zu ausgewerten. Dazu zaehlt auch eine In-Shared-Memory-Console mit 128 MByte. Ein kleiner Serviceprozess haelt diese dauerhaft offen. Es wird Monostate-Pattern benutzt. Selbst halte ich nicht viel von LogManager, TextureManager: link



  • @SideWinder
    Ja kommt drauf an was man unter "dem Logger" versteht. Das Ding wo im Prinzip nur drinnen steht wie der Logger heisst und vielleicht ab welchem Log-Level er denn loggt, das kann ruhig statisch sein.

    Im Prinzip geht es um die "Sink" von der dann letzten Endes die Messages rausgeschrieben (bzw. halt allgemein: konsumiert) werden. Diese, sowie diverse Settings (aktiver Log-Level, vielleicht noch Formatierung, diverse Zusatzinfos die mit jeder Zeile rausgeloggt werden sollen, ...) sollte IMO zumindest thread-local sein.



  • Mechanics schrieb:

    Ja, ich bin neulich auch über ein ähnliches Problem gestolpert. Nicht mit Loggern, aber andere Objekte, die so ähnlich überall benutzt werden. Hab mir zuerst gedacht, ganz einfach, steck die Teile ins TLS. Und dann nach paar Minuten Nachdenken gemerkt, dass es in dem Fall viel komplizierter war.

    Japp. Alles nicht so einfach 🙂

    Mechanics schrieb:

    Grad sowas wie Parent Thread muss es an die erzeugten Threads weitergeben. Und dann gibts auch noch Cross-Thread Calls, was will ich in dem Fall überhaupt haben?

    Naja, bei Cross-Thread Calls will man üblicherweise dass das Ding vom Caller temporär das Ding vom Callee "überlagert".

    Mechanics schrieb:

    Und dann könnte es noch passieren, dass der ursürüngliche Thread früher beendet wird, als irgendwelche Threads, die noch diese Objekte benutzen, was mache ich damit?

    Mögliche Ansätze:

    1. Ref-Counting (ob über shared_ptr oder wie auch immer), idealerweise dann in Verbindung mit "immutable"
    2. Cloneable

    Mechanics schrieb:

    Hab im Endeffekt keine brauchbare Lösung gefunden.

    Ja. Eines der Probleme is auch dass es keinen Standard gibt, und es daher nicht von diversen Libs unterstützt wird. Weil es sollte ja natürlich im Idealfall alles automatisch gehen, also wenn man nen Work-Item über Library X in Pool Y queued, dann sollte das Ding da mitwandern.

    Und wenn die Library X dann keine passenden "Erweiterungspunkte" hat, wo man sich einklinken könnte, dann muss man wieder wrappen oder im schlimmsten Fall an den Stellen wo 'was wandern sollte wieder alles mit Hand machen.



  • knivil schrieb:

    Ich halte es etwas fraglich einen stringstream mittels throw zu werfen. Nach diversen Coding-Guidelines sollte nur std::exception oder abgeleitete Typen geworfen werden. Ausserdem bietet std einige vorgefertigte an, z.B. runtime_error oder logic_error.

    Ja das werfen von stringstream ist in der tat keine gute Idee und muss noch geändert werden.

    knivil schrieb:

    Und wenn es sich hier um die Authentifizierung handlet, dann kann diese schonmal fehlschlagen, und es ist zu ueberlegen, ob eine Exception gerechtfertigt ist.

    Das ist immernoch ein kompliziertes Thema für mich, obwohl ich mich intensiv damit beschäftigt habe ....Rückgabewerte vs Exceptions. Da gibt es scheinbar viele Meinungen und ob meine Umsetzung ok ist, ist fraglich. Wenn man sich fremde Codeprojekte anschaut findet man leider keine Lösung die von vielen Entwicklern bevorzugt werden. ....oder meine Sicht der Dinge ist verzerrt. ...wer weiß.

    In 10 Jahren kann ich über meine Foreneinträge bestimmt nur noch müde lächeln (hoffe ich).



  • Nö, ist nicht schwierig ob Exception oder Rückgabewert.

    checkPassword(string pwd);
    

    Exception oder Rückgabewert? Rückgabewert, z.B. Boolean. Denn ein falsches Passwort ist nicht unerwartet und kein Ausnahmefall. Wird ja explizit danach geprüft.

    tryLogin(string user, string pwd);
    

    Rückgabewert, weil der Versuch des Logins erfolgreich oder fehlschlagen könnte, das sagt die Funktion aus. Ob da ein gültiges Login-Objekt, Session-Objekt o.ä. zurück gegeben wird, oder ein Boolean, ist ein anderes Thema.

    login(string user, string pwd);
    

    Exception, weil der Funktionsname nichts von einem möglichen Fehlschlag erzählt.

    Eine Exception ist für unerwartete Ausnahmesituationen. D.h. auch das vorletzte Beispiel (tryLogin) könnte eine Exception werfen. Sie könnte z.B. eine LostConnection-Exception werfen, etwas was unerwartet ist und mit dem Login selbst nichts zu tun hat.
    Diese Ausnahme würde aber für alle anderen genannten Funktionen auch gelten!

    Wenn du einen gültigen Printer-Handler hast, und du in einem zweiten Schritt ein Dokument mit diesem Printer-Handler drucken willst, kann der Benutzer z.B. den Drucker vor oder in diesem zweiten Schritt ausschalten. Bamm! Runtime-Exception! Drucker nicht erreichbar!
    http://www.cplusplus.com/reference/stdexcept/runtime_error/

    Bei tryLogin und login könnte man auch eine invalid_argument-Exception werfen, wenn der Username ungültige Zeichen hat.
    http://www.cplusplus.com/reference/stdexcept/invalid_argument/

    Runtime-Exceptions (out of memory u.ä.) sind Fehler, für die der Aufrufer nichts kann.
    Logic-Exceptions (invalid arguments u.ä.) sind Fehler, für die der Aufrufer etwas kann.

    Ich würde aber immer versuchen zu spezialisieren, also z.B. invalid_username_chars. Dann kann man gezielter mit einem Catch darauf reagieren. Oder wenn es nicht abgefangen wird, kann man zumindest den Exception-Typ später heraus finden.

    http://www.cplusplus.com/reference/exception/exception/



  • hustbaer schrieb:

    Mechanics schrieb:

    Grad sowas wie Parent Thread muss es an die erzeugten Threads weitergeben. Und dann gibts auch noch Cross-Thread Calls, was will ich in dem Fall überhaupt haben?

    Naja, bei Cross-Thread Calls will man üblicherweise dass das Ding vom Caller temporär das Ding vom Callee "überlagert".

    Oder auch nicht 😉 Ich weiß es wirklich nicht. Kann sein, dass Thread A etwas macht und ein Objekt hat, das in Thread B "lebt" und an diesen Thread gebunden ist (muss aber nicht sein, da haben wir verschiedene Möglichkeiten, so ähnlich wie Apartment Models). Dann geht der Aufruf in den anderen Thread rüber und wird dort ausgeführt. Und das Objekt macht vielleicht irgendwas, was ich nicht wirklich im Griff habe (wir haben zig Millionen Zeilen Code, "keine Ahnung", wer was warum macht) und will dann einfach "X" benutzen. Vielleicht wurde es im Caller Thread mit Absicht überschrieben und der Callee sollte tatsächlich das nutzen. Kann aber auch sein, dass es einen Grund gibt, warum das Objekt an seinen Thread gebunden ist und vielleicht wurde "X" auch in diesem Thread aus gutem Grund überschrieben und dem Caller ist es hingegen relativ egal. Das kann ich tatsächlich nicht wirklich entscheiden.
    Technische Probleme, wie man sowas rüberreicht und irgendwelche fremden Libs miteinbezieht natürlich inbegriffen


Anmelden zum Antworten