Wie strukturiere ich am besten meine Klassen/Projektmappe

  • C#
  • .NET (FX) 3.0–3.5

Es gibt 25 Antworten in diesem Thema. Der letzte Beitrag () ist von Fortender.

    Wie strukturiere ich am besten meine Klassen/Projektmappe

    Hallo,
    ich arbeite mich seit kurzer Zeit in die objektorientierte Programmierung ein und habe nun ein für mich großes Problem, da
    ich nicht weiß wie ich meine Projekte/Klassen richtig strukturieren soll bzw. wann eine Klasse überhaupt angebracht ist.

    Da es an einem Beispiel wohl einfacher zu erklären ist, zu meinem Beispiel:
    Meine Grundfunktion die Sinusbot-API anzusprechen und die Results auszuwerten steht.
    Allerdings werden im Body der HttpRequests Json Objekte versendet und so brauche ich die in der API Dokumentation angegebene Struktur
    um Parameter Json-serialisiert mitzuliefern und Json-serialisierte results zu empfangen.

    Nehmen wir uns aus der API mal den Punkt General heraus. Hier fängt es bei mir schon an. Überlegt habe ich mir ein solches Schema:
    Spoiler anzeigen

    C#-Quellcode

    1. ​namespace Sinusbot.Core.JsonClasses
    2. {
    3. class General
    4. {
    5. public class Information
    6. {
    7. public const string RequestMethod = "GET";
    8. public const string RequestPath = "api/v1/bot/info";
    9. public class Success
    10. {
    11. public object bot { get; set; }
    12. public object system { get; set; }
    13. public string[] codecs { get; set; }
    14. public string[] formats { get; set; }
    15. public float usageMemory { get; set; }
    16. }
    17. }
    18. public class DefaultBot
    19. {
    20. public const string RequestMethod = "GET";
    21. public const string RequestPath = "api/v1/botId";
    22. public class Success
    23. {
    24. public string defaultBotId { get; set; }
    25. }
    26. }
    27. public class Login
    28. {
    29. public const string RequestMethod = "POST";
    30. public const string RequestPath = "api/v1/bot/login";
    31. public class Params
    32. {
    33. public string username { get; set; }
    34. public string password { get; set; }
    35. public string botId { get; set; }
    36. }
    37. public class Success
    38. {
    39. public string token { get; set; }
    40. public string botId { get; set; }
    41. }
    42. }
    43. }
    44. }

    Ich habe ein solches Schema gewählt um mich nah an der API zu bewegen, aber hier liegt eben meine Befürchtung, dass diese Vorgehensweise falsch ist.
    Die Klasse General hat ja im Prinzip rein gar nichts mit den untergeordneten Klassen zu tun. Das würde nur zur Struktur dienen um nah an der API Notation dran zu bleiben.
    Gibt es ordentliche Guides wo man lernt wie man sich ordnungsgemäße Klassenstrukturen zusammenstellt?

    MfG Tim
    Du verschachtelst Klassen, sogar mehrfach.
    Das hat so gut wie nie einen Sinn.
    Klassen verschachteln hat keine Auswirkung auf ihre Sichtbarkeit oder Funktionalität, sondern hat einzig eine "Namespace-Wirkung", also der vollqualifizierte Name einer eingeschachtelten Klasse wird um die entsprechenden Segmente länger - ist das dein Ziel?

    C#-Quellcode

    1. var login = new Sinusbot.Core.JsonClasses.General.Login.Success()
    Also ich finde,

    C#-Quellcode

    1. var login = new Sinusbot.Core.JsonClasses.LoginSuccess()
    wäre doch lang genug, und führt sicherlich ebensowenig zu Namens-Konflikten (Vermeidung von Namenskonflikten ist ja der Sinn der Namespacerei)

    Insbesondere die General-Klasse ist hier nichts anderes als ein Namespace - sie enthält ja keinen Member.
    Das ist vlt. was du grad verwechselst: das Konzept "Klassen-Member" mit dem Konzept "Namespace/SchachtelKlasse".
    Eine eingeschachtelte Klasse ist kein Klassen-Member.

    Edit: Jetzt habich die Api-Dokumentation angeguckt, nu scheint mir ein General-Namespace sinnvoll - das wäre dann entlang der Strukturierung der ApiBot-Doku.
    Wie gesagt: Als Namespace, denn General ist keine Klasse, die iwelche Member enthielte, und von der man Objekte erstellte.
    Infragestellen würde ich die Notwendigkeit des Core-Namespaces - gibt es innerhalb von Sinusbot, jedoch ausserhalb von Core so immens viel Funktionalität und Zuständigkeiten, dass man die Übersicht verliertete, wenn die einfach unter Sinusbot zusammen-subsummiert wären?

    Ein ganz generelles Grundprinzip, wie man Code unübersichtlich machen kann, ist nämlich, alles mögliche möglichst tief ineinander zu verschachteln.
    Womit ich nicht prinzipiell gegen Verschachtelung rede: Wird eine Einheit (etwa durch umfang) unübersichtlich, so gewinnt man Übersicht teilweise zurück, indem man sie (durch Einzug einer Schachtel-Ebene) unterteilt.
    Nur wenn die Einheit garnet unübersichtlich war, dann verliert man durch die unnütze Unterteilerei ein Stück Übersichtlichkeit.

    Edit Edit:
    wo ich länger drüber nachdenke, gefällt mir das mit deiner Params/Success - Einschachtelei immer besser.
    Dadurch kann ein Login-Success und ein Information-Success gleich heissen, was ja auch vom Concern her auch dasselbe ist, und es sind trotzdem ganz andere, unabhängig Klassen, was hier ja sinnvoll zu sein scheint.
    Also General -> Namespace, Core infragestellen, aber sonst findich das Design plausibel. :thumbsup:
    bekommen Information, DefaultBot, Login noch weitere Member?

    Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „ErfinderDesRades“ ()

    ErfinderDesRades schrieb:

    Also General -> Namespace, Core infragestellen, aber sonst findich das Design plausibel.

    Mit Core wollte ich mir was bei anderen abschauen. Core ist aber wie du schon erörtert hast unsinnig.
    Habe ich die Ordner/Namespace-Struktur jetzt so richtig angepasst:
    Sinusbot.JsonClasses.General wäre dann mein Namespace. In diesem liegen dann die Klassen Information, DefaultBot, Login. Würdest du empfehlen die Klassen einzeln in
    jeweils eine Datei zu schreiben? Also dann Information.cs, DefaultBot.cs, Login.cs?

    ErfinderDesRades schrieb:

    bekommen Information, DefaultBot, Login noch weitere Member?

    In den Klassen sind nur die 2 Konstanten drin, die für den HttpWebRequest benötigt werden.

    MfG Tim
    Dasselbe Grundprinzip: übermäßiges Unterteilen verringert Übersichtlichkeit.
    Solange die Dateien nicht auf > 100 Zeilen anwachsen, geht der Kram gut in eine. Zumal die ja thematisch sehr ähnlich sind, da versteht man das hinterliegende Schema auch besser, wenn man die auch zusammen in einem Blick hat.

    Ich mach immer DateiHeader über meine Dateien, in denen ich gradezu ausschweifend meine Überlegungen dazu auseinandersetze.
    Diese File-Header sind bei mir die einzigen Regionen, die ich verwende, dass man das Gelaber auch zusammenklappen kann.

    Code klapp ich nie zusammen, denn Code muss man sehen.
    Was ich dir auch empfehle, ein zeilensparendes Editor-Layout einzurichten, dass du wirklich auch alles auf einen Blick haben kannst - File Sinusbot\JsonClasses.General.cs sähe dann so aus:

    C#-Quellcode

    1. #region FileHeader
    2. #if false
    3. hier ausschweifend erklären, und darauf hinweisen, dass diese Datei 7(!!) Klassen enthält, und was du dir dabei gedacht hast
    4. #endif
    5. // Usings
    6. using System;
    7. using System.Collections.Generic;
    8. using System.Linq;
    9. using System.Text;
    10. using System.Threading.Tasks;
    11. #endregion //FileHeader
    12. namespace Sinusbot.JsonClasses.General {
    13. public class Information {
    14. public const string RequestMethod = "GET";
    15. public const string RequestPath = "api/v1/bot/info";
    16. public class Success {
    17. public object bot { get; set; }
    18. public object system { get; set; }
    19. public string[] codecs { get; set; }
    20. public string[] formats { get; set; }
    21. public float usageMemory { get; set; }
    22. } //Success
    23. } //Information
    24. public class DefaultBot {
    25. public const string RequestMethod = "GET";
    26. public const string RequestPath = "api/v1/botId";
    27. public class Success {
    28. public string defaultBotId { get; set; }
    29. } //Success
    30. } //DefaultBot
    31. public class Login {
    32. public const string RequestMethod = "POST";
    33. public const string RequestPath = "api/v1/bot/login";
    34. public class Params {
    35. public string username { get; set; }
    36. public string password { get; set; }
    37. public string botId { get; set; }
    38. } //Params
    39. public class Success {
    40. public string token { get; set; }
    41. public string botId { get; set; }
    42. } //Success
    43. } //Login
    44. }
    Nach Gutdünken habich auch mal Leerzeilen spendiert - muss nichtmal sein - bei c# finde ich die zeilen mit } meist leer genug.
    Beachte auch die Orientierungs-Kommentare am Ende der Klassen - die sind hier schon nütze.
    Insbes. wenn man den FileHeader zuklappt geht der komplette Code auf einen Bildschirm und ist sogar noch bischen Platz übrig.
    Solche Sachen (Leerzeilen, Orientierungs-kommentare) mach ich wie gesagt nach GutDünken, meist in ungewöhnlichen Dateien, weil ist schon unüblich, mehrere Klassen in eine Datei zu stopfen.

    Beachte auch Pfad und Dateinamen - hier gibts einen Ordner Sinusbots, und darin eine Gruppe von JsonClasses.<Blabla>.cs-Dateien.
    Kann auch anders strukturiert sein, dass Sinusbots\JsonClasses ein eigener Ordner wird - dann wären die Dateinamen entsprechend kürzer.

    Dieser Beitrag wurde bereits 8 mal editiert, zuletzt von „ErfinderDesRades“ ()

    Vielen Dank für deine Hilfe.
    Ich hänge mal meine Projektmappe an. Ich würde mich freuen, wenn du dir das mal ansehen und mir evtl. noch ein paar Tipps geben könntest :thumbsup:
    Ist noch lange nicht fertig, aber mir geht es in erster Linie um Codedesign und Struktur.
    Klickste hier
    MfG Tim
    jo, hab bischen drin rumgefuhrwerkt.
    Aber eiglich viel zu verbessern fand ich garnet :thumbsup:
    eine StreamX-Klasse hab ich noch angedeihen lassen.

    Mit den General-Dingern hab ich versucht, Importe zu nutzen, aber da tun sich Namens-Konflikte auf, etwa der Bot hat eine Methode LogIn, im General-Namespace gibts eine Klasse LogIn, da kann man dann den Namespace-Import nicht nutzen.

    Aber findich alles nicht so gravierend oder iwie merklich überverkompliziert. Ist ja auch ein kleines Projekt, und mit nur einer Assembly.

    Wie gesagt: Viel Leer-Raum-Verschwendung wird vermieden, wenn man einfach den c#-Editor umstellt aufs zeilensparende Layout. Das geht inne VS-Optionen.
    Dateien
    • SinusbotUI00.zip

      (19,18 kB, 271 mal heruntergeladen, zuletzt: )
    @ErfinderDesRades
    Ich hätte da noch eine Frage bezügl. des Stils.
    Ich hab doch die Requests/Responses mit:

    C#-Quellcode

    1. public static class JsonHttpHelper {
    2. public static string GetRequestedJsonString(Uri requestUri, string requestMethod, object requestParams = null, params string[] headerAdditions) {
    3. var httpWebReq = HttpWebRequest.Create(requestUri);
    4. httpWebReq.Method = requestMethod;
    5. httpWebReq.ContentType = "application/json";
    6. foreach(var h in headerAdditions) httpWebReq.Headers.Add(h);
    7. if (requestParams != null) //Gegebenenfalls Parameter mitliefern
    8. httpWebReq.GetRequestStream().WriteAllText(JsonConvert.SerializeObject(requestParams));
    9. var httpWebRes = (HttpWebResponse)httpWebReq.GetResponse();
    10. if ((int)httpWebRes.StatusCode != 200) return null; //Noch nicht fertig: Error-Codes
    11. return httpWebRes.GetResponseStream().ReadAllText();
    12. }
    13. public static T GetRequestedJsonOf<T>(Uri requestUri, string requestMethod, object requestParams = null, params string[] headerAdditions) {
    14. return JsonConvert.DeserializeObject<T>(GetRequestedJsonString(requestUri, requestMethod, requestParams, headerAdditions));
    15. }
    16. }

    gekapselt.
    Allerdings hab ich ja in meinen JsonClasses extra eine Success und Error Klasse angelegt. Mit der gezeigten Kapselung ist es aber nicht möglich Success ODER Error
    zu bekommen. Das ist ziemlich doof, da ich damit nicht so flexibel bin schon in der Kapselungsmethode richtig zu Deserialisieren.

    Wie geht man in solchen Fällen vor? Ich würde spontan sagen man gibt den StatusCode (der laut API 200=Success, 4xx=Error entspricht) und den ResponseStream als JsonString aus um dann
    JsonConvert.DeserializeObject<T> den entsprechenden Typ mitzugeben.
    Allerdings resultiert daraus wieder eine Frage bezügl. des Stils. Erstellt man sich für den Rückgabewert einer solchen Funktion dann eine extra Struktur oder Klasse oder nimmt man da z.b. Tuple?

    Ich weiß.. Hört sich alles so an als wäre ich n richtiger Anfänger, aber ich möchte halt einfach erstmal weitgehend alles richtig machen. Deshalb solche Fragen :|

    MfG Tim
    Bezeichnungen wie Core machen nur für Grund, Kern oder Basis Programme sind. JSON Dokumente Strukturiert man am besten so wie sie sind in Namespaces wo bei man unter bestimmten Punkten wie Name / ID bei der Facebook-API auch von einer Basis Klasse Ableiten kann da die Klasse immer gebraucht wird da viele Arrays in der Faceook API (als Beispiel) Name / ID Wert Pare sind und das Profiel selber diese eigenschaften auch hat weswegen man diese NameID Klasse von Profiel aus gesehen ableiten kann.

    Klassen Verschachteln macht wenig sinn und erinert an vb6. Da wie meine vorposter schon gesagt Namespaces verwenden.
    Ja das hat der EDR ja schon geschrieben. Hab das schon verinnerlicht. Schwer von Begriff bin ich nicht :P Aber danke für die Wiederholung :P
    Schade, dass du zu meinen neuen Fragen nicht geantwortet hast, sonst hätte mir der Post evtl. weiterhelfen können.
    Das Problem ist, dass ich so der JsonConvert.DeserializeObject<T> den Typ zum Deserialisieren übergeben muss. Ich bräuchte also 2 mehr Parameter um die Typen mitzugeben:
    ​Type SuccessType, Type ErrorType und dann anhand des StatusCodes entscheiden, welchen Typ ich zum Deserialisieren brauche in der Situation.
    Aber mir scheint das alles nicht so der gute Stil zu sein. Dann bräuchte ich noch den von dir vorgeschlagenen out StatusCode Parameter.

    MfG Tim
    Schau dir die API mal an. Success und Error sind in vielen Fällen unterschiedlich aufgebaut und haben unterschiedliche Rückgabetypen.
    Sonst hätte mir der EDR wahrscheinlich in seinem ersten Post schon den Vogel gezeigt, dass ich für Success und Error extra 2 Klassen habe und
    nicht eine allgemeine Result-Klasse. Die Rückgabeinformationen möchte ich eben vollständig verwerten, ansonsten hätte ich auch einfach den StatusCode verwenden können.
    verstehe ich das richtig - man weiß garnet sicher, ob man ein serialisiertes Error-Objekt bekommt oder ein Success-Objekt?
    Und richtige Verbindungs-Fehler können ja auch immer auftreten.
    Ich glaub da würde ich Try-Error-Retry-Thow machen.
    Also erst normal zu deserialiseren versuchen und dann als Error-Objekt, ebenfalls mit TryCatch, denn kann ja auch schief gehen.
    Und dann halt differenzierte Exception werfen.
    Die muss dann wohl auch behandelt werden, aber das würd ich mir für später aufheben, wenn alles soweit läuft, und wenn man dann auch weiß, wie man drauf reagieren will.
    Ist auch vmtl. lausig zu testen (und damit zu entwickeln), man muss ja alle möglichen Fehler ühaupt erstmal reproduzieren.
    Oder das Fehler-Thema halt komplett vertagen: Eintrag in die ToDo-Liste und fehlerfrei weitercoden, und sich den Fehlern erst widmen, wenn sie auch kommen.
    Ich hab mich ja mal ausschweifend zum Thema verbreitert: TryCatch ist ein heißes Eisen
    Naja. Der Entwickler hat in der API dokumentiert, dass ein Success immer den StatusCode 200 zurückgibt und Error 4xx...
    Das heißt bei 400 <= Statuscode < 500 oder wenn eine WebException gethrowt wird, müsste unter umständen das zugrundeliegende
    Errorobjekt ausgegeben werden.

    Ich muss doch an die Deserializer-Funktion den Typ mitliefern. Es gibt aber zwei Typen in dem Fall. Success und Error. Ich würde das ja gerne Kapseln, dass ich
    nur einen Typ/Objekt zurückgebe "Result" welcher dann Success oder Error enthält. Aber wie ist das programmiertechnisch am besten zu lösen?

    MfG Tim
    Man kann von beiden Typen, Success und Error eine Instanz in einen Super-Typen stopfen - hat thuCommix ja schon gesagt.
    Ist bisserl mehr Aufwand, und ist auch nervig, denn den Supertypen kannste nicht einfach benutzen, sondern musst immer erst reingucken, ob der Success-Member überhaupt gültig ist.
    Also letztendlich biste so oder so immer am rumprobieren (engl: Try ;) )
    Und wenn ich recht verstehe, gibt es insgesamt nur einen Error-Typen, und der enthält auch nix anneres als eine Nummer, und zwar zw 400 und 499.
    Wie gesagt: bastel dir eine Exception-Klasse, die diese Nummer transportiert, oder auch eine komplexere Exception, oder mach was mit Re-Throwing und InnerException.
    Aber treib nicht zuviel Aufwand damit, denn mit Fortschritt der Entwicklung wirst du vmtl. eh alles wieder umschmeissen.
    Und das mit dem SuperTypen gefällt mir garnet, weil
    1. erleichtert das überhaupt nichts, im Gegenteil - da muss man nur noch mehr ifse einbauen
    2. Hört die Exception damit auf, eine Exception zu sein, und wird wie ein normales, nur anderes Ergebnis verarbeitet.
      Aber ich finde, wenn der Server dir einen Error liefert, ist das eine Exception.
    Ja was mir übrig bleibt hab ich ja jetz verstanden. Mir geht es aber um den "SuperTypen". Ich habe ja für jede Funktion die ich requeste eigene Success und Error Klassen.
    Die unterscheiden sich von den Success/Error Klassen anderer. Ich würde mir dann eine Klasse/Struktur anlegen die jeweils den Success und Error Typ beinhalten muss.

    C#-Quellcode

    1. public class ResultTypes {
    2. public Type SuccessType { get; set; }
    3. public Type ErrorType { get; set; }
    4. public ResultTypes(Type success, Type error) {
    5. SuccessType = success;
    6. ErrorType = error;
    7. }
    8. }

    Das wird dann eben an die Methode übergeben die das Ganze deserialisieren soll. Weil für DeserializeObject gibt es noch einen Type Parameter.

    C#-Quellcode

    1. public object DeserializeJsonString(string jsonString, int statusCode, ResultTypes resType) {
    2. if (statusCode == 200)
    3. return JsonConvert.DeserializeObject(jsonString, resType.SuccessType);
    4. else
    5. return JsonConvert.DeserializeObject(jsonString, resType.ErrorType);
    6. }


    Ist halt alles blöd...