Classe base astratta con interfacce come comportamenti?


14

Devo progettare una gerarchia di classi per il mio progetto C #. Fondamentalmente, le funzionalità della classe sono simili alle classi WinForms, quindi prendiamo ad esempio il toolkit WinForms. (Tuttavia, non posso usare WinForms o WPF.)

Ci sono alcune proprietà e funzionalità di base che ogni classe deve fornire. Dimensioni, posizione, colore, visibilità (vero / falso), metodo di disegno, ecc.

Ho bisogno di consigli di progettazione Ho usato un design con una classe base astratta e interfacce che non sono realmente tipi ma più simili a comportamenti. È un buon design? In caso contrario, quale sarebbe un design migliore.

Il codice è simile al seguente:

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

Alcuni controlli possono contenere altri controlli, alcuni possono essere contenuti solo (come figli), quindi sto pensando di creare due interfacce per queste funzionalità:

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 controlla la visualizzazione del testo, quindi anche questo va nell'interfaccia:

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

    void DrawText();
}

Alcuni controlli possono essere agganciati all'interno del loro controllo genitore in modo che:

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

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

... e ora creiamo alcune classi concrete:

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

Il primo "problema" che mi viene in mente qui è che le interfacce sono fondamentalmente incastonate una volta pubblicate. Ma supponiamo che sarò in grado di rendere le mie interfacce abbastanza buone da evitare la necessità di apportarle modifiche in futuro.

Un altro problema che vedo in questo progetto è che ognuna di queste classi dovrebbe implementare le sue interfacce e si verificherà rapidamente la duplicazione del codice. Ad esempio, in Label and Button il metodo DrawText () deriva dall'interfaccia ITextHolder o in ogni classe derivata dalla gestione IContainer dei bambini.

La mia soluzione a questo problema è implementare queste funzionalità "duplicate" in adattatori dedicati e inoltrare le chiamate a loro. Quindi sia Label che Button avrebbero un membro TextHolderAdapter che verrebbe chiamato all'interno dei metodi ereditati dall'interfaccia ITextHolder.

Penso che questo progetto dovrebbe proteggermi dall'avere molte funzionalità comuni nella classe base che potrebbero rapidamente gonfiarsi con metodi virtuali e "noise code" non necessari. I cambiamenti di comportamento verrebbero realizzati estendendo gli adattatori e non le classi derivate dal controllo.

Penso che questo sia chiamato il modello "Strategia" e sebbene ci siano milioni di domande e risposte su quell'argomento, vorrei chiederti le tue opinioni su ciò che prendo in considerazione per questo disegno e su quali difetti puoi pensare il mio approccio.

Vorrei aggiungere che esiste una probabilità quasi del 100% che i requisiti futuri richiedano nuove classi e nuove funzionalità.


Perché non semplicemente ereditare da System.ComponentModel.Componento da una System.Windows.Forms.Controlqualsiasi delle altre classi base esistenti? Perché è necessario creare la propria gerarchia di controllo e definire nuovamente tutte queste funzioni da zero?
Cody Gray,

3
IChildsembra un nome orribile.
Raynos,

@Cody: in primo luogo, non posso usare WinForms o gli assembly WPF, in secondo luogo, dai, è solo un esempio di un problema di progettazione di cui sto chiedendo. Se per te è più facile pensare a forme o animali. le mie lezioni devono comportarsi in modo simile ai controlli WinForms ma non in tutti i modi e non esattamente come loro
grapkulec

2
Non ha molto senso dire che non è possibile utilizzare gli assembly WinForms o WPF, tuttavia si sarebbe in grado di utilizzare qualsiasi framework di controllo personalizzato che si sarebbe dovuto creare. Questo è un caso serio di reinvenzione della ruota, e non riesco a immaginare per quale possibile scopo. Ma se assolutamente è necessario, perché non seguire semplicemente l' esempio dei controlli WinForms? Ciò rende questa domanda praticamente obsoleta.
Cody Gray,

1
@graphkulec ecco perché è un commento :) Se avessi avuto un feedback utile reale avrei risposto alla tua domanda. È ancora un nome orribile;)
Raynos

Risposte:


5

[1] Aggiungi "getter" virtuali e "setter" alle tue proprietà, ho dovuto hackerare un'altra libreria di controllo, perché ho bisogno di questa funzione:

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

So che questo suggerimento è più "dettagliato" o complesso, ma è molto utile nel mondo reale.

[2] Aggiungi una proprietà "IsEnabled", non confondere con "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

Significa che potresti dover mostrare il tuo controllo, ma non mostrare alcuna informazione.

[3] Aggiungi una proprietà "IsReadOnly", non confondere con "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

Ciò significa che il controllo può visualizzare informazioni, ma non può essere modificato dall'utente.


anche se non mi piacciono tutti questi metodi virtuali nella classe base, darò loro una seconda possibilità nel mio pensiero sul design. e mi hai ricordato che ho davvero bisogno della proprietà IsReadOnly :)
grapkulec

@grapkulec Dal momento che è la classe di base, è necessario specificare che le classi derivate avranno tali proprietà, ma che i metodi che controllano il comportamento possono essere modificati su ogni ambito di classe ;-)
umlcat

mmkey, vedo il tuo punto e ti ringrazio per aver
dedicato del

1
Ho accettato questo come una risposta perché una volta che ho iniziato a implementare il mio design "cool" con interfacce mi sono subito trovato nel bisogno di setter virtuali e talvolta anche di getter. Ciò non significa che non utilizzo affatto le interfacce, ma ho limitato il loro uso ai luoghi in cui sono realmente necessarie.
Grapkulec,

3

Bella domanda! La prima cosa che noto è che il metodo draw non ha alcun parametro, dove disegna? Penso che dovrebbe ricevere alcuni oggetti Surface o Graphics come parametro (come interfaccia ovviamente).

Un'altra cosa che trovo strana è l'interfaccia IContainer. Ha un GetChildmetodo che restituisce un singolo figlio e non ha parametri: forse dovrebbe restituire una raccolta o se sposti il ​​metodo Disegna su un'interfaccia, potrebbe implementare questa interfaccia e avere un metodo di disegno in grado di disegnare la sua raccolta figlio interna senza esporla .

Forse per idee che potresti guardare anche a WPF - è un framework molto ben progettato secondo me. Inoltre puoi dare un'occhiata ai motivi di design di Composizione e Decoratore. Il primo può essere utile per gli elementi contenitore e il secondo per aggiungere funzionalità agli elementi. Non posso dire quanto funzionerà a prima vista, ma ecco cosa penso:

Per evitare la duplicazione potresti avere qualcosa come elementi primitivi - come l'elemento di testo. Quindi puoi costruire gli elementi più complicati usando queste primitive. Ad esempio, a Borderè un elemento che ha un unico figlio. Quando chiami il suo Drawmetodo, disegna un bordo e uno sfondo e quindi disegna il suo figlio all'interno del bordo. A TextElementdisegna solo del testo. Ora, se vuoi un pulsante, puoi crearne uno componendo quei due primitivi, ovvero inserendo del testo all'interno del bordo. Qui il confine è qualcosa di simile a un decoratore.

Il post sta diventando piuttosto lungo, quindi fatemi sapere se trovate quell'idea interessante e posso dare alcuni esempi o più spiegazioni.


1
i parametri mancanti non sono un problema, dopo tutto è solo uno schizzo di metodi reali. Per quanto riguarda WPF ho progettato un sistema di layout basato sulla loro idea, quindi penso di avere già una parte di composizione, per quanto riguarda i decoratori dovrò pensarci. la tua idea di elementi primitivi suona ragionevole e sicuramente ci provo. Grazie!
Grapkulec,

@grapkulec Prego! A proposito di params - sì, va bene, volevo solo essere sicuro di capire correttamente le tue intensità. Sono contento che ti piaccia l'idea. In bocca al lupo!

2

Ritagliare il codice e andare al nocciolo della tua domanda "Il mio design con una classe base astratta e le interfacce non sono come tipi ma più come comportamenti è un buon design o no", direi che non c'è niente di sbagliato in questo approccio.

In effetti ho usato un tale approccio (nel mio motore di rendering) in cui ogni interfaccia definiva il comportamento previsto dal sottosistema consumante. Ad esempio, IUpdateable, ICollidable, IRenderable, ILoadable, ILoader e così via e così via. Per chiarezza, ho anche separato ciascuno di questi "comportamenti" in classi parziali separate come "Entity.IUpdateable.cs", "Entity.IRenderable.cs" e ho cercato di mantenere i campi e i metodi il più indipendenti possibile.

Anche l'approccio dell'uso delle interfacce per definire modelli comportamentali concorda con me perché generici, vincoli e parametri del tipo di variante co (ntra) funzionano molto bene insieme.

Una delle cose che ho osservato su questo metodo di progettazione è stata: se mai ti ritrovi a definire un'interfaccia con più di una manciata di metodi, probabilmente non stai implementando un comportamento.


ti sei imbattuto in qualche problema o in qualsiasi momento ti sei pentito di tale progetto e hai dovuto combattere con il tuo codice per ottenere effettivamente le cose previste? come ad esempio all'improvviso si è scoperto che è necessario creare così tante implementazioni comportamentali che non ha senso e che si desidera semplicemente attenersi al vecchio tema "creare una classe base con milioni di virtuali e ereditare e scavalcare l'inferno"?
grapkulec,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.