Kontynuuję to pytanie , ale skupiam się z kodu na zasadzie.
Z mojego zrozumienia zasady podstawienia Liskowa (LSP), wszystkie metody w mojej klasie bazowej muszą być zaimplementowane w mojej podklasie i zgodnie z tą stroną, jeśli zastąpisz metodę w klasie bazowej i nie robi ona nic ani nie rzuca wyjątek, naruszasz zasadę.
Teraz mój problem można podsumować w następujący sposób: mam streszczenie Weapon
class
oraz dwie klasy Sword
i Reloadable
. Jeśli Reloadable
zawiera konkretny method
, zwany Reload()
, musiałbym zejść na dół, aby uzyskać do niego dostęp method
, i najlepiej byłoby tego uniknąć.
Potem pomyślałem o użyciu Strategy Pattern
. W ten sposób każda broń była świadoma działań, które może wykonać, więc na przykład Reloadable
broń może oczywiście przeładować, ale Sword
nie może, a nawet nie jest świadoma Reload class/method
. Jak stwierdziłem w moim wpisie Przepełnienie stosu, nie muszę spuszczać i mogę zachować List<Weapon>
kolekcję.
Na innym forum pierwsza odpowiedź sugerowała , że możesz Sword
być świadomy Reload
, po prostu nic nie rób. Ta sama odpowiedź została podana na stronie Przepełnienie stosu, do której odsyłam powyżej.
Nie do końca rozumiem dlaczego. Po co naruszać tę zasadę i pozwolić, by Miecz był tego świadomy Reload
, i pozostawić to puste? Jak powiedziałem w moim wpisie Przepełnienie stosu, SP prawie rozwiązało moje problemy.
Dlaczego nie jest to realne rozwiązanie?
public final Weapon{
private final String name;
private final int damage;
private final List<AttackStrategy> validactions;
private final List<Actions> standardActions;
private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
{
this.name = name;
this.damage = damage;
standardActions = new ArrayList<Actions>(standardActions);
validAttacks = new ArrayList<AttackStrategy>(validActions);
}
public void standardAction(String action){} // -- Can call reload or aim here.
public int attack(String action){} // - Call any actions that are attacks.
public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
return new Weapon(name, damage,standardActions, attacks) ;
}
}
Interfejs ataku i implementacja:
public interface AttackStrategy{
void attack(Enemy enemy);
}
public class Shoot implements AttackStrategy {
public void attack(Enemy enemy){
//code to shoot
}
}
public class Strike implements AttackStrategy {
public void attack(Enemy enemy){
//code to strike
}
}
class Weapon { bool supportsReload(); void reload(); }
. Klienci sprawdziliby, czy są obsługiwani przed ponownym załadowaniem.reload
jest określona w umowie, aby rzucić IFF!supportsReload()
. Przestrzeganie klas kierowanych przez LSP iff jest zgodne z protokołem, który właśnie przedstawiłem.reload()
puste pole, czystandardActions
też nie zawiera operacji przeładowania, to po prostu inny mechanizm. Nie ma zasadniczej różnicy. Możesz zrobić jedno i drugie. => Twoje rozwiązanie jest realne (jakie było twoje pytanie) .; Miecz nie musi wiedzieć o przeładowaniu, jeśli Broń zawiera pustą domyślną implementację.Odpowiedzi:
LSP jest zaniepokojony podtypami i polimorfizmem. Nie cały kod faktycznie korzysta z tych funkcji, w takim przypadku LSP jest nieistotny. Dwa typowe przypadki użycia konstrukcji języka dziedziczenia, które nie są przypadkami podtypów, to:
Dziedziczenie używane do dziedziczenia implementacji klasy podstawowej, ale nie jej interfejsu. W prawie wszystkich przypadkach preferowana jest kompozycja. Języki takie jak Java nie mogą oddzielić dziedziczenia implementacji i interfejsu, ale np. C ++ ma
private
dziedziczenie.Dziedziczenie używane do modelowania typu sumy / unii, np .: a
Base
jest alboCaseA
alboCaseB
. Typ podstawowy nie deklaruje żadnego odpowiedniego interfejsu. Aby użyć jego instancji, musisz rzucić je na właściwy typ betonu. Odlewanie można wykonać bezpiecznie i nie stanowi to problemu. Niestety wiele języków OOP nie jest w stanie ograniczyć podtypów klas podstawowych tylko do podtypów zamierzonych. Jeśli kod zewnętrzny może utworzyć aCaseC
, to kod przy założeniu, że aBase
może być tylko aCaseA
lubCaseB
jest niepoprawny. Scala może to zrobić bezpiecznie dzięki swojejcase class
koncepcji. W Javie można to modelować, gdyBase
jest to klasa abstrakcyjna z prywatnym konstruktorem, a zagnieżdżone klasy statyczne dziedziczą następnie z bazy.Niektóre koncepcje, takie jak hierarchie pojęciowe rzeczywistych obiektów, bardzo źle odwzorowują się na modele obiektowe. Myśli takie jak „Pistolet jest bronią, a miecz jest bronią, dlatego będę miał
Weapon
klasę podstawową, z którejGun
iSword
odziedziczę”, są mylące: prawdziwe słowa to - relacje nie sugerują takiego związku w naszym modelu. Jednym pokrewnym problemem jest to, że obiekty mogą należeć do wielu hierarchii pojęciowych lub mogą zmieniać przynależność do hierarchii w czasie wykonywania, czego większość języków nie może modelować, ponieważ dziedziczenie jest zwykle dla klasy nie dla obiektu i zdefiniowane w czasie projektowania, a nie w czasie wykonywania.Projektując modele OOP, nie powinniśmy myśleć o hierarchii ani o tym, jak jedna klasa „rozszerza” inną. Klasa podstawowa nie jest miejscem do wyróżnienia wspólnych części wielu klas. Zamiast tego zastanów się, w jaki sposób będą używane Twoje obiekty, tj. Jakiego zachowania potrzebują użytkownicy tych obiektów.
Tutaj użytkownicy mogą potrzebować
attack()
broni, a może ireload()
oni. Jeśli mamy stworzyć hierarchię typów, wówczas obie te metody muszą być typu podstawowego, chociaż broń nieładowalna może zignorować tę metodę i nie robić nic, gdy zostanie wywołana. Zatem klasa podstawowa nie zawiera wspólnych części, ale połączony interfejs wszystkich podklas. Podklasy nie różnią się interfejsem, ale jedynie implementacją tego interfejsu.Nie jest konieczne tworzenie hierarchii. Dwa typy
Gun
iSword
mogą być całkowicie niezwiązane. NatomiastGun
puszkifire()
i tylko może . Jeśli potrzebujesz zarządzać tymi obiektami polimorficznie, możesz użyć Wzorca adaptera, aby uchwycić odpowiednie aspekty. W Javie 8 jest to możliwe dość wygodnie dzięki interfejsom funkcjonalnym i referencjom lambdas / metody. Np. Możesz mieć strategię, którą dostarczasz lub .reload()
Sword
strike()
Attack
myGun::fire
() -> mySword.strike()
Wreszcie czasami rozsądne jest unikanie jakichkolwiek podklas, ale modelowanie wszystkich obiektów za pomocą jednego typu. Jest to szczególnie istotne w grach, ponieważ wiele obiektów nie pasuje do żadnej hierarchii i może mieć wiele różnych możliwości. Np. W grze fabularnej może znajdować się przedmiot, który jest jednocześnie przedmiotem misji, wzmacnia statystyki o siłę +2, jeśli jest na wyposażeniu, ma 20% szans na zignorowanie otrzymanych obrażeń i zapewnia atak w zwarciu. A może miecz do ponownego załadowania, ponieważ jest to * magia *. Kto wie, czego wymaga ta historia.
Zamiast próbować ustalić hierarchię klas dla tego bałaganu, lepiej mieć klasę, która zapewnia miejsca dla różnych możliwości. Te miejsca można zmienić w czasie wykonywania. Każde miejsce będzie strategią / wywołaniem zwrotnym, takim jak
OnDamageReceived
lubAttack
. Ze swoimi broni, możemy miećMeleeAttack
,RangedAttack
iReload
szczeliny. Te gniazda mogą być puste, w którym to przypadku obiekt nie zapewnia takiej możliwości. Szczeliny są wówczas nazywane warunkowo:if (item.attack != null) item.attack.perform()
.źródło
Ponieważ posiadanie strategii
attack
nie jest wystarczające dla twoich potrzeb. Jasne, pozwala ci to wyrejestrować, jakie działania może wykonać przedmiot, ale co się stanie, gdy będziesz musiał znać zasięg broni? Czy pojemność amunicji? Lub jakiego rodzaju amunicji potrzeba? Wróciłeś do downcastingu, żeby się na to zdobyć. Posiadanie tego poziomu elastyczności sprawi, że interfejs użytkownika będzie nieco trudniejszy do wdrożenia, ponieważ będzie musiał mieć podobny wzór strategii, aby poradzić sobie ze wszystkimi możliwościami.Mimo wszystko nie zgadzam się szczególnie z odpowiedziami na pozostałe pytania. Mając
sword
Dziedziczweapon
jest przerażające, naiwny OO które niezmiennie prowadzi do metod no-op lub typu kontroli porozrzucane kodu.Ale u podstaw sprawy żadne rozwiązanie nie jest złe . Oba rozwiązania można wykorzystać do stworzenia przyjemnej gry. Każdy z nich ma własny zestaw kompromisów, tak jak każde wybrane rozwiązanie.
źródło
Weapon
klasy z instancją miecza i pistoletu.WeaponBuilder
który mógłby budować miecze i pistolety, komponując broń strategiczną.Oczywiście jest to realne rozwiązanie; to po prostu bardzo zły pomysł.
Problem nie polega na tym, że masz tę jedną instancję, w której przeładowujesz klasę podstawową. Problem polega na tym, że trzeba również umieścić „huśtawka”, „strzelać”, „parować”, „pukać”, „polerować”, „demontować”, „wyostrzać” i „wymieniać gwoździe spiczastego końca pałki” metoda w klasie bazowej.
Chodzi o to, że algorytmy najwyższego poziomu muszą działać i mieć sens. Więc jeśli mam taki kod:
Teraz, jeśli spowoduje to niezaimplementowany wyjątek i spowoduje awarię programu, to bardzo zły pomysł.
Jeśli twój kod wygląda tak,
wtedy twój kod może być zaśmiecony bardzo specyficznymi właściwościami, które nie mają nic wspólnego z abstrakcyjną ideą „broni”.
Jeśli jednak wdrażasz strzelankę FPS, a cała twoja broń może strzelać / przeładowywać, z wyjątkiem tego, że jeden nóż to (w twoim konkretnym kontekście) bardzo sensowne jest, aby przeładowanie noża nic nie robiło, ponieważ jest to wyjątek i szanse zaśmiecanie klasy podstawowej określonymi właściwościami jest niskie.
Aktualizacja: spróbuj pomyśleć o abstrakcyjnym przypadku / terminach. Na przykład, być może każda broń ma akcję „przygotowania”, która polega na przeładowaniu broni i nieosłonięciu mieczy.
źródło
Oczywiście jest OK, jeśli nie tworzysz podklasy z zamiarem zastąpienia instancji klasy podstawowej, ale jeśli tworzysz podklasę, używając klasy podstawowej jako wygodnego repozytorium funkcjonalności.
To, czy jest to dobry pomysł, czy nie, jest bardzo dyskusyjne, ale jeśli nigdy nie zastąpisz podklasy klasą podstawową, to fakt, że nie działa, nie stanowi problemu. Możesz mieć problemy, ale LSP nie jest problemem w tym przypadku.
źródło
LSP jest dobry, ponieważ pozwala kodowi wywołującemu nie martwić się działaniem klasy.
na przykład. Mogę wywołać Weapon.Attack () na wszystkich broniach zamontowanych na moim BattleMechu i nie martw się, że niektóre z nich mogą rzucić wyjątek i zawiesić grę.
Teraz w twoim przypadku chcesz rozszerzyć swój typ podstawowy o nową funkcjonalność. Attack () nie stanowi problemu, ponieważ klasa Gun może śledzić swoją amunicję i przestać strzelać, gdy skończy się. Ale Reload () jest czymś nowym i nie jest częścią bycia bronią.
Najłatwiejszym rozwiązaniem jest spowolnienie, nie sądzę, że musisz zbytnio martwić się o wydajność, nie będziesz robił tego w każdej klatce.
Alternatywnie możesz ponownie ocenić swoją architekturę i wziąć pod uwagę, że w skrócie wszystkie bronie można przeładowywać, a niektóre bronie po prostu nigdy nie wymagają przeładowywania.
Zatem nie przedłużasz już klasy broni i nie naruszasz LSP.
Ale jest to problematyczne w dłuższej perspektywie, ponieważ musisz wymyślić więcej specjalnych przypadków, Gun.SafteyOn (), Sword.WipeOffBlood () itp., A jeśli umieścisz je wszystkie w Weapon, masz bardzo skomplikowaną uogólnioną klasę podstawową, którą trzymasz muszę się zmienić.
edycja: dlaczego wzorzec strategii jest zły (tm)
Nie jest, ale weź pod uwagę konfigurację, wydajność i ogólny kod.
Muszę gdzieś mieć konfigurację, która mówi mi, że broń może przeładować. Kiedy tworzę instancję broni, muszę przeczytać tę konfigurację i dynamicznie dodać wszystkie metody, sprawdzić, czy nie ma duplikatów nazw itp.
Kiedy wywołuję metodę, muszę przeglądać listę akcji i dopasowywać ciąg znaków, aby zobaczyć, które wywołać.
Kiedy kompiluję kod i wywołuję Weapon.Do („atack”) zamiast „atak”, nie pojawia się błąd przy kompilacji.
Może to być odpowiednie rozwiązanie niektórych problemów, powiedzmy, że masz setki broni, wszystkie z różnymi kombinacjami metod losowych, ale tracisz wiele korzyści z OO i silnego pisania. To tak naprawdę nie oszczędza niczego przy downcastingu
źródło
SafteyOn()
iSword
miałabywipeOffBlood()
. Każda broń nie zna innych metod (i nie powinny być)weapon.do("attack")
typ, jak i typweapon.attack.perform()
mogą być przykładami strategii. Wyszukiwanie strategii według nazwy jest konieczne tylko podczas konfigurowania obiektu z pliku konfiguracyjnego, chociaż użycie odbicia byłoby równie bezpieczne dla typu.