¿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 centrales que cada clase necesita 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 más bien comportamientos. ¿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 hijos), 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);
}

WinForms controla el texto de la pantalla, 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 para que:

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

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

... y ahora creemos 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" en el que puedo pensar aquí es que las interfaces se establecen básicamente en piedra una vez que se publican. Pero supongamos que podré hacer que mis interfaces sean lo suficientemente buenas para evitar la necesidad de hacer 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 ocurrirá rápidamente. Por ejemplo, en Label and Button, el método DrawText () se deriva de la interfaz ITextHolder o de cada clase derivada de la administración de IContainer de niños.

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

Creo que este diseño debería protegerme de 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 realizarían ampliando los adaptadores y no las clases derivadas de Control.

Creo que esto se llama el patrón "Estrategia" y, aunque hay millones de preguntas y respuestas sobre ese tema, me gustaría pedirle su opinión sobre lo que tengo en cuenta para este diseño y qué defectos puede pensar en mi acercamiento.

Debo agregar que hay casi un 100% de posibilidades de que los requisitos futuros requieran nuevas clases y nuevas funcionalidades.

grapkulec
fuente
¿Por qué no simplemente heredar de System.ComponentModel.Componento de System.Windows.Forms.Controlcualquiera de las otras clases base existentes? ¿Por qué necesita crear su propia jerarquía de control y definir todas estas funciones nuevamente desde cero?
Cody Gray
3
IChildParece un nombre horrible.
Raynos 01 de
@Cody: primero, no puedo usar WinForms o ensamblajes WPF, segundo, vamos, es solo un ejemplo de un problema de diseño que estoy preguntando. Si te resulta más fácil pensar en formas o animales. mis clases deben comportarse de manera similar a los controles de WinForms pero no en todos los sentidos y no exactamente como ellos
grapkulec
2
No tiene mucho sentido decir que no puede usar los ensamblados WinForms o WPF, sin embargo, podría usar cualquier marco de control personalizado que haya creado. Este es un caso grave de reinvención de la rueda, y no puedo imaginar para qué posible propósito. Pero si es absolutamente necesario, ¿por qué no seguir el ejemplo de los controles WinForms? Eso hace que esta pregunta sea bastante obsoleta.
Cody Gray
1
@graphkulec es por eso que es un comentario :) Si tuviera comentarios útiles para responder, habría respondido a su pregunta. Sigue siendo un nombre horrible;)
Raynos

Respuestas:

5

[1] Agregue "getters" y "setters" virtuales a 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 que tenga que mostrar su control, pero no muestra 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.

umlcat
fuente
Aunque no me gustan todos estos métodos virtuales en la clase base, les daré una segunda oportunidad en mi pensamiento sobre el diseño. y me recordó que realmente necesito la propiedad IsReadOnly :)
grapkulec
@grapkulec Ya que su clase base, se debe especificar que las clases derivadas tendrán esas propiedades, pero, que los métodos que el comportamiento de control se puede cambiar a cada purpouse clase ;-)
umlcat
mmkey, entiendo tu punto y gracias por tomarse el tiempo para responder
grapkulec
1
Acepté esto como respuesta porque una vez que comencé a implementar mi diseño "genial" con interfaces, rápidamente me encontré necesitando setters virtuales y, a veces, getters también. Eso no significa que no use interfaces en absoluto, solo limité su uso a los lugares donde realmente se necesitan.
grapkulec el
3

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

Una cosa más que encuentro extraño es la interfaz IContainer. TieneGetChild método que devuelve un solo hijo 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 infantil interna sin exponerla .

Tal vez para ideas también podría mirar WPF: en mi opinión, es un marco muy bien diseñado. También puede echar un vistazo a los patrones de diseño de Composición y Decorador. El primero puede ser útil para elementos de contenedor 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. Entonces puedes construir los elementos más complicados usando estas primitivas. Por ejemplo, a Borderes un elemento que tiene un solo hijo. Cuando llamas a suDraw método, dibuja un borde y un fondo y luego dibuja a su hijo dentro del borde. A TextElementsolo dibuja algo de texto. Ahora, si desea un botón, puede crear uno componiendo esas dos primitivas, es decir, poner texto dentro del borde. Aquí la frontera es algo así como un decorador.

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

Georgi Stoyanov
fuente
1
los parámetros perdidos no son un problema, después de todo, es solo un bosquejo de métodos reales. Con respecto a WPF, he diseñado un sistema de diseño basado en su idea, por lo que creo que ya tengo parte de la composición, ya que para los decoradores tendré que pensarlo. su idea de elementos primitivos suena razonable y definitivamente lo intento. ¡Gracias!
grapkulec 01 de
@grapkulec De nada! Acerca de los parámetros: sí, está bien, solo quería asegurarme de que entiendo tus intensiones correctamente. Me alegra que te guste la idea. ¡Buena suerte!
2

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

De hecho, he utilizado este enfoque (en mi motor de renderizado) donde cada interfaz definió el comportamiento que el subsistema consumidor esperaba. 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" e intenté mantener los campos y métodos lo más independientes posible.

El enfoque de usar interfaces para definir patrones de comportamiento también concuerda conmigo porque los parámetros genéricos, las restricciones y los tipos de variante 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.

Y yo
fuente
¿Tuviste algún problema o en algún momento te arrepentiste de ese diseño y tuviste que luchar con tu propio código para realmente hacer las cosas intencionadas? como por ejemplo, de repente resultó que necesita crear tantas implementaciones de comportamiento que simplemente no tenía sentido y deseaba simplemente apegarse al viejo tema "crear clase base con millones de virtuales y simplemente heredar y anular el infierno".
grapkulec 01 de