Abstrakcyjna klasa bazowa z interfejsami jako zachowania?

14

Muszę zaprojektować hierarchię klas dla mojego projektu w języku C #. Zasadniczo funkcje klas są podobne do klas WinForms, więc weźmy na przykład zestaw narzędzi WinForms. (Nie mogę jednak użyć WinForms ani WPF.)

Istnieje kilka podstawowych właściwości i funkcjonalności, które musi zapewnić każda klasa. Wymiary, pozycja, kolor, widoczność (prawda / fałsz), metoda rysowania itp.

Potrzebuję porady projektowej. Użyłem projektu z abstrakcyjną klasą bazową i interfejsami, które nie są tak naprawdę typami, ale bardziej zachowaniami. Czy to dobry projekt? Jeśli nie, jaki byłby lepszy projekt.

Kod wygląda następująco:

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

Niektóre kontrolki mogą zawierać inne kontrolki, niektóre mogą być zawarte tylko (jako dzieci), więc myślę o stworzeniu dwóch interfejsów dla tych funkcji:

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 steruje wyświetlanym tekstem, więc przechodzi on również do interfejsu:

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

    void DrawText();
}

Niektóre elementy sterujące można zadokować w obrębie elementu nadrzędnego, aby:

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

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

... a teraz stwórzmy konkretne klasy:

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

Pierwszym „problemem”, jaki mogę tutaj wymyślić, jest to, że interfejsy są zasadniczo zepsute po ich opublikowaniu. Załóżmy jednak, że będę w stanie sprawić, że moje interfejsy będą wystarczająco dobre, aby uniknąć konieczności wprowadzania w nich zmian w przyszłości.

Innym problemem, jaki widzę w tym projekcie, jest to, że każda z tych klas musiałaby zaimplementować swoje interfejsy i szybko nastąpi duplikacja kodu. Na przykład w Label i Button metoda DrawText () pochodzi z interfejsu ITextHolder lub w każdej klasie pochodzącej z zarządzania dziećmi przez IContainer.

Moim rozwiązaniem tego problemu jest wdrożenie tej „zduplikowanej” funkcjonalności w dedykowanych adapterach i przekazywanie do nich wywołań. Zatem zarówno Label, jak i Button miałyby element TextHolderAdapter, który byłby wywoływany w metodach odziedziczonych z interfejsu ITextHolder.

Myślę, że ten projekt powinien uchronić mnie przed wieloma powszechnymi funkcjami w klasie bazowej, które mogłyby szybko zostać rozdęte wirtualnymi metodami i niepotrzebnym „kodem szumu”. Zmiany w zachowaniu zostałyby osiągnięte poprzez rozszerzenie adapterów, a nie klas pochodnych od Control.

Myślę, że nazywa się to „strategią” i chociaż na ten temat są miliony pytań i odpowiedzi, chciałbym zapytać o opinie na temat tego, co biorę pod uwagę przy tym projekcie i jakie wady można sobie wyobrazić w moje podejście.

Powinienem dodać, że istnieje prawie 100% szansa, że ​​przyszłe wymagania będą wymagać nowych klas i nowych funkcjonalności.

grapkulec
źródło
Dlaczego po prostu nie dziedziczą System.ComponentModel.Componentlub System.Windows.Forms.Controllub którykolwiek z pozostałych istniejących klas bazowych? Dlaczego musisz stworzyć własną hierarchię kontroli i zdefiniować wszystkie te funkcje od nowa?
Cody Gray
3
IChildwygląda na okropne imię.
Raynos
@Cody: po pierwsze, nie mogę używać zestawów WinForms lub WPF, po drugie - daj spokój, to tylko przykład problemu projektowego, o który pytam. Jeśli łatwiej ci myśleć o kształtach lub zwierzętach. moje klasy muszą zachowywać się podobnie do kontrolek WinForm, ale nie pod każdym względem i nie do końca tak jak one
grapkulec
2
Nie ma większego sensu stwierdzenie, że nie można używać zestawów WinForms lub WPF, ale można użyć dowolnej niestandardowej struktury sterowania, którą utworzysz. To poważny przypadek ponownego wynalezienia koła i nie mogę sobie wyobrazić, w jakim możliwym celu. Ale jeśli absolutnie musisz, dlaczego nie pójść za przykładem kontrolek WinForm? To sprawia, że ​​to pytanie jest prawie nieaktualne.
Cody Gray
1
@graphkulec dlatego jest to komentarz :) Gdybym miał naprawdę przydatne informacje zwrotne, odpowiedziałbym na twoje pytanie. To wciąż okropne imię;)
Raynos

Odpowiedzi:

5

[1] Dodaj wirtualne „pobierające” i „ustawiające” do swoich właściwości, musiałem zhakować inną bibliotekę kontrolną, ponieważ potrzebuję tej funkcji:

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

Wiem, że ta sugestia jest bardziej „pełna” lub złożona, ale bardzo przydatna w prawdziwym świecie.

[2] Dodaj właściwość „IsEnabled”, nie myl z „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

Oznacza, że ​​być może będziesz musiał pokazać swoją kontrolę, ale nie pokazuj żadnych informacji.

[3] Dodaj właściwość „IsReadOnly”, nie myl z „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

Oznacza to, że formant może wyświetlać informacje, ale użytkownik nie może go zmienić.

umlcat
źródło
chociaż nie lubię tych wszystkich wirtualnych metod w klasie bazowej, dam im drugą szansę w myśleniu o projektowaniu. i przypomniałeś mi, że naprawdę potrzebuję własności IsReadOnly :)
grapkulec
@grapkulec Ponieważ jest to klasa podstawowa, musisz określić, że klasy pochodne będą miały te właściwości, ale metody, które kontrolują zachowanie, mogą być zmieniane po każdej klasie ;-)
umlcat
mmkey, rozumiem twój punkt widzenia i dziękuję za
poświęcenie
1
Zaakceptowałem to jako odpowiedź, ponieważ kiedy zacząłem wdrażać mój „fajny” projekt z interfejsami, szybko potrzebowałem wirtualnych setterów, a czasem także getterów. To nie znaczy, że w ogóle nie używam interfejsów, ograniczyłem ich użycie do miejsc, w których są naprawdę potrzebne.
grapkulec
3

Fajne pytanie! Pierwszą rzeczą, którą zauważam, jest to, że metoda losowania nie ma żadnych parametrów, gdzie ona rysuje? Myślę, że powinien otrzymać jakiś obiekt Surface lub Graphics jako parametr (oczywiście jako interfejs).

Kolejną rzeczą, która wydaje mi się dziwna, jest interfejs IContainer. Ma GetChildmetodę, która zwraca pojedyncze dziecko i nie ma parametrów - może powinna zwrócić kolekcję lub jeśli przeniesiesz metodę Draw do interfejsu, mogłaby następnie zaimplementować ten interfejs i mieć metodę rysowania, która może narysować swoją wewnętrzną kolekcję bez ujawniania jej .

Być może dla pomysłów można również przyjrzeć się WPF - moim zdaniem jest to bardzo dobrze zaprojektowane środowisko. Możesz także rzucić okiem na wzory projektowe Kompozycja i Dekorator. Pierwszy może być przydatny w przypadku elementów kontenera, a drugi w celu dodania funkcjonalności do elementów. Nie mogę powiedzieć, jak dobrze to zadziała na pierwszy rzut oka, ale oto, co myślę:

Aby uniknąć powielania, możesz mieć coś w rodzaju elementów pierwotnych - takich jak element tekstowy. Następnie możesz budować bardziej skomplikowane elementy za pomocą tych prymitywów. Na przykład a Borderjest elementem, który ma jedno dziecko. Kiedy wywołujesz tę Drawmetodę, rysuje ramkę i tło, a następnie rysuje swoje dziecko wewnątrz ramki. A TextElementtylko rysuje tekst. Teraz, jeśli chcesz przycisk, możesz go zbudować, tworząc te dwa prymitywy, tj. Wstawiając tekst do ramki. Tutaj granica jest czymś w rodzaju dekoratora.

Wpis jest dość długi, więc daj mi znać, jeśli uważasz, że ten pomysł jest interesujący, a ja mogę podać kilka przykładów lub więcej wyjaśnień.

Georgi Stoyanov
źródło
1
brakujące parametry nie stanowią problemu, to po prostu szkic prawdziwych metod. Jeśli chodzi o WPF, zaprojektowałem układ układu na podstawie ich pomysłu, więc myślę, że mam już część kompozycyjną, ponieważ w przypadku dekoratorów będę musiał o tym pomyśleć. twój pomysł na prymitywne elementy brzmi rozsądnie i zdecydowanie próbuję. dzięki!
grapkulec
@grapkulec Nie ma za co! O params - tak, jest ok, chciałem tylko upewnić się, że poprawnie rozumiem twoje intencje. Cieszę się, że podoba ci się ten pomysł. Powodzenia!
2

Wycinanie kodu i przejście do sedna pytania „Czy mój projekt z abstrakcyjną klasą bazową i interfejsami nie jest typem, ale bardziej, że zachowania są dobrym projektem, czy nie.”, Powiedziałbym, że nie ma nic złego w tym podejściu.

W rzeczywistości zastosowałem takie podejście (w moim silniku renderującym), w którym każdy interfejs określał zachowanie, jakiego oczekiwał podsystem konsumujący. Na przykład IUpdateable, ICollidable, IRenderable, ILoadable, ILoader i tak dalej. Dla jasności podzieliłem również każde z tych „zachowań” na osobne klasy cząstkowe, takie jak „Entity.IUpdateable.cs”, „Entity.IRenderable.cs” i starałem się zachować pola i metody tak niezależne, jak to możliwe.

Podejście polegające na stosowaniu interfejsów do definiowania wzorców behawioralnych jest również zgodne ze mną, ponieważ parametry ogólne, ograniczenia i parametry typu wariantu co (ntra) działają bardzo dobrze razem.

Jedną z rzeczy, które zauważyłem w tej metodzie projektowania, było: Jeśli kiedykolwiek zdołasz zdefiniować interfejs za pomocą więcej niż kilku metod, prawdopodobnie nie wdrażasz takiego zachowania.

Ani
źródło
Czy napotkałeś jakieś problemy lub w którymkolwiek momencie żałowałeś takiego projektu i musiałeś walczyć z własnym kodem, aby faktycznie osiągnąć zamierzony cel? na przykład nagle okazało się, że musisz stworzyć tak wiele implementacji zachowań, że to po prostu nie miało sensu i żałowałeś, że nie przywiązałeś się do starego tematu: „stwórz klasę bazową z milionami wirtuozów i po prostu odziedzicz i wypierdź z tego piekło”?
grapkulec