Classe de base abstraite avec des interfaces comme comportements?

14

J'ai besoin de concevoir une hiérarchie de classes pour mon projet C #. Fondamentalement, les fonctionnalités des classes sont similaires aux classes WinForms, prenons donc la boîte à outils WinForms comme exemple. (Cependant, je ne peux pas utiliser WinForms ou WPF.)

Il y a quelques propriétés et fonctionnalités de base que chaque classe doit fournir. Dimensions, position, couleur, visibilité (vrai / faux), méthode de dessin, etc.

J'ai besoin de conseils de conception J'ai utilisé une conception avec une classe de base abstraite et des interfaces qui ne sont pas vraiment des types mais plutôt des comportements. Est-ce un bon design? Sinon, quelle serait une meilleure conception.

Le code ressemble à ceci:

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();
}

Certains contrôles peuvent contenir d'autres contrôles, certains ne peuvent être contenus (qu'en tant qu'enfants), donc je pense à faire deux interfaces pour ces fonctionnalités:

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);
}

Les contrôles WinForms affichent du texte, donc cela va aussi dans l'interface:

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

    void DrawText();
}

Certains contrôles peuvent être ancrés dans leur contrôle parent afin:

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

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

... et maintenant créons des classes concrètes:

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

Le premier "problème" auquel je peux penser ici est que les interfaces sont fondamentalement figées une fois publiées. Mais supposons que je serai en mesure de rendre mes interfaces suffisamment bonnes pour éviter d'avoir à les modifier à l'avenir.

Un autre problème que je vois dans cette conception est que chacune de ces classes aurait besoin d'implémenter ses interfaces et la duplication de code se produira rapidement. Par exemple, dans Label et Button, la méthode DrawText () dérive de l'interface ITextHolder ou de chaque classe dérivée de la gestion IContainer des enfants.

Ma solution à ce problème consiste à implémenter ces fonctionnalités "dupliquées" dans des adaptateurs dédiés et à leur renvoyer les appels. Ainsi, Label et Button auraient tous deux un membre TextHolderAdapter qui serait appelé à l'intérieur de méthodes héritées de l'interface ITextHolder.

Je pense que cette conception devrait me protéger d'avoir de nombreuses fonctionnalités communes dans la classe de base qui pourraient rapidement être gonflées avec des méthodes virtuelles et un "code de bruit" inutile. Les changements de comportement seraient accomplis en étendant les adaptateurs et non les classes dérivées de Control.

Je pense que cela s'appelle le modèle de "stratégie" et bien qu'il y ait des millions de questions et réponses sur ce sujet, je voudrais vous demander votre avis sur ce que je prends en considération pour cette conception et sur les défauts auxquels vous pouvez penser mon approche.

Je dois ajouter qu'il y a presque 100% de chances que les exigences futures appellent de nouvelles classes et de nouvelles fonctionnalités.

grapkulec
la source
Pourquoi ne pas simplement hériter de System.ComponentModel.Componentou System.Windows.Forms.Controlde l'une des autres classes de base existantes? Pourquoi devez-vous créer votre propre hiérarchie de contrôle et définir à nouveau toutes ces fonctions à partir de zéro?
Cody Gray
3
IChildsemble être un nom horrible.
Raynos
@Cody: premièrement, je ne peux pas utiliser d'assemblys WinForms ou WPF, deuxièmement - allez, c'est juste un exemple d'un problème de conception que je pose. S'il vous est plus facile de penser aux formes ou aux animaux. mes classes doivent se comporter de la même manière que les contrôles WinForms mais pas dans tous les sens et pas exactement comme eux
grapkulec
2
Cela n'a pas beaucoup de sens de dire que vous ne pouvez pas utiliser les assemblys WinForms ou WPF, mais vous pourriez utiliser n'importe quel framework de contrôle personnalisé que vous deviez créer. Il s'agit d'un cas sérieux de réinventer la roue, et je ne peux pas imaginer dans quel but possible. Mais si vous devez absolument le faire, pourquoi ne pas simplement suivre l' exemple des contrôles WinForms? Cela rend cette question à peu près obsolète.
Cody Gray
1
@graphkulec c'est pourquoi c'est un commentaire :) Si j'avais des commentaires utiles à donner, j'aurais répondu à votre question. C'est toujours un nom horrible;)
Raynos

Réponses:

5

[1] Ajoutez des "getters" et "setters" virtuels à vos propriétés, j'ai dû pirater une autre bibliothèque de contrôle, car j'ai besoin de cette fonctionnalité:

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

Je sais que cette suggestion est plus "verbeuse" ou complexe, mais elle est très utile dans le monde réel.

[2] Ajoutez une propriété "IsEnabled", ne confondez pas avec "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

Cela signifie que vous devrez peut-être montrer votre contrôle, mais ne montrez aucune information.

[3] Ajoutez une propriété "IsReadOnly", ne confondez pas avec "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

Cela signifie que le contrôle peut afficher des informations, mais ne peut pas être modifié par l'utilisateur.

umlcat
la source
bien que je n'aime pas toutes ces méthodes virtuelles dans la classe de base, je leur donnerai une seconde chance dans ma réflexion sur le design. et vous m'avez rappelé que j'avais vraiment besoin de la propriété IsReadOnly :)
grapkulec
@grapkulec Puisqu'il s'agit de la classe de base, vous devez spécifier que les classes dérivées auront ces propriétés, mais que les méthodes qui contrôlent le comportement peuvent être modifiées à chaque purpouse de classe ;-)
umlcat
mmkey, je vois votre point et merci d'avoir pris le temps de répondre
grapkulec
1
J'ai accepté cela comme une réponse car une fois que j'ai commencé à implémenter ma conception "cool" avec des interfaces, je me suis rapidement retrouvé dans le besoin de setters virtuels et parfois de getters également. Cela ne signifie pas que je n'utilise pas du tout d'interfaces, je viens de limiter leur utilisation aux endroits où ils sont vraiment nécessaires.
grapkulec
3

Bonne question! La première chose que je remarque, c'est que la méthode draw n'a pas de paramètres, où dessine-t-elle? Je pense qu'il devrait recevoir un objet Surface ou Graphics comme paramètre (comme interface bien sûr).

Une autre chose que je trouve étrange est l'interface IContainer. Il a une GetChildméthode qui retourne un seul enfant et n'a aucun paramètre - il devrait peut-être renvoyer une collection ou si vous déplacez la méthode Draw vers une interface, il pourrait alors implémenter cette interface et avoir une méthode draw qui peut dessiner sa collection enfants interne sans l'exposer .

Peut-être que pour des idées, vous pouvez également regarder WPF - c'est un cadre très bien conçu à mon avis. Vous pouvez également jeter un œil aux modèles de conception Composition et Décorateur. Le premier peut être utile pour les éléments de conteneur et le second pour ajouter des fonctionnalités aux éléments. Je ne peux pas dire à quel point cela fonctionnera à première vue, mais voici ce que je pense:

Pour éviter la duplication, vous pourriez avoir quelque chose comme des éléments primitifs - comme l'élément texte. Ensuite, vous pouvez construire les éléments les plus compliqués en utilisant ces primitives. Par exemple, a Borderest un élément qui a un seul enfant. Lorsque vous appelez sa Drawméthode, il dessine une bordure et un arrière-plan, puis dessine son enfant à l'intérieur de la bordure. A TextElementne dessine que du texte. Maintenant, si vous voulez un bouton, vous pouvez en créer un en composant ces deux primitives, c'est-à-dire en mettant du texte à l'intérieur de la bordure. Ici, la frontière est quelque chose comme un décorateur.

Le message devient assez long, alors faites-moi savoir si vous trouvez cette idée intéressante et je peux donner quelques exemples ou plus d'explications.

Georgi Stoyanov
la source
1
les paramètres manquants ne sont pas un problème, ce n'est qu'un croquis de méthodes réelles après tout. En ce qui concerne WPF, j'ai conçu un système de mise en page basé sur leur idée, donc je pense avoir déjà une partie de composition en place, car pour les décorateurs, je devrai y penser. votre idée des éléments primitifs semble raisonnable et j'essaie vraiment. Merci!
grapkulec
@grapkulec Vous êtes les bienvenus! À propos des params - oui, ça va, je voulais juste être sûr de bien comprendre vos intentions. Je suis content que cette idée te plaise. Bonne chance!
2

Découper le code et aller au cœur de votre question "Ma conception avec une classe de base abstraite et des interfaces n'est-elle pas en tant que types mais plutôt en tant que comportements est-elle une bonne conception ou non?", Je dirais qu'il n'y a rien de mal à cette approche.

J'ai en fait utilisé une telle approche (dans mon moteur de rendu) où chaque interface définissait le comportement attendu du sous-système consommateur. Par exemple, IUpdateable, ICollidable, IRenderable, ILoadable, ILoader et ainsi de suite, etc. Pour plus de clarté, j'ai également séparé chacun de ces "comportements" en classes partielles distinctes comme "Entity.IUpdateable.cs", "Entity.IRenderable.cs" et j'ai essayé de garder les champs et les méthodes aussi indépendants que possible.

L'approche d'utiliser des interfaces pour définir des modèles de comportement est également d'accord avec moi car les génériques, les contraintes et les paramètres de type variant co (ntra) fonctionnent très bien ensemble.

L'une des choses que j'ai observées à propos de cette méthode de conception était: si jamais vous vous retrouvez à définir une interface avec plus d'une poignée de méthodes, vous n'implémentez probablement pas de comportement.

Ani
la source
avez-vous rencontré des problèmes ou à un moment donné, vous regrettiez une telle conception et avez dû vous battre avec votre propre code pour que les choses soient réellement faites? comme par exemple, il s'est avéré que vous deviez créer tellement d'implémentations de comportement que cela n'avait aucun sens et que vous souhaitiez simplement vous en tenir au vieux thème "créer une classe de base avec des millions de virtuels et hériter et remplacer l'enfer"?
grapkulec