Czy to zły projekt OOP do symulacji z udziałem interfejsów?

13

Projektuję swój mały program OOP do symulacji wampirów, wilków, ludzi i ciężarówek i staram się wdrożyć moje własne ograniczone rozumienie interfejsów.

( Nadal tu streszczam i nie mam jeszcze implementacji kodu, więc jest to raczej kwestia projektu OOP ... myślę!)

Czy mam rację, szukając „wspólnego zachowania” między tymi klasami i wdrażając je jako interfejsy ?

Na przykład gryzą wampiry i wilki ... więc powinienem mieć interfejs gryza?

public class Vampire : Villain, IBite, IMove, IAttack

Podobnie w przypadku samochodów ciężarowych ...

public class Truck : Vehicle, IMove

A dla ludzi ...

public class Man : Human, IMove, IDead

Czy moje myślenie jest tutaj? (Doceniam Twoją pomoc)

użytkownik3396486
źródło
14
Zwierzęta, warzywa i minerały rzadko stanowią dobry przykład realizacji aplikacji. Rzeczywiste implementacje są bardziej abstrakcyjne, jak IEnumerable, IEquatableitp
Robert Harvey
6
Masz jedną wzmiankę o tym, co Twoje obiekty zamierzają zrobić w twoim oprogramowaniu („ugryzienie”). Oprogramowanie jest zwykle zaprojektowane do robienia czegoś, opieranie modelu obiektowego tylko na cechach nie prowadzi nigdzie.
tofro
@tofro Moim zamiarem było, aby IBite zawierał wiele metod, które wdrażałyby zachowanie dotyczące (1) Zmniejszenia poziomu „życia / energii” innej osoby (2) Pojawienia się lub wywołania grafiki „krwi” i (3) aktualizacji statyki symulacji dane (takie jak NoOfBites). Myślę, że mogę docenić to, że interfejs najlepiej nadaje się do implementacji szeregu zachowań metod.
user3396486,
2
Czy klasy Human, Vampire i Vehicle nie implementują już interfejsu IMove? Dlaczego musisz sprawić, by podklasy zaimplementowały go zbyt wyraźnie?
Pierre Arlaud,
Czy wszystkie te interfejsy są naprawdę potrzebne? Na szczęście w Pythonie nie potrzebujesz tych rzeczy, co było naprawdę odświeżającą zmianą (moim pierwszym językiem był Object Pascal). W niektórych przypadkach lepszym rozwiązaniem mogą być również metody wirtualne.
Ajasja

Odpowiedzi:

33

Ogólnie rzecz biorąc, chcesz mieć interfejsy dla wspólnych cech swoich klanów.

Częściowo zgadzam się z @Robert Harvey w komentarzach, który powiedział, że zwykle interfejsy reprezentują bardziej abstrakcyjne cechy klas. Niemniej jednak uważam, że począwszy od bardziej konkretnych przykładów dobry sposób na rozpoczęcie myślenia abstrakcyjnego.

Chociaż twój przykład jest technicznie poprawny (tzn. Tak, zarówno wampiry, jak i wilki gryzą, więc możesz mieć do tego interfejs), istnieje jednak kwestia istotności. Każdy obiekt ma tysiące cech (np. Zwierzęta mogą mieć futro, mogą pływać, wspinać się na drzewa itd.). Czy stworzysz interfejs dla nich wszystkich? Bardzo mało prawdopodobne.

Zwykle chcesz, aby interfejsy dla rzeczy, które mają sens, były grupowane w aplikacji jako całości. Na przykład, jeśli budujesz grę, możesz mieć szereg obiektów IMove i zaktualizować ich pozycję. Jeśli nie chcesz tego robić, interfejs IMove jest całkiem bezużyteczny.

Chodzi o to, żeby nie przesadzać z inżynierem. Musisz pomyśleć o tym, jak zamierzasz korzystać z tego interfejsu, a dwie klasy posiadające wspólną metodę nie są wystarczającym powodem do utworzenia interfejsu.

Paul92
źródło
1
Z pewnością mam nadzieję, że każdy obiekt nie ma tysięcy atrybutów.
ogrodnik
4
Atrybuty nie jak w atrybutach oop, ale atrybuty / cechy gramatyczne (takie jak policzalne, porównywalne itp.): D. Zły wybór słów.
Paul92
3
Warto zauważyć, że przydatne są interfejsy. Na przykład IBitenie jest szczególnie przydatny, ale możesz chcieć IAttack, abyś mógł pracować nad wszystkimi rzeczami, które robią ataki, lub IUpdateabyś mógł uruchamiać aktualizacje dla wszystkiego lub IPhysicsEnabledabyś mógł zastosować do nich fizykę itp.
Anaximander
1
Ta odpowiedź podnosi kilka bardzo dobrych punktów. Ostatni akapit podsumowuje to całkiem dobrze; co najmniej tak dobrze, jak to możliwe, z podanym poziomem szczegółowości.
Wyścigi lekkości na orbicie
1
Grupowanie wspólnych metod bardziej pasuje do klas abstrakcyjnych. Interfejs służy do projektowania umów, które muszą szanować tych, którzy go wdrażają, nie grupując tej samej implementacji dla niektórych obiektów.
Walfrat
29

Wygląda na to, że tworzysz kilka interfejsów z pojedynczą metodą . Na pierwszy rzut oka jest to w porządku, ale należy pamiętać, że interfejsy nie są własnością klas, które je implementują. Są własnością klientów, którzy ich używają. Klienci decydują, czy coś musi być czymś, co może się poruszać i atakować.

Jeśli mam Combatklasę z fight()metodą, która metoda prawdopodobnie ma potrzeby, aby zadzwonić zarówno move()i attack()na tym samym obiekcie. To zdecydowanie sugeruje potrzebę ICombatantinterfejsu, który fight()można wywoływać move()i attack()przesyłać. Jest to czystsze niż fight()wzięcie IAttackobiektu i rzucenie go, IMoveaby sprawdzić, czy może się również poruszać.

To nie znaczy, że nie możesz mieć także IMove IAttackinterfejsów. Mam tylko nadzieję, że nie robisz ich bez jakiegoś klienta, który ich potrzebuje. I odwrotnie, jeśli żaden klient nigdy nie musi zmuszać obiektu do ruchu i ataku, ICombatantnie jest to konieczne.

Ten prosty sposób patrzenia na interfejsy jest często gubiony, ponieważ ludzie lubią następujące przykłady. Pierwsze interfejsy, na które jesteśmy narażeni, znajdują się w bibliotekach. Niestety biblioteki nie mają pojęcia, czym są ich klienci. Mogą więc zgadywać tylko potrzeby swoich klientów. Nie najlepszy przykład do naśladowania.

candied_orange
źródło
1
Cholera, to dobrze. Granie wydaje się być naprawdę dobrym sposobem na użycie i wyjaśnienie OOP.
JeffO
7
@JeffO, dopóki nie wdrożysz dość dużej gry i nie uświadomisz sobie, że OOP jest gorącym bałaganem i lepiej byłoby, gdybyś był oparty na komponentach lub projektach zorientowanych na dane.
Darkhogg,
„Interfejsy są własnością klientów, którzy ich używają”
Tibos,
1
+1 za różnicę między bibliotekami a aplikacjami, często (za dużo?: /) Czytam mnóstwo rzeczy, które pasują do jednej, a nie drugiej.
Walfrat
3

Zastanów się, czy powszechne będzie posiadanie kolekcji obiektów o różnych kombinacjach umiejętności i czy kod może chcieć wykonać akcję na tych elementach w obrębie kolekcji, które je obsługują . Jeśli tak, i jeśli byłoby rozsądne „domyślne zachowanie” dla obiektów, które nie mają użytecznego wsparcia dla niektórych działań, pomocne może być wdrożenie interfejsów przez szeroką gamę klas, a nie tylko tych, które mogą zachowywać się pożytecznie.

Załóżmy na przykład, że tylko kilka rodzajów stworów może mieć Woozle i ktoś chce, aby takie stworzenia miały NumerOfWoozleswłasność. Gdyby taka właściwość znajdowała się w interfejsie, który został zaimplementowany tylko przez stworzenia, które mogą mieć Woozle, wówczas kod, który chciał znaleźć całkowitą liczbę Woozles przechowywanych przez zbiór stworzeń mieszanych typów, musiałby powiedzieć coś w stylu:

int total = 0;
foreach (object it in creatures)
{
   IWoozleCountable w = trycast(it, IWoozleCountable);
   if (w != null) total += w.WoozleCount;
}

Gdyby jednak WoozleCount było członkiem Creature / ICreature, chociaż kilka podtypów zastąpiłoby domyślną implementację WoozleCount Creature, która zawsze zwraca zero, kod można uprościć w celu:

int total = 0;
foreach (ICreature it in creatures)
   total += it.WoozleCount;

Chociaż niektórzy ludzie mogą się obawiać, że każde stworzenie stworzy właściwość WoozleCount, która jest naprawdę użyteczna tylko dla kilku podtypów, właściwość ta byłaby znacząca dla wszystkich typów, niezależnie od tego, czy przydałaby się w przypadku elementów tego typu, i uważałbym, że interfejs „zlewu kuchennego” jest mniej zapachowy niż operator Trycast.

supercat
źródło