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.
źródło
System.ComponentModel.Component
lubSystem.Windows.Forms.Control
lub którykolwiek z pozostałych istniejących klas bazowych? Dlaczego musisz stworzyć własną hierarchię kontroli i zdefiniować wszystkie te funkcje od nowa?IChild
wygląda na okropne imię.Odpowiedzi:
[1] Dodaj wirtualne „pobierające” i „ustawiające” do swoich właściwości, musiałem zhakować inną bibliotekę kontrolną, ponieważ potrzebuję tej funkcji:
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”:
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”:
Oznacza to, że formant może wyświetlać informacje, ale użytkownik nie może go zmienić.
źródło
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
GetChild
metodę, 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
Border
jest elementem, który ma jedno dziecko. Kiedy wywołujesz tęDraw
metodę, rysuje ramkę i tło, a następnie rysuje swoje dziecko wewnątrz ramki. ATextElement
tylko 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ń.
źródło
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.
źródło