2010-04-11 13 views
6

Czy źle jest mieć wiele zajęć "pracowniczych" (takich jak klasy strategiczne), które robią tylko jedno?Wzorzec strategii i eksplozja klas akcji

Załóżmy, że chcę utworzyć klasę Monster. Zamiast definiować wszystko, co chcę o potworze w jednej klasie, postaram się określić, jakie są jego główne cechy, więc mogę je zdefiniować w interfejsach. Umożliwi to:

  1. Zamknąć zajęcia, jeśli chcę. Później inni użytkownicy mogą po prostu utworzyć nową klasę i nadal mają polimorfizm za pomocą zdefiniowanych przeze mnie interfejsów. Nie muszę się martwić, jak ludzie (lub ja) mogą chcieć zmienić/dodać funkcje do klasy bazowej w przyszłości. Wszystkie klasy dziedziczą z Object i implementują dziedziczenie poprzez interfejsy, a nie z klas macierzystych.
  2. Wykorzystaj strategie, których używam z tym potworem dla innych członków mojego świata gry.

Con: Ten model jest sztywny. Czasami chcielibyśmy zdefiniować coś, co nie jest łatwe do osiągnięcia, po prostu próbując złożyć te "klocki".

public class AlienMonster : IWalk, IRun, ISwim, IGrowl { 
    IWalkStrategy _walkStrategy; 
    IRunStrategy _runStrategy; 
    ISwimStrategy _swimStrategy; 
    IGrowlStrategy _growlStrategy; 

    public Monster() { 
     _walkStrategy = new FourFootWalkStrategy(); 
     ...etc 
    } 

    public void Walk() { _walkStrategy.Walk(); } 
    ...etc 
} 

Mój pomysł przyniesie serię różnych strategii, które mogą być używane przez różne potwory. Z drugiej strony, niektóre z nich mogłyby być również wykorzystywane do zupełnie innych celów (tj. Mógłbym mieć zbiornik, który również "pływa"). Jedyny problem, jaki widzę z tym podejściem, to to, że może to doprowadzić do eksplozji czystych klas "metod", tj. Klas strategii, które mają tylko jeden cel, do wykonania tego lub innego działania. Z drugiej strony ten rodzaj "modułowości" pozwoliłby na wielokrotne wykorzystywanie strategii, czasami nawet w całkowicie odmiennych kontekstach.

Jaka jest Twoja opinia w tej sprawie? Czy to jest uzasadnione rozumowanie? Czy to nadmierna inżynieria?

Ponadto, zakładając, że chcemy dokonać odpowiednich korekt do przykładu dałem powyżej, byłoby lepiej, aby zdefiniować iWalk jak:

interface IWalk { 
    void Walk(); 
} 

lub

interface IWalk { 
    IWalkStrategy WalkStrategy { get; set; } //or something that ressembles this 
} 

jest, że robi to ja nie musiałbym definiować metod w Monsterze, po prostu miałbym publiczne pozyskiwanie dla IWalkStrategy (wydaje się to sprzeczne z ideą, że powinieneś hermetyzować wszystko tak bardzo, jak tylko możesz!) Dlaczego?

Dzięki

Odpowiedz

2

Spacer, bieganie i pływanie to raczej implementacje niż interfejsy. Możesz mieć interfejs ILocomotion i pozwolić twojej klasie na zaakceptowanie listy implementacji ILocomotion.

Program Growl może być implementacją interfejsu podobnego do interfejsu IAbility. A konkretny potwór mógłby mieć zbiór implementacji IAbility.

Następnie mamy kilka interfejsów, które są logiką, której zdolności lub lokomocji użyć: IMove, IAct na przykład.

public class AlienMonster : IMove, IAct 
{ 
    private List<IAbility> _abilities; 
    private List<ILocomotion> _locomotions; 

    public AlienMonster() 
    { 
    _abilities = new List<IAbility>() {new Growl()}; 
    _locomotion = new List<ILocomotion>() {new Walk(), new Run(), new Swim()} 
    } 

    public void Move() 
    { 
    // implementation for the IMove interface 
    } 

    public void Act() 
    { 
    // implementation for the IAct interface 
    } 

} 

Komponując swoją klasę w ten sposób unikniesz pewnej sztywności.

EDIT: dodany materiał o IMOVE i Iakt

EDIT: po niektórych komentarzach

Dodając iWalk, Irun i ISwim do potwora mówisz, że wszystko można zobaczyć obiekt powinien być w stanie wywołaj dowolną z metod zaimplementowanych w dowolnym z tych interfejsów i upewnij się, że ma ona znaczenie. Aby coś zdecydować, który z trzech interfejsów powinien użyć, musisz przekazać cały obiekt. Jedną z ogromnych zalet korzystania z interfejsu jest to, że można go użyć w tym interfejsie.

void SomeFunction(IWalk alienMonster) {...} 

Powyższa funkcja będzie miała nic implementującą iWalk ale jeśli istnieją odmiany someFunction do Irun, ISwim, i tak dalej trzeba napisać zupełnie nową funkcję dla każdego z tych lub przekazać obiekt AlienMonster w całości . Jeśli przekażemy obiekt, funkcja ta będzie mogła wywoływać na nim dowolne interfejsy. Oznacza to również, że funkcja ta musi wysłać zapytanie do obiektu AlienMonster, aby sprawdzić, jakie są jego możliwości, a następnie zdecydować, którego użyć. Wszystko to sprawia, że ​​zewnętrzne funkcje mają wiele funkcji, które powinny być przechowywane w klasie. Ponieważ jesteś eksternalizatorem tego wszystkiego i nie ma wspólnych cech między IWalk, IRun, ISwim, więc niektóre funkcje mogą niewinnie wywoływać wszystkie trzy interfejsy, a twój potwór może jednocześnie biegać-chodzić-pływać.Ponadto, ponieważ będziesz chciał wywoływać IWalk, IRun, ISwim na niektórych klasach, wszystkie klasy będą w zasadzie musiały wdrożyć wszystkie trzy interfejsy, a skończysz na tworzeniu strategii takiej jak CannotSwim, aby spełnić wymagania interfejsu dla ISwim, gdy potwór nie umie pływać. W przeciwnym razie możesz spróbować wywołać interfejs, który nie jest zaimplementowany na potworze. W efekcie jeszcze pogorszysz kod dla dodatkowych interfejsów, IMO.

+1

Inną miłą rzeczą w robieniu tego w ten sposób jest to, że kiedy chcesz przejść z punktu A do punktu B, możesz po prostu przejrzeć listę interfejsów lokomocyjnych i poprosić o koszt zamiast o kodowanie "pobierz koszt trasy, zdobądź koszt uruchomienia , uzyskaj koszty pływania, zdobądź koszty przelotu, uzyskaj koszty teleportacji, ... "i poniesiesz techniczny dług, że będziesz musiał aktualizować tę listę w miarę wprowadzania nowych typów lokomocji. –

+0

Czy możesz wyjaśnić, dlaczego Walk, Run i Swim wydają Ci się bardziej podobnymi implementacjami niż interfejsami? Generalnie nie powinien to być model interfejsu, który pewna klasa może "zrobić"? ISwim oznaczałoby, że mój potwór (lub czołg, czy jakakolwiek klasa, którą próbuję wprowadzić) może pływać. Mógłbym wtedy zaimplementować kilka różnych klas strategii (na przykład) takich jak SwimInWater SwimInAcid itp. Czego tu mi brakuje? –

+1

Pozwolę sobie przeformułować "Interfejsy nie opisują, co coś może zrobić per se". Dobrym pomysłem jest używanie interfejsów w logiczny i spójny sposób. Używanie ILocomotion to Growl byłoby złym wyborem. Ale chodzenie, pływanie, bieganie, latanie, teleportowanie itd. To tylko formy ruchu. Przez wyodrębnienie ich do interfejsu ruchu można znacznie uprościć swój kod. Możesz mieć 100 implementacji ILocomotion i być lepszym niż 20 IWalk, 20 IRun, 20 ISwim, 20 ISkip, 2 ITeleport, 18 implementacji IJump. Jeśli coś nie pasuje do interfejsu ILocomotion, zrób nowy interfejs. – Felan

2

W językach, które wspierają dziedziczenie wielokrotne, można rzeczywiście stworzyć swój Monster przez dziedziczenie z klas, które reprezentują, co może zrobić. W tych klasach należy napisać kod, aby żaden kod nie był kopiowany między implementującymi klasami.

W języku C#, będąc jednym językiem dziedziczenia, nie widzę innego sposobu niż tworzenie dla nich interfejsów. Czy jest dużo kodu dzielonego pomiędzy klasami, wtedy twoje podejście IWalkStrategy dobrze by działało, aby zredukować nadmiarowy kod. W przypadku Twojego przykładu możesz również połączyć kilka powiązanych działań (takich jak chodzenie, pływanie i bieganie) w jedną klasę.

Ponowne użycie i modułowość to dobre rzeczy, a posiadanie wielu interfejsów za pomocą tylko kilku metod nie jest moim zdaniem prawdziwym problemem w tym przypadku. Zwłaszcza, że ​​działania zdefiniowane za pomocą interfejsów są w rzeczywistości różnymi rzeczami, które mogą być używane w inny sposób.Na przykład metoda może wymagać obiektu, który jest w stanie przeskoczyć, więc musi zaimplementować ten interfejs. W ten sposób wymuszasz to ograniczenie przez typ podczas kompilacji, zamiast w inny sposób rzucając wyjątki w czasie wykonywania, gdy nie spełnia on oczekiwań metody.

Krótko mówiąc: zrobiłbym to w taki sam sposób, jak proponowałeś, stosując dodatkowe podejście I...Strategy, gdy redukuje kod skopiowany pomiędzy klasami.

1

"Znajdź to, co jest różne i obuduj je".

Jeśli sposób poruszania się potwora jest różny, należy uwzględnić tę zmianę za abstrakcją. Jeśli chcesz zmienić sposób poruszania się potwora, prawdopodobnie masz w swoim problemie wzorzec stanu. Jeśli chcesz się upewnić, że strategie Walk and Growl są zgodne, prawdopodobnie masz abstrakcyjny wzór fabryczny.

Ogólnie rzecz ujmując: nie, zdecydowanie nie jest to nadmierna inżynieria, aby zamknąć różne koncepcje w swoich klasach. Nie ma również nic złego w tworzeniu konkretnych klas sealed lub final. Zmusza ludzi do świadomego przełamywania enkapsulacji przed odziedziczeniem czegoś, co prawdopodobnie powinno zostać odziedziczone.

2

Z punktu widzenia konserwacji, nadmierne pobudzenie może być równie złe jak sztywny, monolityczny kod. Większość abstrakcji dodaje komplikacji, więc musisz zdecydować, czy dodatkowa złożoność kupi ci coś cennego. Posiadanie wielu bardzo małych klas pracy może być oznaką tego rodzaju nadmiernej abstrakcji.

Dzięki nowoczesnym narzędziom do refaktoryzacji zwykle nie jest zbyt trudno tworzyć dodatkowe abstrakcje tam, gdzie jest to potrzebne, a nie w pełni projektować z góry. W przypadku projektów, w których zacząłem bardzo abstrakcyjnie, odkryłem, że kiedy opracowałem konkretną implementację, odkryłbym przypadki, których nie brałem pod uwagę i często próbowałem wykręcić implementację, aby pasowała do wzorca, zamiast powrót i ponowne rozważenie abstrakcji. Kiedy zaczynam bardziej konkretnie, identyfikuję (więcej) przypadków narożnych z wyprzedzeniem i mogę lepiej określić, gdzie naprawdę ma sens abstrakcja.