Base di partenza astratta con interfacce come comportamenti?

14

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

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

Ho bisogno di una consulenza di design Ho usato un design con una classe di 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 a questo:

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 bambini), 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 questa entra 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 controllo principale in modo che:

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

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

... e ora creiamo alcune lezioni 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" a cui posso pensare è che le interfacce sono fondamentalmente scolpite nella pietra una volta pubblicate. Ma supponiamo che sarò in grado di rendere le mie interfacce abbastanza buone da evitare di dover apportare modifiche a loro in futuro.

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

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

Penso che questo design dovrebbe impedirmi di avere molte funzionalità comuni nella classe base che potrebbero diventare rapidamente gonfie di metodi virtuali e di "codice rumore" non necessario. I cambiamenti di comportamento si otterrebbero estendendo gli adattatori e non le classi derivate da Control.

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

Devo aggiungere che c'è una probabilità quasi del 100% che i requisiti futuri richiederanno nuove classi e nuove funzionalità.

    
posta grapkulec 01.07.2011 - 11:34
fonte

3 risposte

5

[1] Aggiungi virtual "getter" & "setter" per le tue proprietà, ho dovuto hackerare un'altra libreria di controlli, perché ho bisogno di questa funzionalità:

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ù "verboso" 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.

    
risposta data 01.07.2011 - 19:57
fonte
3

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

Un'altra cosa che trovo strana è l'interfaccia IContainer. Ha il metodo GetChild che restituisce un singolo figlio e non ha parametri - forse dovrebbe restituire una collezione o se si sposta il metodo Draw su un'interfaccia allora potrebbe implementare questa interfaccia e avere un metodo draw che può disegnare la sua collezione di bambini interni senza esporlo.

Forse per le idee potresti anche guardare al WPF - è una struttura molto ben progettata secondo me. Puoi anche dare un'occhiata ai modelli di design di Composizione e Decoratore. Il primo può essere utile per gli elementi del contenitore e il secondo per aggiungere funzionalità agli elementi. Non posso dire quanto bene funzionerà al primo sguardo, ma ecco cosa penso:

Per evitare la duplicazione potresti avere qualcosa di simile agli elementi primitivi, come l'elemento di testo. Quindi puoi costruire gli elementi più complicati usando questi primitivi. Ad esempio, un Border è un elemento che ha un singolo figlio. Quando chiami il suo metodo Draw disegna un bordo e uno sfondo e poi disegna il suo bambino all'interno del bordo. Un TextElement disegna solo del testo. Ora se vuoi un pulsante, puoi crearne uno componendo quei due primitivi, cioè mettendo il testo all'interno del bordo. Qui il confine è qualcosa come un decoratore.

Il post sta diventando piuttosto lungo, quindi fammi sapere se trovi l'idea interessante e posso fornire alcuni esempi o ulteriori spiegazioni.

    
risposta data 01.07.2011 - 13:52
fonte
2

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

In effetti ho utilizzato un tale approccio (nel mio motore di rendering) in cui ogni interfaccia ha definito il comportamento previsto dal sottosistema di consumo. 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.

L'approccio dell'uso delle interfacce per definire i modelli comportamentali concorda anche con me perché i parametri generici, i vincoli e i parametri della variante co (ntra) funzionano molto bene insieme.

Una delle cose che ho osservato riguardo 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.

    
risposta data 01.07.2011 - 23:53
fonte

Leggi altre domande sui tag