¿Clase base abstracta con interfaces como comportamientos?

14

Necesito diseñar una jerarquía de clases para mi proyecto C #. Básicamente, las funcionalidades de la clase son similares a las clases de WinForms, así que tomemos el kit de herramientas de WinForms como ejemplo. (Sin embargo, no puedo usar WinForms o WPF).

Hay algunas propiedades y funcionalidades principales que cada clase debe proporcionar. Dimensiones, posición, color, visibilidad (verdadero / falso), método de dibujo, etc.

Necesito consejos de diseño. He utilizado un diseño con una clase base abstracta e interfaces que no son realmente tipos sino comportamientos similares. ¿Es este un buen diseño? Si no, cuál sería un mejor diseño.

El código se ve así:

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

Algunos Controles pueden contener otros Controles, algunos solo pueden estar contenidos (como niños), así que estoy pensando en hacer dos interfaces para estas funcionalidades:

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

Los controles de WinForms muestran el texto, por lo que esto también entra en la interfaz:

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

    void DrawText();
}

Algunos controles se pueden acoplar dentro de su control principal, por lo que:

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

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

... y ahora vamos a crear algunas clases concretas:

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

El primer "problema" que se me ocurre aquí es que las interfaces están básicamente escritas en piedra una vez que se publican. Pero supongamos que podré hacer que mis interfaces sean lo suficientemente buenas para evitar la necesidad de realizar cambios en ellas en el futuro.

Otro problema que veo en este diseño es que cada una de estas clases necesitaría implementar sus interfaces y la duplicación de código se producirá rápidamente. Por ejemplo, en Label and Button, el método DrawText () se deriva de la interfaz ITextHolder o en cada clase derivada de la gestión de niños de IContainer.

Mi solución para este problema es implementar estas funcionalidades "duplicadas" en adaptadores dedicados y reenviarlas. Por lo tanto, tanto la etiqueta como el botón tendrían un miembro de TextHolderAdapter que se llamaría dentro de los métodos heredados de la interfaz de ITextHolder.

Creo que este diseño debería protegerme de tener que tener muchas funcionalidades comunes en la clase base, que podrían hincharse rápidamente con métodos virtuales y un "código de ruido" innecesario. Los cambios de comportamiento se lograrían extendiendo los adaptadores y no las clases derivadas de Control.

Creo que a esto se le llama el patrón de "Estrategia" y aunque hay millones de preguntas y respuestas sobre ese tema, me gustaría preguntarle sus opiniones sobre lo que tomo en cuenta para este diseño y qué fallas puede hacer. pensar en mi enfoque.

Debo agregar que hay una probabilidad de casi el 100% de que los requisitos futuros exijan nuevas clases y nuevas funcionalidades.

    
pregunta grapkulec 01.07.2011 - 11:34

3 respuestas

5

[1] Agrega virtual "getters" & "configuradores" de sus propiedades, tuve que hackear otra biblioteca de control, porque necesito esta función:

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

Sé que esta sugerencia es más "detallada" o compleja, pero es muy útil en el mundo real.

[2] Agregue una propiedad "IsEnabled", no confunda 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 que puede tener que mostrar su control, pero no mostrar ninguna información.

[3] Agregue una propiedad "IsReadOnly", no confunda 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

Eso significa que el control puede mostrar información, pero el usuario no puede cambiarlo.

    
respondido por el umlcat 01.07.2011 - 19:57
3

Buena pregunta! Lo primero que noté es que el método de dibujo no tiene ningún parámetro, ¿dónde se dibuja? Creo que debería recibir algún objeto Surface o Graphics como parámetro (como interfaz, por supuesto).

Una cosa más que me parece extraña es la interfaz de IContainer. Tiene el método GetChild que devuelve un solo elemento secundario y no tiene parámetros; tal vez debería devolver una colección o si mueve el método Draw a una interfaz, podría implementar esta interfaz y tener un método de dibujo que pueda dibujar su colección secundaria interna. sin exponerlo.

Tal vez para ideas, también podría ver WPF, en mi opinión, es un marco muy bien diseñado. También puedes echar un vistazo a los patrones de diseño de Composición y Decorador. El primero puede ser útil para los elementos contenedores y el segundo para agregar funcionalidad a los elementos. No puedo decir qué tan bien funcionará a primera vista, pero esto es lo que pienso:

Para evitar la duplicación, podría tener algo como elementos primitivos, como el elemento de texto. Luego puedes construir los elementos más complicados usando estos primitivos. Por ejemplo, un Border es un elemento que tiene un solo elemento secundario. Cuando llama a su método Draw , dibuja un borde y un fondo y luego dibuja a su hijo dentro del borde. Un TextElement solo dibuja un texto. Ahora, si desea un botón, puede crear uno componiendo esas dos primitivas, es decir, colocando texto dentro del borde. Aquí la frontera es algo así como un decorador.

La publicación se está haciendo bastante larga, así que avísame si la idea te parece interesante y puedo dar algunos ejemplos o más explicaciones.

    
respondido por el Georgi Stoyanov 01.07.2011 - 13:52
2

Cortar el código e ir al núcleo de su pregunta "¿Mi diseño con clase base abstracta e interfaces no como tipos, sino más como comportamientos es un buen diseño o no?", diría que no hay nada de malo en ese enfoque .

De hecho, he utilizado un enfoque de este tipo (en mi motor de representación) donde cada interfaz definió el comportamiento que esperaba el subsistema consumidor. Por ejemplo, IUpdateable, ICollidable, IRenderable, ILoadable, ILoader y así sucesivamente. Para mayor claridad, también separé cada uno de estos "comportamientos" en clases parciales separadas como "Entity.IUpdateable.cs", "Entity.IRenderable.cs" y traté de mantener los campos y métodos lo más independientes posible.

El enfoque de usar interfaces para definir patrones de comportamiento también está de acuerdo conmigo porque los parámetros genéricos, de restricciones y de tipo de variante de co (ntra) funcionan muy bien juntos.

Una de las cosas que observé sobre este método de diseño fue: si alguna vez te encuentras definiendo una interfaz con más de un puñado de métodos, probablemente no estés implementando un comportamiento.

    
respondido por el Ani 01.07.2011 - 23:53

Lea otras preguntas en las etiquetas