W tej serii postów na blogu Eric Lippert opisuje problem z projektowaniem obiektowym, wykorzystując jako przykład kreatorów i wojowników:
abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }
abstract class Player
{
public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }
a następnie dodaje kilka zasad:
- Wojownik może używać tylko miecza.
- Czarodziej może używać tylko laski.
Następnie demonstruje problemy, na które napotykasz, gdy próbujesz egzekwować te reguły za pomocą systemu typu C # (np. Czyniąc Wizard
klasę odpowiedzialną za upewnienie się, że czarodziej może korzystać tylko z personelu). Naruszasz Zasadę zastępowania Liskowa, ryzykujesz wyjątki w czasie wykonywania lub kończysz się trudnym do rozszerzenia kodem.
Rozwiązanie, które wymyślił, polega na tym, że klasa Player nie dokonuje weryfikacji. Służy wyłącznie do śledzenia stanu. Następnie zamiast dać graczowi broń:
player.Weapon = new Sword();
stan jest modyfikowany przez Command
s i zgodnie z Rule
s:
... tworzymy
Command
obiekt o nazwie,Wield
który przyjmuje dwa obiekty stanu gry, aPlayer
i aWeapon
. Kiedy użytkownik wydaje polecenie systemowi „ten czarodziej powinien dzierżyć ten miecz”, wówczas polecenie to jest oceniane w kontekście zestawuRule
s, który tworzy sekwencjęEffect
s. Mamy taki,Rule
który mówi, że kiedy gracz próbuje użyć broni, efektem jest to, że istniejąca broń, jeśli taka istnieje, zostaje upuszczona, a nowa broń staje się bronią gracza. Mamy kolejną zasadę, która wzmacnia pierwszą zasadę, która mówi, że efekty pierwszej reguły nie mają zastosowania, gdy czarodziej próbuje użyć miecza.
Zasadniczo podoba mi się ten pomysł, ale mam obawy, jak można go wykorzystać w praktyce.
Wydaje się, że nic nie stoi na przeszkodzie, aby deweloper omijał Commands
and Rule
, po prostu ustawiając Weapon
na Player
. Weapon
Nieruchomość musi być dostępny przez Wield
komendę, więc nie może być wykonana private set
.
Więc, co ma zapobiec deweloper z tej operacji? Czy muszą tylko pamiętać, żeby tego nie robić?
źródło
Odpowiedzi:
Cały argument, do którego prowadzi seria postów na blogu, znajduje się w części piątej :
Broń, postacie, potwory i inne przedmioty w grze nie są odpowiedzialne za sprawdzanie, co mogą, a czego nie mogą zrobić. Odpowiedzialny za to jest system reguł.
Command
Obiekt nie robi nic z obiektami gry albo. To po prostu próba zrobienia z nimi czegoś. System reguł sprawdza następnie, czy polecenie jest możliwe, a kiedy to jest, system reguł wykonuje polecenie, wywołując odpowiednie metody na obiektach gry.Jeśli deweloper chce stworzyć drugi system reguł, który robi rzeczy z postaciami i bronią, na które pierwszy system reguł nie zezwala, mogą to zrobić, ponieważ w C # nie możesz (bez nieprzyjemnych hacków refleksyjnych) dowiedzieć się, skąd pochodzi wywołanie metody od.
Obejściem, które może działać w niektórych sytuacjach, jest umieszczenie obiektów gry (lub ich interfejsów) w jednym zestawie z silnikiem reguł i oznaczenie dowolnej metody mutatora jako
internal
. Wszelkie systemy wymagające dostępu tylko do odczytu do obiektów gry byłyby w innym zestawie, co oznacza, że miałyby dostęp tylko dopublic
metod. Pozostawia to lukę w obiektach gry, które nazywają się metodami wewnętrznymi. Ale zrobienie tego byłoby oczywistym zapachem kodu, ponieważ zgodziliście się, że klasy obiektów gry powinny być głupimi posiadaczami stanów.źródło
Oczywistym problemem oryginalnego kodu jest to, że wykonuje modelowanie danych zamiast modelowania obiektowego . Należy pamiętać, że absolutnie nie ma wzmianki o rzeczywistych wymaganiach biznesowych w powiązanym artykule!
Zacznę od próby uzyskania rzeczywistych wymagań funkcjonalnych. Na przykład: „Każdy gracz może atakować innych graczy, ...”. Tutaj:
„Gracze mogą władać bronią użytą w ataku, czarodzieje mogą posługiwać się laską, wojownikami mieczem”
„Każda broń zadaje obrażenia zaatakowanemu wrogowi”. Ok, teraz musimy mieć wspólny interfejs dla broni:
I tak dalej ... Dlaczego nie ma go
Wield()
wPlayer
? Ponieważ nie było wymogu, aby każdy gracz mógł używać dowolnej broni.Mogę sobie wyobrazić, że byłby wymóg mówiący: „Każdy
Player
może próbować władać dowolnymWeapon
”. Byłoby to jednak coś zupełnie innego. Modelowałbym to może w ten sposób:Podsumowanie: modeluj wymagania i tylko wymagania. Nie rób modelowania danych, to nie jest modelowanie oo.
źródło
Jednym ze sposobów byłoby przekazanie
Wield
polecenia doPlayer
. Następnie gracz wykonujeWield
polecenie, które sprawdza odpowiednie reguły i zwracaWeapon
, z którymPlayer
następnie ustawia własne pole broni. W ten sposób pole broni może mieć prywatnego setera i można je ustawić tylko poprzez przekazanieWield
polecenia graczowi.źródło
Nic nie stoi na przeszkodzie, aby programista to zrobił. Właściwie Eric Lippert wypróbował wiele różnych technik, ale wszystkie miały słabości. Taki był sens tej serii, że powstrzymanie programisty od zrobienia tego nie jest łatwe, a wszystko, czego próbował, miało wady. W końcu zdecydował, że należy użyć
Command
obiektu z regułami.Za pomocą reguł możesz ustawić
Weapon
właściwość aWizard
naSword
ale, ale kiedy poprosisz o to,Wizard
aby władać bronią (Miecz) i zaatakować ją, nie będzie to miało żadnego skutku, a zatem nie zmieni żadnego stanu. Jak mówi poniżej:Innymi słowy, nie możemy egzekwować takiej reguły poprzez
type
relacje, które próbował na wiele różnych sposobów, ale albo jej się to nie podobało, albo nie działało. Zatem jedyne, co powiedział, że możemy zrobić, to zrobić coś z tym w czasie wykonywania. Zgłoszenie wyjątku nie było dobre, ponieważ nie uważa go za wyjątek.W końcu zdecydował się na powyższe rozwiązanie. To rozwiązanie zasadniczo mówi, że możesz ustawić dowolną broń, ale jeśli ją oddasz, jeśli nie jest to właściwa broń, byłaby zasadniczo bezużyteczna. Ale nie zostanie zgłoszony żaden wyjątek.
Myślę, że to dobre rozwiązanie. Chociaż w niektórych przypadkach wybrałbym też wzór try-set.
źródło
This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.
Nie udało mi się go znaleźć w tej serii. Czy mógłbyś wskazać mi, gdzie proponowane jest to rozwiązanie?that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon
. Chociaż druga zasada,that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.
więc myślę, że istnieje reguła sprawdzająca, czy broń jest mieczem, więc nie może być władana przez czarodzieja, więc nie jest ustawiona. Zamiast tego zabrzmi smutny puzon.Wield
tutaj. Myślę, że to nieco myląca nazwa polecenia. Coś takiegoChangeWeapon
byłoby bardziej dokładne. Sądzę, że możesz mieć inny model, w którym możesz ustawić dowolną broń, ale kiedy ją oddasz, jeśli nie jest to właściwa broń, byłaby zasadniczo bezużyteczna . Brzmi interesująco, ale nie sądzę, że tak opisuje Eric Lippert.Pierwszym odrzuconym rozwiązaniem autora było przedstawienie reguł według systemu typów. System typów jest oceniany podczas kompilacji. Jeśli odłączysz reguły od systemu typów, nie będą one już sprawdzane przez kompilator, więc nic nie stoi na przeszkodzie, aby programista popełnił błąd sam w sobie.
Ale ten problem napotyka każdy element logiki / modelowania, który nie jest sprawdzany przez kompilator, a ogólną odpowiedzią na to pytanie jest testowanie (jednostkowe). Dlatego zaproponowane przez autora rozwiązanie wymaga silnej uprzęży testowej, aby obejść błędy programistów. Aby podkreślić, że potrzebna jest silna wiązka testowa dla błędów wykrywanych tylko w czasie wykonywania, zapoznaj się z tym artykułem autorstwa Bruce'a Eckela, który stanowi argument, że należy wymienić mocne pisanie na silniejsze testowanie w dynamicznych językach.
Podsumowując, jedyną rzeczą, która może zapobiec popełnianiu błędów przez programistów, jest zestaw testów (jednostkowych) sprawdzających, czy przestrzegane są wszystkie reguły.
źródło
Mogłem tu pominąć subtelność, ale nie jestem pewien, czy problem dotyczy systemu czcionek. Może to konwencja w języku C #.
Na przykład możesz uczynić ten typ całkowicie bezpiecznym,
Weapon
chroniąc seterPlayer
. Następnie dodaćsetSword(Sword)
isetStaff(Staff)
naWarrior
iWizard
odpowiednio, że wezwanie chroniony seter.W ten sposób relacja
Player
/Weapon
jest sprawdzana statycznie, a kod, który go nie obchodzi, może po prostu użyć a,Player
aby uzyskaćWeapon
.źródło
Weapon
doPlayer
. Ale nie ma systemu typów, w którym nie znasz konkretnych typów w czasie kompilacji, które mogłyby działać na tych konkretnych typach w czasie kompilacji. Zgodnie z definicją. Ten schemat oznacza, że tylko ten przypadek musi być rozpatrywany w czasie wykonywania, ponieważ jest on w rzeczywistości lepszy niż jakikolwiek schemat Erica.To pytanie jest w istocie takie samo w przypadku dość świętej wojny o nazwie „ gdzie umieścić walidację ” (najprawdopodobniej również ddd).
Zanim odpowiesz na to pytanie, zadaj sobie pytanie: jaki jest charakter zasad, których chcesz przestrzegać? Czy są wyryte w kamieniu i definiują byt? Czy złamanie tych zasad powoduje, że jednostka przestaje być tym, czym jest? Jeśli tak, wraz z utrzymaniem tych zasad w sprawdzaniu poprawności poleceń , umieść je również w encji. Jeśli więc programista zapomni o sprawdzeniu poprawności polecenia, twoje podmioty nie będą w niepoprawnym stanie.
Jeśli nie - cóż, z natury rzeczy oznacza to, że reguły te są specyficzne dla poleceń i nie powinny znajdować się w jednostkach domeny. Naruszenie tych reguł skutkuje więc działaniami, które nie powinny były mieć miejsca, ale nie były w stanie nieprawidłowego modelu.
źródło