Próbuję modelować grę karcianą, w której karty mają dwa ważne zestawy funkcji:
Pierwszy to efekt. Są to zmiany stanu gry, które mają miejsce podczas gry w kartę. Interfejs efektu jest następujący:
boolean isPlayable(Player p, GameState gs);
void play(Player p, GameState gs);
Możesz uznać kartę za możliwą do zagrania tylko wtedy, gdy jesteś w stanie pokryć jej koszt, a wszystkie jej efekty można zagrać. Tak jak:
// in Card class
boolean isPlayable(Player p, GameState gs) {
if(p.resource < this.cost) return false;
for(Effect e : this.effects) {
if(!e.isPlayable(p,gs)) return false;
}
return true;
}
Jak dotąd, całkiem proste.
Innym zestawem funkcji na karcie są zdolności. Te umiejętności to zmiany stanu gry, które możesz aktywować do woli. Kiedy wymyśliłem interfejs dla nich, zdałem sobie sprawę, że potrzebowali metody określania, czy można je aktywować, czy nie, oraz metody implementacji aktywacji. W końcu jest
boolean isActivatable(Player p, GameState gs);
void activate(Player p, GameState gs);
I zdaję sobie sprawę, że z wyjątkiem nazywania go „aktywuj” zamiast „graj” Ability
i Effect
mam dokładnie taki sam podpis.
Czy źle jest mieć wiele interfejsów z identycznym podpisem? Czy powinienem po prostu użyć jednego i mieć dwa zestawy tego samego interfejsu? Tak więc:
Set<Effect> effects;
Set<Effect> abilities;
Jeśli tak, to jakie kroki refaktoryzacyjne powinienem podjąć, jeśli stają się one nieidentyczne (w miarę uwalniania większej liczby funkcji), szczególnie jeśli są rozbieżne (tj. Oboje zyskują coś, czego nie powinni inni, a nie tylko jedną zdobywają a drugi jest kompletnym podzbiorem)? Szczególnie martwię się, że ich połączenie nie będzie zrównoważone, gdy tylko coś się zmieni.
Drobnym drukiem:
Zdaję sobie sprawę, że to pytanie powstaje podczas tworzenia gier, ale wydaje mi się, że jest to rodzaj problemu, który równie łatwo może wkraść się w programowanie inne niż gry, szczególnie przy próbie dostosowania modeli biznesowych wielu klientów w jednej aplikacji, jak to się dzieje w przypadku prawie każdy projekt, który kiedykolwiek zrealizowałem z więcej niż jednym wpływem biznesowym ... Ponadto, fragmenty są fragmentami Java, ale równie dobrze można je zastosować do wielu języków obiektowych.
źródło
Odpowiedzi:
To, że dwa interfejsy mają tę samą umowę, nie oznacza, że są one tym samym interfejsem.
Zasada substytucji Liskowa stanowi:
Lub innymi słowy: wszystko, co jest zgodne z interfejsem lub nadtypem, powinno być prawdziwe dla wszystkich jego podtypów.
Jeśli dobrze rozumiem twój opis, zdolność nie jest efektem, a efekt nie jest zdolnością. Jeśli jedno z nich zmieni umowę, jest mało prawdopodobne, że druga ulegnie zmianie. Nie widzę żadnego dobrego powodu, aby próbować je ze sobą wiązać.
źródło
Z Wikipedii : „ Interfejs jest często używany do zdefiniowania typu abstrakcyjnego, który nie zawiera danych, ale ujawnia zachowania zdefiniowane jako metody ”. Moim zdaniem interfejs służy do opisu zachowania, więc jeśli masz różne zachowania, sensowne jest mieć różne interfejsy. Czytając twoje pytanie, mam wrażenie, że mówisz o różnych zachowaniach, więc różne interfejsy mogą być najlepszym podejściem.
Innym punktem, który sam powiedziałeś, jest to, że jeśli jedno z tych zachowań się zmieni. Co się wtedy stanie, gdy masz tylko jeden interfejs?
źródło
Jeśli zasady twojej gry karcianej rozróżniają „efekty” i „zdolności”, musisz upewnić się, że są to różne interfejsy. Zapobiegnie to przypadkowemu użyciu jednego z nich, gdy drugi jest wymagany.
To powiedziawszy, jeśli są bardzo do siebie podobne, sensowne może być wyprowadzenie ich od wspólnego przodka. Starannie rozważyć: czy masz powody, by wierzyć, że „efekty” i „umiejętności” będzie zawsze muszą mieć ten sam interfejs? Jeśli dodasz element do
effect
interfejsu, czy spodziewałbyś się, że ten sam element zostanie dodany doability
interfejsu?Jeśli tak, możesz umieścić takie elementy we wspólnym
feature
interfejsie, z którego oba pochodzą. Jeśli nie, to nie powinieneś próbować ich ujednolicać - zmarnujesz swój czas na przenoszenie rzeczy między interfejsem podstawowym a pochodnym. Ponieważ jednak nie zamierzasz używać wspólnego interfejsu podstawowego do niczego, ale „nie powtarzaj się”, może to nie mieć większego znaczenia. I, jeśli trzymasz się tej intencji, domyślam się, że jeśli dokonasz złego wyboru na początku, refaktoryzacja w celu naprawy go później może być stosunkowo prosta.źródło
To, co widzisz, jest w zasadzie artefaktem ograniczonej ekspresyjności systemów typów.
Teoretycznie, jeśli twój system typów pozwolił ci dokładnie określić zachowanie tych dwóch interfejsów, wówczas ich podpisy byłyby różne, ponieważ ich zachowania są różne. W praktyce ekspresyjność systemów typów jest ograniczona podstawowymi ograniczeniami, takimi jak problem zatrzymania i twierdzenie Rice'a, więc nie można wyrazić każdego aspektu zachowania.
Teraz różne systemy typów mają różny stopień ekspresji, ale zawsze będzie coś, czego nie da się wyrazić.
Na przykład: dwa interfejsy, które mają różne zachowanie błędów, mogą mieć tę samą sygnaturę w języku C #, ale nie w Javie (ponieważ w Javie wyjątki są częścią podpisu). Dwa interfejsy, których zachowanie różni się jedynie efektami ubocznymi, mogą mieć tę samą sygnaturę w Javie, ale będą miały różne sygnatury w Haskell.
Dlatego zawsze jest możliwe, że skończysz z tym samym podpisem dla różnych zachowań. Jeśli uważasz, że ważne jest, aby móc rozróżnić te dwa zachowania na więcej niż tylko poziomie nominalnym, musisz przełączyć się na bardziej (lub inny) ekspresyjny system typów.
źródło