Zadaniem w mojej klasie inżynierii oprogramowania jest zaprojektowanie aplikacji, która może grać w różne formy konkretnej gry. Ta gra to Mancala, niektóre z nich nazywane są Wari lub Kalah. Te gry różnią się w niektórych aspektach, ale na moje pytanie ważne jest, aby wiedzieć, że gry mogą się różnić w następujący sposób:
- Sposób, w jaki obsługiwany jest wynik ruchu
- Sposób określania końca gry
- Sposób określania zwycięzcy
Pierwszą rzeczą, jaka przyszła mi do głowy, aby to zaprojektować, było użycie wzorca strategii, mam różne algorytmy (rzeczywiste zasady gry). Projekt może wyglądać następująco:
Pomyślałem wtedy, że w grze Mancala i Wari sposób określania zwycięzcy jest dokładnie taki sam, a kod byłby duplikowany. Nie sądzę, że jest to z definicji naruszenie zasady „jedna zasada, jedno miejsce” lub zasada SUCHA postrzegana jako zmiana zasad dla Mancala nie oznacza automatycznie, że zasada ta powinna zostać również zmieniona w Wari. Niemniej jednak dzięki opiniom otrzymanym od mojego profesora miałem wrażenie, że znalazłem inny projekt.
Potem wymyśliłem:
Każda gra (Mancala, Wari, Kalah, ...) miałaby po prostu atrybut typu interfejsu każdej reguły, tj. WinnerDeterminer
I jeśli istnieje wersja Mancala 2.0, która jest taka sama jak Mancala 1.0, z wyjątkiem tego, w jaki sposób zostanie wyłoniony zwycięzca, może po prostu użyj wersji Mancala.
Myślę, że wdrożenie tych zasad jako wzorca strategii jest z pewnością prawidłowe. Ale prawdziwy problem pojawia się, gdy chcę go dalej projektować.
Czytając o wzorcu metody szablonu, od razu pomyślałem, że można go zastosować do tego problemu. Czynności wykonywane, gdy użytkownik wykonuje ruch, są zawsze takie same i w tej samej kolejności, a mianowicie:
- osadzaj kamienie w dołkach (jest to takie samo dla wszystkich gier, więc byłoby zaimplementowane w samej metodzie szablonu)
- określić wynik ruchu
- ustalić, czy gra zakończyła się z powodu poprzedniego ruchu
- jeśli gra się zakończy, określ, kto wygrał
Te trzy ostatnie kroki są zgodne ze schematem mojej strategii opisanym powyżej. Mam problem z połączeniem tych dwóch. Jednym z możliwych rozwiązań, które znalazłem, byłoby porzucenie wzorca strategii i wykonanie następujących czynności:
Naprawdę nie widzę różnicy w projekcie między wzorem strategii a tym? Ale jestem pewien, że muszę użyć metody szablonu (chociaż byłem równie pewien, że muszę użyć wzorca strategii).
Nie potrafię też ustalić, kto byłby odpowiedzialny za utworzenie TurnTemplate
obiektu, podczas gdy przy wzorcu strategicznym czuję, że mam rodziny obiektów (trzy reguły), które można łatwo utworzyć za pomocą abstrakcyjnego wzorca fabrycznego. Chciałbym mieć wtedy MancalaRuleFactory
, WariRuleFactory
itp a oni stworzyć odpowiednie instancje zasad i ręce mi się RuleSet
obiekt.
Powiedzmy, że używam strategii + abstrakcyjnego wzorca fabrycznego i mam RuleSet
obiekt, który ma algorytmy dla trzech reguł. Jedynym sposobem, w jaki mogę nadal używać wzorca metody szablonu w tym celu, jest przekazanie tego RuleSet
obiektu do mojego TurnTemplate
. „Problem”, który się wtedy pojawia, polega na tym, że nigdy nie potrzebowałbym moich konkretnych implementacji TurnTemplate
, klasy te stałyby się przestarzałe. W moich chronionych metodach TurnTemplate
mogłem po prostu wywołać ruleSet.determineWinner()
. W rezultacie TurnTemplate
klasa nie byłaby już abstrakcyjna, ale musiałaby stać się konkretna, czy w takim razie nadal jest wzorcem metody szablonu?
Podsumowując, czy myślę we właściwy sposób, czy brakuje mi czegoś łatwego? Jeśli jestem na dobrej drodze, jak połączyć wzorzec strategii i wzorzec metody szablonu?
źródło
Odpowiedzi:
Po spojrzeniu na projekty zarówno pierwsza, jak i trzecia iteracja wydają się być bardziej eleganckie. Wspominasz jednak, że jesteś studentem, a profesor przekazał ci informacje zwrotne. Nie wiedząc dokładnie, jakie jest twoje zadanie lub cel zajęć, ani więcej informacji na temat tego, co sugerował twój profesor, wezmę wszystko, co powiem poniżej, z odrobiną soli.
W swoim pierwszym projekcie deklarujesz, że jesteś
RuleInterface
interfejsem, który określa, jak poradzić sobie z turą każdego gracza, jak ustalić, czy gra się skończyła, i jak określić zwycięzcę po zakończeniu gry. Wygląda na to, że jest to poprawny interfejs dla rodziny gier, w których występują różnice. Jednak w zależności od gier być może zduplikowano kod. Zgadzam się, że elastyczność zmiany zasad jednej gry jest dobrą rzeczą, ale twierdzę również, że powielanie kodu jest straszne z punktu widzenia wad. Jeśli skopiujesz / wkleisz uszkodzony kod między implementacjami, a jeden zawiera błąd, masz teraz wiele błędów, które należy naprawić w różnych lokalizacjach. Jeśli przepiszesz implementacje w różnych momentach, możesz wprowadzić defekty w różnych lokalizacjach. Żadne z nich nie jest pożądane.Drugi projekt wydaje się dość złożony, z głębokim drzewem dziedziczenia. Przynajmniej jest głębszy niż się spodziewałbym po rozwiązaniu tego rodzaju problemu. Zaczynasz również rozbijać szczegóły implementacji na inne klasy. Ostatecznie modelujesz i wdrażasz grę. To może być interesujące podejście, jeśli musisz wymieszać i dopasować swoje zasady określania wyników ruchu, końca gry i zwycięzcy, które nie wydają się spełniać wymagań, o których wspomniałeś . Wasze gry są dobrze zdefiniowanymi zestawami reguł, a ja postaram się je jak najściślej podzielić na osobne byty.
Twój trzeci projekt najbardziej mi się podoba. Moją jedyną obawą jest to, że nie jest na odpowiednim poziomie abstrakcji. W tej chwili wydajesz się modelować turę. Poleciłbym rozważyć zaprojektowanie gry. Weź pod uwagę, że masz graczy, którzy wykonują ruchy na jakiejś planszy, używając kamieni. Twoja gra wymaga obecności tych aktorów. Stamtąd twój algorytm nie jest,
doTurn()
aleplayGame()
, który przechodzi od pierwszego ruchu do ostatniego ruchu, po którym się kończy. Po każdym ruchu gracza dostosowuje stan gry, określa, czy gra jest w stanie końcowym, a jeśli tak, określa zwycięzcę.Poleciłbym przyjrzeć się bliżej swojemu pierwszemu i trzeciemu projektowi i pracować z nimi. Pomóc może również myślenie w kategoriach prototypów. Jak wyglądają klienci korzystający z tych interfejsów? Czy jedno podejście do projektowania ma sens w przypadku wdrażania klienta, który faktycznie utworzy instancję gry i w nią zagra? Musisz zdać sobie sprawę z tego, z czym wchodzi w interakcje. W twoim konkretnym przypadku jest to
Game
klasa i wszelkie inne powiązane elementy - nie możesz projektować osobno.Ponieważ wspominasz, że jesteś studentem, chciałbym podzielić się kilkoma rzeczami z czasów, gdy byłem inżynierem TA na kurs projektowania oprogramowania:
źródło
GameTemplate
co jest o wiele lepsze. Pozwala mi także łączyć fabryczną metodę inicjalizacji graczy, planszy itp.Twoje zamieszanie jest uzasadnione. Chodzi o to, że wzory nie wykluczają się wzajemnie.
Metoda szablonów jest podstawą wielu innych wzorców, takich jak Strategia i Stan. Zasadniczo interfejs strategii zawiera jedną lub więcej metod szablonów, z których każda wymaga, aby wszystkie obiekty implementujące strategię miały (przynajmniej) coś w rodzaju metody doAction (). Pozwala to na wzajemne zastępowanie strategii.
W Javie interfejs jest niczym innym jak zestawem metod szablonów. Podobnie każda metoda abstrakcyjna jest zasadniczo metodą szablonową. Ten wzór (między innymi) był dobrze znany projektantom języka, więc go wbudowali.
@ThomasOwens oferuje doskonałe porady dotyczące podejścia do konkretnego problemu.
źródło
Jeśli rozpraszają Cię wzorce projektowe, radzę najpierw stworzyć prototyp gry, a wzorce powinny po prostu wyskoczyć na Ciebie. Nie sądzę, że naprawdę możliwe lub wskazane jest, aby najpierw spróbować zaprojektować system, a następnie go zaimplementować (w podobny sposób wprawia mnie to w zakłopotanie, gdy ludzie próbują najpierw napisać całe programy, a następnie skompilować, zamiast robić to krok po kroku .) Problem polega na tym, że nie jest prawdopodobne, abyś pomyślał o każdym scenariuszu, z którym logika będzie musiała sobie poradzić, a podczas fazy implementacji albo stracisz wszelką nadzieję, albo spróbujesz trzymać się swojego pierwotnego wadliwego projektu i wprowadzić hacki, a nawet gorzej nic nie dostarczają.
źródło
Przejdźmy do mosiężnych haczyków. Nie ma absolutnie żadnej potrzeby jakiegokolwiek interfejsu gry, żadnych wzorców projektowych, żadnych klas abstrakcyjnych i żadnego UML.
Jeśli masz rozsądną liczbę klas wsparcia, takich jak interfejs użytkownika, symulacja i cokolwiek, to w zasadzie cały kod niezwiązany z logiką gry jest ponownie wykorzystywany. Co więcej, użytkownik nie zmienia swojej gry dynamicznie. Nie zmieniasz częstotliwości 30 Hz między grami. Grasz jedną grę przez około pół godziny. Zatem twój „dynamiczny” polimorfizm wcale nie jest dynamiczny. Jest raczej statyczny.
Tak więc rozsądnym sposobem na przejście tutaj jest użycie ogólnej abstrakcyjnej funkcjonalności, takiej jak C #
Action
lub C ++std::function
, utworzenie jednej klasy Mancala, jednej Wari i jednej klasy Kalah i przejście od tego miejsca.Gotowy.
Nie dzwonisz do gier. Gry do ciebie dzwonią.
źródło