Laden...

Konstruktor überladen - das effizienteste Vorgehen für meine App?

Erstellt von GeneVorph vor 6 Jahren Letzter Beitrag vor 6 Jahren 2.653 Views
G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 6 Jahren
Konstruktor überladen - das effizienteste Vorgehen für meine App?

Hallo,

ich suche Vorschläge/Anregungen/Rat:
ich habe mir als Programmierübung folgende Aufgabe gestellt: eine Win-Forms-Anwendung, mit der Schulklassen erstellt und bearbeitet werden können.

Weil es darum um das Programmierenlernen geht, ist der Aufbau recht simpel:
Ich habe eine Haupt-Form mit einer ComboBox, in der verschiedene Kürzel für Schulklassen einer Schule enthalten sind, z. B. "P 2B". Das Befüllen der ComboBox und ihre Aufgabe sind für meine Frage aber nebensächlich. Zudem hat die Form einen Menu-Strip, mit dem ich über Datei-->Neu, bzw. Bearbeiten --> Klasse bearbeiten eine neue Schulklasse anlegen, bzw. eine bestehende bearbeiten kann.

Um eine Schulklasse anzulegen brauche ich drei Angaben: Name der KLassenleitung, Klassenkürzel und Partnerklassenname. Diese Parameter können auf der zuständigen Nebenform (classNewOrEdit) jeweils per TextBox eingegeben werden.
Der Name der Form zeigt schon, dass ich Klassen damit anlegen möchte (New), aber auch bearbeiten möchte(Edit); dazu würden dann später mal Daten aus einer Datenbank ausgelesen, die zu dem Kürzel der ComboBox auf der Hauptform gehören.

Sämtliche Tasks, die eine Schulklasse betreffen können, habe ich in einer Enumerable Klasse untergebracht, hier der Code:


public enum MenuItemTasks
    {
        newClass = 1,
        editClass = 2,
        deleteClass = 3       
    }

Soweit so gut.

  1. Fall: eine neue Schulklasse erstellen
    Auf der Hauptform im Menu-Strip: Datei-->Neu

private void newClassMenuEntry_Click(object sender, EventArgs e)
        {
            var task = MenuItemTasks.newClass;

            var newClass = new classNewOrEdit(task);
            newClass.ShowDialog();
        }

In der Nebenform gibt es eine Methode, die vom Konstruktor aus aufgerufen wird, und die als Argument den Enumerable task entgegennimmt und in einem Switch-Statement auswertet:


 private void SetForm(MenuItemTasks taskName, string SchoolClassLabel)
        {
            switch(taskName)
            {
                case MenuItemTasks.newClass:
                   
                    this.Text = "Klasse anlegen";
                   
                    this.btnNewOrEdit.Text = "Klasse erstellen";
                    break;

                case MenuItemTasks.editClass:
                   
                   this.Text = "Klasse " + SchoolClassLabel + " bearbeiten";
                                     
                    this.btnNewOrEdit.Text = "OK";
                    break;
            }
        }         

Hier geht es darum, die Form entsprechend zu benennen, bzw. den "OK"-Button, der je nach Task entweder mit "klasse erstellen" beschriftet sein soll oder schlicht mit "OK".

  1. Fall: Schulklasse bearbeiten
    In der Hauptform im Menu-Strip: Bearbeiten --> Klasse bearbeiten

private void editClass_MenuEntry_Click(object sender, EventArgs e)
        {
            var task = MenuItemTasks.editClass;

            var editClass = new classNewOrEdit(task, cmbSchoolClassLabels.Text);
                                  
            editClass.ShowDialog();
        }

Wie man unschwer erkennen kann, werden hier beim Aufruf der Klasse classNewOrEdit 2 Parameter übergeben: task, sowie der ComboBoxText, weil ja hier u. a. der Klassenname bearbeitet werden soll (Klassenleitung und Partnerklasse folgen später einmal).

Dazu habe ich in der Nebenform 2 Konstruktoren:


public classNewOrEdit(MenuItemTasks taskName)
        {
            InitializeComponent();
            SetFormNew(taskName, "new");
        }

public classNewOrEdit(MenuItemTasks taskName, string SchoolClassLabel)
        {
            InitializeComponent();
            SetForm(taskName, SchoolClassLabel);
        }

OK, mein Code tut das was er soll, aber mit folgendem bin ich NICHT zufrieden:

  1. Die Funktion SetForm() nimmt 2 Parameter an: einen Enumerable (taskName), um im Switch-Statement zu prüfen, ob die Klasse neu angelegt oder lediglich bearbeitet werden soll. Dann ein String (das Klassenkürzel): dieses wird aber nur beim zweiten Konstruktor überhaupt übergeben, weil ja beim neu anlegen einer Klasse noch kein Kürzel existiert. Das zwingt mich dazu beim Aufruf im ersten Konstruktor einen Dummy-String zu übergeben ("new"), der natürlich genausogut leer oder null sein könnte. Das würde ich möglichst gerne vermeiden!

  2. Ich habe in der Nebenform 2 Überladungen für meinen Konstruktor, die aber - prinzipiell - denselben Code ausführen. Da sag ich nur: DRY! Finde ich sehr unschön.

Und zuletzt 3: mir gefällt schon nicht, dass ich beim Aufruf der Nebenform zumindest theoretisch für den Edit den Parameter des Klassenkürzels vergessen könnte - und trotzdem würde mein Programm laufen, weil dadurch der falsche Konstruktor aufgerufen würde (OK, später würde es bei der Methode SetForm() crashen, weil das Klassenkürzel null wäre...).

Meine Bitte/Frage: wie kann ich meinen Code optimieren? Gibt es eine, hm, elegantere Lösung in Hinblick auf Code-Reusability, DRY usw.?

viele Grüße
Vorph

3.003 Beiträge seit 2006
vor 6 Jahren

Vorab: bitte enums nicht Enumerable nennen, auch wenn es das bedeutet. Enumerable ist in .net wieder was anderes.

Zum zweiten: du überlässt der Form (classNewOrEdit) der Kontrolle über ihren Zustand. Der ist aber Anwendungslogik: wird gerade editiert oder neu erstellt, und wenn editiert, was wird editiert?

Trenn das sauber auf ( [Artikel] Drei-Schichten-Architektur ) und dein Problem erledigt sich ganz von alleine.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 6 Jahren

Vorab: bitte enums nicht Enumerable nennen, auch wenn es das bedeutet. Enumerable ist in .net wieder was anderes.

Ah - OK, ich muss zugeben, das war mir so nicht bewusst; danke.

Zum zweiten: du überlässt der Form (classNewOrEdit) der Kontrolle über ihren Zustand. Der ist aber Anwendungslogik: wird gerade editiert oder neu erstellt, und wenn editiert, was wird editiert?

Sorry - da muss ich kurz nachhaken, ich hab glaub ich nicht ganz verstanden was du meinst.
Also:

...du überlässt der Form (classNewOrEdit) der Kontrolle über ihren Zustand. Der ist aber Anwendungslogik: wird gerade editiert oder neu erstellt...

Genau das geschieht doch in meinem Falls mit den Enums, oder? Auf der Form wird ausgewertet, ob der Aufruf zum Zweck der Neuerstellung oder des Editierens erfolgt. Ich vermute, das soll bedeuten, es müsste noch eine Klasse dazwischengeschaltet werden? Falls ja: habe ich dann nicht wieder dasselbe Problem, dass ich - je nach Anwendungsfall - der Form zwei Konstruktoren verpassen muss? Irgendwann muss ich ja Farbe bekennen und für den Fall B auch einen Schulklassennamen übergeben.

Ich könnte natürlich auf Properties anlegen, die von der Hauptform aus befüllt werden. Aber das hat dann auch irgendwie einen faden Beigeschmack, weil ich die Properties als Instanziierungsparameter missbrauche.

Ich hatte mir jetzt noch überlegt, dass es doch möglich sein müsste, in meiner SetForm()-Methode im "Editier"-Teil ein Event zu raisen, bei dem eine Methode in Form1 abonniert und dann den Text aus der ComboBox einfach übermittelt...auch wenn ich gerade nicht weiß, wie ich das im Einzelnen mache, aber theoretisch müsste das auch gehen, oder?

Trenn das sauber auf (
>
) und dein Problem erledigt sich ganz von alleine.

LaTino

An welcher "Stelle" müsste ich da auftrennen? Danke für deine Antwort - ich kenne den Artikel über die Drei-Schichten-Architektur bereits, aber ich werde mir das auf jeden Fall nochmal ansehen.

lg
Vorph

3.170 Beiträge seit 2006
vor 6 Jahren

Hallo,

zunächst mal möchte ich LaTino beipflichten.

Und dann noch einen Hinweis nachschieben zu überladenen Konstruktoren, da das auch konkret ein Teil der ursprünglichen Fragestellung war (Stichwort DRY): Du kannst von einem überladenen Konstruktor auch einen anderen aufrufen.
In Deinem Fall (auch wenn es hier nicht wirklich angebracht ist) würde das so aussehen:

public classNewOrEdit(MenuItemTasks taskName): this(taskName, "new")
        {
        }

public classNewOrEdit(MenuItemTasks taskName, string SchoolClassLabel)
        {
            InitializeComponent();
            SetForm(taskName, SchoolClassLabel);
        }

Ich sehe das Problem hier aber auch eher in der Architektur - und wenn Du schon auf DRY anspielst, dann denk auch an das Single-Responsibility-Prinzip, Deine Form hat nämlcih jetzt definitiv mehrere Verantwortlichkeiten.

Gruß,MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 6 Jahren

Hallo MarsStein,
vielen Dank für deine Antwort - das bringt mich jetzt eine ganze Ecke weiter!

Zunächstmal: das mit dem Single Responsibility-Prinzip saß mir die ganze Zeit schon im Nacken! Ich hatte genau diese Frage hier schon mal gepostet, bezüglich dessen, ob ich eine Form für 2 Aufgaben nutzen kann, wenn sie von der GUI her genau gleich daherkommt, lediglich einmal für das Editieren und einmal für das Neuerstellen benutzt wird. Damals wurde mit DRY argumentiert. Ich denke, vielleicht kann man hier tatsächlich gegeneinander abwägen, das weiß ich halt nicht, ob man SRP DRY gegenüber vorzieht. Gibt es da eine Hierarchie? Eine New-Form, eine Edit-Form - klar, damit löst sich das Problem in Wohlgefallen auf.

Im Übrigen: ich wollte LaTina ja nicht in Abrede stellen - ich bin mir sicher, dass mein Schichtmodell Fehler hat. Trotzdem: als Hobby-Coder, der sich von Verständnisbröckchen zu Verständnisbröckchen hangelt: wo genau soll ich denn meine Schichten hier auftrennen?

Und: verstehe ich das richtig: meine Logikschicht beeinflusst meine Präsentationsschicht? [Weil: Programmlogik entscheidet über Editieren/Neuerstllen, als 2 verschiedene Aufgaben, und diese Logik bestimmt über die Erscheinungsform der Winform. Ist das Problem so richtig dargestellt?] Also, so wie das verbotene Beispiel im zuvorgenannten Artikel über die Layers?

OK, sagt mir bitte, dass ich auf dem richtigen Weg bin, ich glaube der Nebel lichtet sich 😁

3.170 Beiträge seit 2006
vor 6 Jahren

Hallo,

ob man SRP DRY gegenüber vorzieht. Gibt es da eine Hierarchie?

Hmm... in einer guten Umsetzung sollten sich die beiden Prinzipien gar nicht (oder nur minimal) widersprechen, sondern beide brücksichtigt werden.
Für eine Schichtentrennung, die das erlaubt, gibt es eine ganze Reihe Patterns. Für WPF gibt es das maßgeschneiderte MVVM, für eine Umsetzung in WinForms eignet sich z.B. MVP (Model-View-Presenter) ganz gut.

Als Basis hättest Du dann ein z.B. ein Model für Deine Schulklasse, und eine View die sich um die Darstellung kümmert. Dazwischen der Presenter, der sich darum kümmert, der View Daten zur Verfügung zu stellen, und auf Ereignisse der View zu reagieren (z.B. ein Event, das ausgelöst wird, wenn die Bearbeitung beendet ist).
Nun könnte man den Presenter entscheiden lassen, was für Texte im Titel/auf den Knöpfen stehen (dazu dann in de Schnittstelle der View entsprechende Properties vorsehen) oder man könnte eine EditView und eine CreateView haben, die jeweils auf einer gemeinsamen Basisklasse basieren oder oder oder... es sind nur Patterns, eine richtige Umsetzung kann auf unterschiedleiche Weise erreicht werden.

Schau Dir am besten mal ein paar Beispiele dazu an.
Es muss auch nicht MVP sein, wie gesagt gibt es eine ganze Reihe an Pattern (z.B. MVC), die unterschiedlich Ansätze der Zuständigkeiten der einzelnen Teile verfolgen, sich aber vom Prinzip her ähneln.

Schau z.B. mal Differences between MVC and MVP for Beginners
Da werden auch die Pattern nochmal erklärt.

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

3.003 Beiträge seit 2006
vor 6 Jahren

Hallo MarsStein,
vielen Dank für deine Antwort - das bringt mich jetzt eine ganze Ecke weiter!

Zunächstmal: das mit dem Single Responsibility-Prinzip saß mir die ganze Zeit schon im Nacken! Ich hatte genau diese Frage hier schon mal gepostet, bezüglich dessen, ob ich eine Form für 2 Aufgaben nutzen kann, wenn sie von der GUI her genau gleich daherkommt, lediglich einmal für das Editieren und einmal für das Neuerstellen benutzt wird.

Als Denkanstoß:


interface IModifySchoolClassPresenter
{
     public string AcceptButtonText { get; }
     public string InfoText { get; }
}
abstract class SchoolClassPresenterBase : INotifyPropertyChanged
{
     public string AcceptButtonText { get; protected set;}
     public string InfoText { get; protected set;}

    //INPC implementieren und hierfür anwenden:
     public string ShortName { get; set; }
     public string Leader { get; set; }
     public string PartnerClass { get; set; }

    protected abstract void OnSubmit();
}

class EditSchoolClassPresenter : SchoolClassPresenterBase
{
    public EditSchoolClassPresenter() 
    {
        AcceptButtonText = "Bearbeiten";
        Infotext = "Bearbeiten Sie die Klasse und klicken Sie dann auf \"Bearbeiten\"";
    }
}

class CreateSchoolClassPresenter : SchoolClassPresenterBase
{
    public CreateSchoolClassPresenter() 
    {
        AcceptButtonText = "Erstellen";
        Infotext = "Füllen Sie die Felder aus und bestätigen Sie die neue Klasse";
    }
}

//MainForm

private void CreateOrEdit()
{
   var presenter = create 
        ? new CreateSchoolClassPresenter() 
        : new EditSchoolClassPresenter(selectedSchoolClass);

    var dialog = new CreateOrEditDialog(presenter);
    if(DialogResult.Ok = dialog.ShowDialog()) presenter.OnSubmit();
}

Nur ganz grob, das wird hinten und vorn noch nicht funktionieren. Der Punkt ist einfach: abhängig vom Anwendungsfall übergibt man einfach einen leicht unterschiedlichen Presenter und kann dieselbe Form wieder verwenden. (Du könntest mit einem dritten Presenter dieselbe Form benutzen, um eine eventuelle Löschung noch einmal überprüfen zu lassen, zum Beispiel)

Das geht aber nur, wenn die Form nichts von ihrer Bedeutung weiß: Single Responsibility (nämlich, Daten darstellen und eine User-Aktion entgegennehmen). DRY hat damit gar nichts zu tun, und die zwei Prinzipien widersprechen sich auch nicht.

Die Datenbindungen kannst du dir übrigens im Designer zusammenklicken. Insgesamt dürfte sehr, sehr wenig wirkliche Programmierarbeit nötig sein.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 6 Jahren

Hallo Leute,

vielen Dank für euere Antworten!
@MarsStein: allein der Artikel MVC/MVP ist schon sehr interessant, obwohl ich den noch einige Male werde lesen müssen, bis ich's soweit verstanden habe, dass ich mal ein Praxisexempel wagen werde. Schade, dass in einschlägiger Literatur nicht bereits bei den Basics dieses - ich nenn's mal "Layering" - eingehendere Betrachtung findet^^ Wenn ich bedenke welche Probleme man damit umgehen kann, verwundet es mich wirklich, dass das unter "advanced topics" residiert...

@LaTino: Danke für den Denkanstoß! Auf die Idee mit dem Interface (eigentlich naheliegend...) bin ich gar nicht gekommen. Damit hätte ich dann auch keine so enge Klassenbindung. Trotzdem: ich würde lügen, täte ich jetzt behaupten das zu 100% zu verstehen. Ich werd also noch etwas Lücken füllen müssen, bevor es soweit ist.

Nochmals vielen Dank!
viele Grüße
Vorph

3.170 Beiträge seit 2006
vor 6 Jahren

Hallo,

noch eine kleine Anmerkung:

Ich hatte genau diese Frage hier schon mal gepostet, bezüglich dessen, ob ich eine Form für 2 Aufgaben nutzen kann, wenn sie von der GUI her genau gleich daherkommt, lediglich einmal für das Editieren und einmal für das Neuerstellen benutzt wird. Damals wurde mit DRY argumentiert.

Sicher meinst Du damit den Thread Würde man in der Praxis eine Form für zwei versch. Verwendungszwecke nutzen?
Wenn Du den jetzte nochmal liest, wirst Du feststellen, dass die Antworten von Sir Rufo und chilic keineswegs dem widersprechen, was hier geschrieben wurde, sonder im Gegenteil sehr gut dazu passt 😉

Gruß, MarsStein

Non quia difficilia sunt, non audemus, sed quia non audemus, difficilia sunt! - Seneca

4.931 Beiträge seit 2008
vor 6 Jahren

Bei dem Code von LaTino wurde bisher das Interface aber noch nicht eingesetzt:


abstract class SchoolClassPresenterBase : IModifySchoolClassPresenter, INotifyPropertyChanged

Und dann sollte es auch benutzt werden, z.B. in CreateOrEdit:


IMdoifySchoolClassPresenter presenter = ...

(der bisherige Code mit var würde so gar nicht funktionieren, wegen unterschiedlicher Klassen beim ?:-Operator)

Und dann noch ein kleiner Schreibfehler:


if (DialogResult.Ok == dialog.ShowDialog())

3.003 Beiträge seit 2006
vor 6 Jahren

war am tablet geschrieben, daher die von th69 zu Recht bemängelten Stellen...sry.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)