Abstrakte Basisklasse mit Interfaces als Verhalten?

14

Ich muss eine Klassenhierarchie für mein C # -Projekt entwerfen. Grundsätzlich ähneln die Klassenfunktionen den WinForms-Klassen. Nehmen wir als Beispiel das WinForms-Toolkit. (Ich kann jedoch weder WinForms noch WPF verwenden.)

Es gibt einige grundlegende Eigenschaften und Funktionen, die jede Klasse bereitstellen muss. Maße, Position, Farbe, Sichtbarkeit (wahr / falsch), Zeichenmethode usw.

Ich brauche Designberatung Ich habe ein Design mit einer abstrakten Basisklasse und Schnittstellen verwendet, die keine echten Typen sind, sondern eher Verhaltensweisen. Ist das ein gutes Design? Wenn nicht, was wäre ein besseres Design.

Der Code sieht folgendermaßen aus:

abstract class Control
{
    public int Width { get; set; }

    public int Height { get; set; }

    public int BackColor { get; set; }

    public int X { get; set; }

    public int Y { get; set; }

    public int BorderWidth { get; set; }

    public int BorderColor { get; set; }

    public bool Visible { get; set; }

    public Rectangle ClipRectange { get; protected set; }

    abstract public void Draw();
}

Einige Steuerelemente können andere Steuerelemente enthalten, andere können nur (als untergeordnete Elemente) enthalten sein. Daher denke ich darüber nach, zwei Schnittstellen für diese Funktionen zu erstellen:

interface IChild
{
    IContainer Parent { get; set; }
}

internal interface IContainer
{
    void AddChild<T>(T child) where T : IChild;
    void RemoveChild<T>(T child) where T : IChild;
    IChild GetChild(int index);
}

WinForms steuert den Anzeigetext, sodass dies auch in die Benutzeroberfläche eingeht:

interface ITextHolder
{
    string Text { get; set; }
    int TextPositionX { get; set; }
    int TextPositionY { get; set; }
    int TextWidth { get; }
    int TextHeight { get; }

    void DrawText();
}

Einige Steuerelemente können in ihrem übergeordneten Steuerelement angedockt werden.

enum Docking
{ 
    None, Left, Right, Top, Bottom, Fill
}

interface IDockable
{
    Docking Dock { get; set; }
}

... und jetzt erstellen wir einige konkrete Klassen:

class Panel : Control, IDockable, IContainer, IChild {}
class Label : Control, IDockable, IChild, ITextHolder {}
class Button : Control, IChild, ITextHolder, IDockable {}
class Window : Control, IContainer, IDockable {}

Das erste "Problem", das mir hier einfällt, ist, dass die Schnittstellen nach ihrer Veröffentlichung im Grunde genommen in Stein gemeißelt sind. Nehmen wir jedoch an, dass ich meine Benutzeroberflächen so gut gestalten kann, dass sie in Zukunft nicht mehr geändert werden müssen.

Ein weiteres Problem, das ich in diesem Entwurf sehe, ist, dass jede dieser Klassen ihre Schnittstellen implementieren müsste und dass eine Vervielfältigung von Code schnell erfolgen wird. Beispielsweise wird in Label und Button die DrawText () -Methode von der ITextHolder-Schnittstelle oder von jeder Klasse abgeleitet, die aus der IContainer-Verwaltung von untergeordneten Elementen stammt.

Meine Lösung für dieses Problem besteht darin, diese "duplizierten" Funktionen in dedizierten Adaptern zu implementieren und Aufrufe an diese weiterzuleiten. Daher hätten sowohl Label als auch Button einen TextHolderAdapter-Member, der innerhalb von Methoden aufgerufen würde, die von der ITextHolder-Schnittstelle geerbt wurden.

Ich denke, dieses Design sollte mich davor schützen, dass ich zu viele gemeinsame Funktionen in der Basisklasse habe, die mit virtuellen Methoden und unnötigem "Rauschcode" schnell aufgebläht werden könnten. Verhaltensänderungen würden durch Erweitern von Adaptern und nicht durch von Steuerelementen abgeleitete Klassen erreicht.

Ich denke, dies wird das "Strategie" -Muster genannt, und obwohl es zu diesem Thema Millionen von Fragen und Antworten gibt, möchte ich Sie um Ihre Meinung bitten, was ich für dieses Design in Betracht ziehe und an welche Fehler Sie denken können mein Ansatz.

Ich sollte hinzufügen, dass es eine fast 100% ige Chance gibt, dass zukünftige Anforderungen neue Klassen und neue Funktionalitäten erfordern.

grapkulec
quelle
Warum nicht einfach von einer System.ComponentModel.Componentoder System.Windows.Forms.Controleiner der anderen vorhandenen Basisklassen erben ? Warum müssen Sie eine eigene Steuerelementhierarchie erstellen und alle diese Funktionen neu definieren?
Cody Grey
3
IChildscheint wie ein schrecklicher Name.
Raynos
@Cody: Erstens kann ich keine WinForms- oder WPF-Assemblys verwenden, zweitens ist dies nur ein Beispiel für ein Entwurfsproblem, nach dem ich frage. Wenn es Ihnen leichter fällt, denken Sie an Formen oder Tiere. Meine Klassen müssen sich ähnlich wie WinForms-Steuerelemente verhalten, aber nicht in jeder Hinsicht und nicht genau wie sie
Grapkulec
2
Es ist nicht sehr sinnvoll zu sagen, dass Sie die WinForms- oder WPF-Assemblys nicht verwenden können, aber Sie können jedes benutzerdefinierte Steuerungsframework verwenden, das Sie erstellen möchten. Dies ist ein schwerwiegender Fall, in dem das Rad neu erfunden werden muss, und ich kann mir nicht vorstellen, zu welchem ​​Zweck dies möglich ist. Aber wenn Sie es unbedingt müssen, warum nicht einfach dem Beispiel der WinForms-Steuerelemente folgen ? Das macht diese Frage so ziemlich überflüssig.
Cody Grey
1
@graphkulec deshalb ist es ein Kommentar :) Wenn ich wirklich nützliches Feedback hätte, hätte ich deine Frage beantwortet. Es ist immer noch ein schrecklicher Name;)
Raynos

Antworten:

5

[1] Füge virtuelle "Getter" & "Setter" zu deinen Eigenschaften hinzu, ich musste eine andere Kontrollbibliothek hacken, da ich diese Funktion benötige:

abstract class Control
{
    // internal fields for properties
    protected int _Width;
    protected int _Height;
    protected int _BackColor;
    protected int _X;
    protected int _Y;
    protected bool Visible;

    protected int BorderWidth;
    protected int BorderColor;


    // getters & setters virtual !!!

    public virtual int getWidth(...) { ... }
    public virtual void setWidth(...) { ... }

    public virtual int getHeight(...) { ... }
    public virtual void setHeight(...) { ... }

    public virtual int getBackColor(...) { ... }
    public virtual void setBackColor(...) { ... }

    public virtual int getX(...) { ... }
    public virtual void setX(...) { ... }

    public virtual int getY(...) { ... }
    public virtual void setY(...) { ... }

    public virtual int getBorderWidth(...) { ... }
    public virtual void setBorderWidth(...) { ... }

    public virtual int getBorderColor(...) { ... }
    public virtual void setBorderColor(...) { ... }

    public virtual bool getVisible(...) { ... }
    public virtual void setVisible(...) { ... }

    // properties WITH virtual getters & setters

    public int Width { get getWidth(); set setWidth(value); }

    public int Height { get getHeight(); set setHeight(value); }

    public int BackColor { get getBackColor(); set setBackColor(value); }

    public int X { get getX(); set setX(value); }

    public int Y { get getY(); set setY(value); }

    public int BorderWidth { get getBorderWidth(); set setBorderWidth(value); }

    public int BorderColor { get getBorderColor(); set setBorderColor(value); }

    public bool Visible { get getVisible(); set setVisible(value); }

    // other methods

    public Rectangle ClipRectange { get; protected set; }   
    abstract public void Draw();
} // class Control

/* concrete */ class MyControl: Control
{
    public override bool getVisible(...) { ... }
    public override void setVisible(...) { ... }
} // class MyControl: Control

Ich weiß, dass dieser Vorschlag "ausführlicher" oder komplexer ist, aber in der Praxis sehr nützlich.

[2] Fügen Sie eine "IsEnabled" -Eigenschaft hinzu und verwechseln Sie sie nicht mit "IsReadOnly":

abstract class Control
{
    // internal fields for properties
    protected bool _IsEnabled;

    public virtual bool getIsEnabled(...) { ... }
    public virtual void setIsEnabled(...) { ... }

    public bool IsEnabled{ get getIsEnabled(); set setIsEnabled(value); }
} // class Control

Bedeutet, dass Sie möglicherweise Ihre Kontrolle zeigen müssen, aber keine Informationen anzeigen.

[3] Fügen Sie eine "IsReadOnly" -Eigenschaft hinzu und verwechseln Sie sie nicht mit "IsEnabled":

abstract class Control
{
    // internal fields for properties
    protected bool _IsReadOnly;

    public virtual bool getIsReadOnly(...) { ... }
    public virtual void setIsReadOnly(...) { ... }

    public bool IsReadOnly{ get getIsReadOnly(); set setIsReadOnly(value); }
} // class Control

Das bedeutet, dass das Steuerelement Informationen anzeigen kann, aber vom Benutzer nicht geändert werden kann.

umlcat
quelle
Obwohl ich all diese virtuellen Methoden in der Basisklasse nicht mag, gebe ich ihnen eine zweite Chance, wenn ich über Design nachdenke. und du hast mich daran erinnert, dass ich wirklich IsReadOnly-Eigenschaft brauche :)
Grapkulec
@grapkulec Da es sich um die Basisklasse handelt, müssen Sie angeben, dass die abgeleiteten Klassen diese Eigenschaften haben, aber dass die Methoden, die das Verhalten steuern, bei jedem Klassenzweck geändert werden können ;-)
umlcat
mmkey, ich verstehe deinen Standpunkt und danke dir, dass du dir die Zeit genommen hast zu antworten
grapkulec
1
Ich akzeptierte dies als Antwort, da ich, als ich anfing, mein "cooles" Design mit Schnittstellen zu implementieren, schnell virtuelle Setter und manchmal auch Getter brauchte. Das heißt nicht, dass ich überhaupt keine Schnittstellen benutze. Ich habe sie nur auf Orte beschränkt, an denen sie wirklich gebraucht werden.
Grapkulec
3

Gute Frage! Das Erste, was mir auffällt, ist, dass die Zeichenmethode keine Parameter hat. Wo zeichnet sie? Ich denke, es sollte ein Surface- oder Graphics-Objekt als Parameter erhalten (als Schnittstelle natürlich).

Eine weitere Sache, die ich merkwürdig finde, ist das IContainer-Interface. Es hatGetChild Methode, die ein einzelnes untergeordnetes Element zurückgibt und keine Parameter enthält. Möglicherweise sollte eine Auflistung zurückgegeben werden, oder wenn Sie die Draw-Methode in eine Schnittstelle verschieben, kann diese Schnittstelle implementiert werden und eine Draw-Methode verwendet werden, mit der die interne untergeordnete Auflistung gezeichnet werden kann, ohne sie verfügbar zu machen .

Vielleicht können Sie sich für Ideen auch WPF ansehen - es ist meiner Meinung nach ein sehr gut gestalteter Rahmen. Sie können sich auch die Entwurfsmuster für Komposition und Dekorateur ansehen. Der erste kann für Containerelemente und der zweite für das Hinzufügen von Funktionen zu Elementen nützlich sein. Ich kann nicht sagen, wie gut das auf den ersten Blick funktionieren wird, aber ich denke Folgendes:

Um die Duplizierung zu vermeiden, könnten Sie so etwas wie primitive Elemente haben - wie das Textelement. Dann können Sie die komplizierteren Elemente mit diesen Grundelementen erstellen. Zum Beispiel aBorder ein Element, das ein einzelnes untergeordnetes Element hat. Wenn Sie seine DrawMethode aufrufen , zeichnet es einen Rahmen und einen Hintergrund und anschließend sein untergeordnetes Element innerhalb des Rahmens. EINTextElement zeichnet nur Text. Wenn Sie nun eine Schaltfläche möchten, können Sie eine erstellen, indem Sie diese beiden Grundelemente zusammensetzen, dh Text in den Rahmen einfügen. Hier ist die Grenze so etwas wie ein Dekorateur.

Der Beitrag wird ziemlich lang, also lass es mich wissen, wenn du diese Idee interessant findest und ich kann ein paar Beispiele oder mehr Erklärungen geben.

Georgi Stoyanov
quelle
1
fehlende params sind kein problem, sondern nur eine skizze echter methoden. In Bezug auf WPF habe ich das Layoutsystem auf der Grundlage ihrer Idee entworfen, so dass ich denke, dass ich bereits einen Kompositionsteil eingerichtet habe, was die Dekorateure betrifft, muss ich darüber nachdenken. Ihre Vorstellung von primitiven Elementen klingt vernünftig und ich versuche es auf jeden Fall. Vielen Dank!
Grapkulec
@grapkulec Gern geschehen! Über params - ja, es ist in Ordnung, ich wollte nur sichergehen, dass ich deine Absichten richtig verstehe. Ich bin froh, dass dir die Idee gefällt. Viel Glück!
2

Schneiden Sie den Code aus und gehen Sie zum Kern Ihrer Frage "Ist mein Design mit abstrakten Basisklassen und Interfaces nicht als Typen, sondern eher als Verhalten ein gutes Design oder nicht", würde ich sagen, dass an diesem Ansatz nichts auszusetzen ist.

Ich habe in der Tat einen solchen Ansatz (in meiner Rendering-Engine) verwendet, bei dem jede Schnittstelle das Verhalten definiert, das das verbrauchende Subsystem erwartet. Zum Beispiel IUpdateable, ICollidable, IRenderable, ILoadable, ILoader usw. Aus Gründen der Klarheit habe ich auch jedes dieser "Verhaltensweisen" in separate Teilklassen wie "Entity.IUpdateable.cs", "Entity.IRenderable.cs" unterteilt und versucht, die Felder und Methoden so unabhängig wie möglich zu halten.

Der Ansatz, Interfaces zur Definition von Verhaltensmustern zu verwenden, stimmt ebenfalls mit mir überein, da Generics, Constraints und Parameter vom Typ co (ntra) variant sehr gut zusammenarbeiten.

Eines der Dinge, die ich bei dieser Entwurfsmethode beobachtet habe, war: Wenn Sie jemals feststellen, dass Sie eine Schnittstelle mit mehr als einer Handvoll Methoden definieren, implementieren Sie wahrscheinlich kein Verhalten.

Ani
quelle
Sind Sie auf irgendwelche Probleme gestoßen oder haben Sie in irgendeinem Moment ein solches Design bereut und mussten mit Ihrem eigenen Code kämpfen, um die beabsichtigten Dinge tatsächlich zu erledigen? Wie sich zum Beispiel plötzlich herausstellte, dass Sie so viele Verhaltensimplementierungen erstellen müssen, dass es einfach keinen Sinn ergab, und Sie wünschten, Sie würden einfach an dem alten Thema festhalten: "Machen Sie eine Basisklasse mit Millionen von Virtuals und erben und überschreiben Sie einfach die Hölle daraus"?
Grapkulec