Classe base abstrata com interfaces como comportamentos?

14

Eu preciso criar uma hierarquia de classes para o meu projeto c #. Basicamente, as funcionalidades da classe são semelhantes às classes WinForms, portanto, tomemos o kit de ferramentas WinForms como exemplo. (No entanto, não posso usar o WinForms ou o WPF.)

Existem algumas propriedades e funcionalidades essenciais que toda classe precisa fornecer. Dimensões, posição, cor, visibilidade (verdadeiro / falso), método Draw, etc.

Preciso de conselhos de design. Utilizei um design com uma classe base abstrata e interfaces que não são realmente tipos, mas sim comportamentos. Esse é um bom design? Caso contrário, qual seria um design melhor.

O código fica assim:

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

Alguns controles podem conter outros controles, alguns podem ser contidos apenas (como filhos), por isso estou pensando em fazer duas interfaces para essas 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);
}

Os controles WinForms exibem o texto, então isso também entra na interface:

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

    void DrawText();
}

Alguns controles podem ser encaixados dentro do controle pai, portanto:

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

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

... e agora vamos criar algumas classes concretas:

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

O primeiro "problema" em que posso pensar aqui é que as interfaces são basicamente definidas quando são publicadas. Mas vamos supor que poderei tornar minhas interfaces boas o suficiente para evitar a necessidade de fazer alterações nelas no futuro.

Outro problema que vejo neste design é que todas essas classes precisariam implementar suas interfaces e a duplicação de código ocorrerá rapidamente. Por exemplo, em Label e Button, o método DrawText () deriva da interface ITextHolder ou em todas as classes derivadas do gerenciamento de filhos do IContainer.

Minha solução para esse problema é implementar essas funcionalidades "duplicadas" em adaptadores dedicados e encaminhar chamadas para eles. Portanto, Label e Button teriam um membro TextHolderAdapter que seria chamado dentro de métodos herdados da interface ITextHolder.

Eu acho que esse design deve me impedir de ter muitas funcionalidades comuns na classe base, que podem rapidamente ficar inchadas com métodos virtuais e "código de ruído" desnecessário. Alterações de comportamento seriam realizadas estendendo adaptadores e não classes derivadas de controle.

Acho que isso é chamado de padrão "Estratégia" e, embora existam milhões de perguntas e respostas sobre esse tópico, gostaria de pedir suas opiniões sobre o que levo em consideração para esse design e quais falhas você pode encontrar em minha abordagem.

Devo acrescentar que há quase 100% de chance de que requisitos futuros exijam novas classes e novas funcionalidades.

grapkulec
fonte
Por que não simplesmente herdar de System.ComponentModel.Componentou de System.Windows.Forms.Controlqualquer outra classe base existente? Por que você precisa criar sua própria hierarquia de controle e definir todas essas funções novamente do zero?
Cody Gray
3
IChildparece um nome horrível.
Raynos 01/07/11
@ Cody: primeiro, não posso usar assemblagens WinForms ou WPF; segundo, vamos lá, é apenas um exemplo de um problema de design que estou perguntando. Se for mais fácil para você pensar em formas ou animais. minhas aulas precisam se comportar semelhantes aos controles WinForms, mas não em todos os sentidos e não exatamente como eles
grapkulec
2
Não faz muito sentido dizer que você não pode usar os assemblies WinForms ou WPF, mas poderá usar qualquer estrutura de controle personalizada que criar. Este é um caso sério de reinventar a roda, e não consigo imaginar com que finalidade possível. Mas se você absolutamente deve, por que não seguir o exemplo dos controles WinForms? Isso torna essa pergunta praticamente obsoleta.
Cody Gray
1
@graphkulec é por isso que é um comentário :) Se eu tivesse um feedback realmente útil, teria respondido sua pergunta. Ainda é um nome horrível;)
Raynos

Respostas:

5

[1] Adicione "getters" e "setters" virtuais às suas propriedades. Eu tive que invadir outra biblioteca de controle, porque preciso desse recurso:

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

Eu sei que essa sugestão é mais "detalhada" ou complexa, mas é muito útil no mundo real.

[2] Adicione uma propriedade "IsEnabled", não confunda com "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 você pode ter que mostrar seu controle, mas não mostra nenhuma informação.

[3] Adicione uma propriedade "IsReadOnly", não confunda com "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

Isso significa que o controle pode exibir informações, mas não pode ser alterado pelo usuário.

umlcat
fonte
embora eu não goste de todos esses métodos virtuais na classe base, darei a eles uma segunda chance no meu pensamento sobre design. e você me lembrou que eu realmente preciso de propriedade IsReadOnly :)
grapkulec
@grapkulec Desde que sua classe base, você deve especificar que as classes derivadas terão essas propriedades, mas, que os métodos que o comportamento de controle pode ser mudança a cada purpouse classe ;-)
umlcat
mmkey, eu vejo o seu ponto e obrigado por ter tempo para resposta
grapkulec
1
Aceitei isso como uma resposta, porque, depois que comecei a implementar meu design "legal" com interfaces, rapidamente me deparei com setters virtuais e, às vezes, getters também. Isso não significa que eu não uso interfaces, apenas limitei o uso a locais onde eles são realmente necessários.
grapkulec
3

Boa pergunta! A primeira coisa que noto é que o método draw não possui nenhum parâmetro, onde ele desenha? Eu acho que ele deve receber algum objeto Surface ou Graphics como parâmetro (como interface, é claro).

Mais uma coisa que acho estranha é a interface do IContainer. Ele possui um GetChildmétodo que retorna um único filho e não possui parâmetros - talvez devesse retornar uma coleção ou, se você mover o método Draw para uma interface, ele poderá implementar essa interface e ter um método de desenho que possa desenhar sua coleção subordinada interna sem expô-la .

Talvez para idéias que você possa ver também o WPF - na minha opinião, é um quadro muito bem projetado. Além disso, você pode dar uma olhada nos padrões de design Composition e Decorator. O primeiro pode ser útil para elementos de contêiner e o segundo para adicionar funcionalidade aos elementos. Não posso dizer o quão bem isso funcionará à primeira vista, mas eis o que penso:

Para evitar a duplicação, você pode ter algo como elementos primitivos - como o elemento de texto. Em seguida, você pode criar os elementos mais complicados usando essas primitivas. Por exemplo, a Borderé um elemento que possui um único filho. Quando você chama seu Drawmétodo, ele desenha uma borda e um plano de fundo e depois desenha seu filho dentro da borda. A TextElementapenas desenha algum texto. Agora, se você quiser um botão, poderá criar um compondo essas duas primitivas, ou seja, colocando o texto dentro da borda. Aqui a fronteira é algo como um decorador.

O post está ficando bastante longo, então deixe-me saber se você acha essa ideia interessante e posso dar alguns exemplos ou mais explicações.

Georgi Stoyanov
fonte
1
parâmetros ausentes não são um problema, é apenas um esboço de métodos reais, afinal. Em relação ao WPF, eu projetei o sistema de layout com base em sua ideia, então acho que já tenho uma parte de composição em prática, pois para os decoradores, terei que pensar sobre isso. sua idéia de elementos primitivos parece razoável e eu definitivamente experimentei. obrigado!
grapkulec
@grapkulec De nada! Sobre os parâmetros - sim, tudo bem, eu só queria ter certeza de que entendi suas intensões corretamente. Estou feliz que você gostou da ideia. Boa sorte!
2

Cortando o código e indo para o centro da sua pergunta "O meu design com classe base abstrata e interfaces não é como tipos, mas mais como comportamentos é bom design ou não.", Eu diria que não há nada de errado com essa abordagem.

Na verdade, usei essa abordagem (no meu mecanismo de renderização) em que cada interface definia o comportamento esperado pelo subsistema consumidor. Por exemplo, IUpdateable, ICollidable, IRenderable, ILoadable, ILoader e assim por diante. Para maior clareza, também separei cada um desses "comportamentos" em classes parciais separadas, como "Entity.IUpdateable.cs", "Entity.IRenderable.cs" e tentei manter os campos e métodos o mais independentes possível.

A abordagem do uso de interfaces para definir padrões comportamentais também concorda comigo, porque os parâmetros genéricos, de restrição e de tipo co (ntra) variante funcionam muito bem juntos.

Uma das coisas que observei sobre esse método de design foi: se você definir uma interface com mais de um punhado de métodos, provavelmente não estará implementando um comportamento.

Ani
fonte
você teve algum problema ou em algum momento estava se arrependendo de tal design e teve que lutar com seu próprio código para realmente realizar as coisas pretendidas? como, por exemplo, de repente, você descobriu que precisava criar tantas implementações de comportamento que simplesmente não fazia sentido e que desejava se apegar ao tema antigo "criar classe base com milhões de virtuais e simplesmente herdar e substituir isso"?
grapkulec