Pracuję nad projektem, który przetwarza żądania, i są dwa elementy żądania: polecenie i parametry. Obsługa każdego polecenia jest bardzo prosta (<10 linii, często <5). Jest co najmniej 20 poleceń i prawdopodobnie będzie ich więcej niż 50.
Wymyśliłem kilka rozwiązań:
- jeden duży przełącznik / if-else na komendach
- mapa poleceń do funkcji
- mapa poleceń do klas statycznych / singletonów
Każde polecenie wykonuje małe sprawdzanie błędów, a jedynym bitem, który można wyodrębnić, jest sprawdzenie liczby parametrów zdefiniowanych dla każdego polecenia.
Jakie byłoby najlepsze rozwiązanie tego problemu i dlaczego? Jestem również otwarty na wszelkie wzorce projektowe, które mogłem przegapić.
Opracowałem następującą listę pro / con dla każdego:
przełącznik
- profesjonaliści
- utrzymuje wszystkie polecenia w jednej funkcji; ponieważ są proste, dzięki temu jest wizualną tabelą odnośników
- nie musisz zaśmiecać źródła tonami małych funkcji / klas, które będą używane tylko w jednym miejscu
- Cons
- bardzo długo
- trudne do dodawania poleceń programowo (trzeba połączyć łańcuch używając domyślnej wielkości liter)
polecenia map -> funkcja
- profesjonaliści
- małe kawałki wielkości kęsa
- potrafi programowo dodawać / usuwać polecenia
- Cons
- jeśli wykonane w linii, tak samo wizualnie jak przełącznik
- jeśli nie jest wykonywany w linii, wiele funkcji jest używanych tylko w jednym miejscu
polecenia map -> klasa statyczna / singleton
- profesjonaliści
- potrafi używać polimorfizmu do obsługi prostego sprawdzania błędów (tylko 3 linie, ale nadal)
- podobne korzyści jak mapa -> funkcja rozwiązania
- Cons
- wiele bardzo małych klas będzie zagracać projekt
- implementacja nie wszystko w tym samym miejscu, więc skanowanie implementacji nie jest tak łatwe
Dodatkowe uwagi:
Piszę to w Go, ale nie sądzę, że rozwiązanie jest specyficzne dla języka. Szukam bardziej ogólnego rozwiązania, ponieważ być może będę musiał zrobić coś bardzo podobnego w innych językach.
Polecenie jest ciągiem, ale mogę łatwo zmapować go na liczbę, jeśli jest to wygodne. Podpis funkcji jest podobny do:
Reply Command(List<String> params)
Go ma funkcje najwyższego poziomu, a inne platformy, które rozważam, mają również funkcje najwyższego poziomu, stąd różnica między drugą a trzecią opcją.
źródło
Odpowiedzi:
Jest to idealne dopasowanie do mapy (2. lub 3. proponowane rozwiązanie). Używałem go dziesiątki razy, a to jest proste i skuteczne. Tak naprawdę nie rozróżniam tych rozwiązań; ważne jest to, że istnieje mapa z nazwami funkcji jako kluczami.
Główną zaletą podejścia opartego na mapie jest, moim zdaniem, to, że tabela jest danymi. Oznacza to, że można go przekazywać, rozszerzać lub w inny sposób modyfikować w czasie wykonywania; łatwo jest również zakodować dodatkowe funkcje, które interpretują mapę w nowy, ekscytujący sposób. Nie byłoby to możliwe w przypadku rozwiązania obudowy / przełącznika.
Tak naprawdę nie doświadczyłem wspomnianych wad, ale chciałbym wspomnieć o dodatkowej wadzie: wysyłka jest łatwa, jeśli liczy się tylko nazwa ciągu, ale jeśli musisz wziąć pod uwagę dodatkowe informacje, aby zdecydować, którą funkcję wykonać , jest o wiele mniej czysty.
Być może po prostu nigdy nie spotkałem się z wystarczająco trudnym problemem, ale widzę małą wartość zarówno we wzorcu poleceń, jak i w kodowaniu wysyłki jako hierarchii klas, jak wspomnieli inni. Istotą problemu jest mapowanie żądań do funkcji; mapa jest prosta, oczywista i łatwa do przetestowania. Hierarchia klas wymaga więcej kodu i projektu, zwiększa sprzężenie między tym kodem i może zmusić cię do podjęcia wcześniejszych decyzji, które później będziesz musiał zmienić. Nie sądzę, żeby wzorzec poleceń w ogóle miał zastosowanie.
źródło
Twój problem bardzo dobrze pasuje do wzorca projektowego Command . Zasadniczo będziesz miał
Command
interfejs podstawowy , a wtedy będzie wieleCommandImpl
klas, które implementowałyby ten interfejs. Interfejs zasadniczo musi mieć tylko jedną metodędoCommand(Args args)
. Możesz przekazać argumenty za pośrednictwem instancjiArgs
klasy. W ten sposób wykorzystujesz siłę polimorfizmu zamiast nieporadnych instrukcji if / else. Również ten projekt jest łatwo rozszerzalny.źródło
Ilekroć zastanawiam się, czy powinienem użyć instrukcji switch czy polimorfizmu w stylu OO, odsyłam do problemu wyrażenia . Zasadniczo, jeśli masz różne „przypadki” dla swoich danych i różdżkę do obsługi różnych „akcji” (gdzie każda akcja robi coś innego dla każdej sprawy), naprawdę ciężko jest stworzyć system, który w naturalny sposób pozwala dodawać zarówno nowe sprawy, jak i nowe akcje w przyszłości.
Jeśli używasz instrukcji switch (lub wzorca gościa), łatwo jest dodawać nowe akcje (ponieważ wszystko zapisujesz w jednej funkcji), ale trudno jest dodawać nowe przypadki (ponieważ musisz wrócić i edytować stare funkcje)
I odwrotnie, jeśli używasz polimorfizmu w stylu OO, łatwo jest dodawać nowe przypadki (po prostu tworzyć nową klasę), ale trudno jest dodawać metody do interfejsu (ponieważ wtedy musisz wrócić i edytować kilka klas)
W twoim przypadku masz tylko jedną metodę, którą musisz obsłużyć (przetworzyć żądanie), ale wiele możliwych przypadków (każda inna komenda). Ponieważ ułatwienie dodawania nowych przypadków jest ważniejsze niż dodawanie nowych metod, po prostu utwórz oddzielną klasę dla każdego innego polecenia.
Nawiasem mówiąc, z punktu widzenia tego, jak rozszerzalne są rzeczy, nie ma wielkiej różnicy, czy używasz klas, czy funkcji. Jeśli porównamy z instrukcją switch, ważne jest, w jaki sposób rzeczy są „wywoływane”, a obie klasy i funkcje są wywoływane w ten sam sposób. Dlatego po prostu używaj tego, co jest wygodniejsze w twoim języku (a ponieważ Go ma zakres leksykalny i zamykanie, rozróżnienie między klasami i funkcjami jest w rzeczywistości bardzo małe).
Na przykład zwykle możesz użyć delegacji, aby wykonać część sprawdzania błędów zamiast polegać na dziedziczeniu (mój przykład jest w języku JavaScript, ponieważ nie znam składni Go, mam nadzieję, że nie masz nic przeciwko)
Oczywiście w tym przykładzie założono, że istnieje rozsądny sposób, aby argumenty funkcji opakowania lub kontroli klasy nadrzędnej były w każdym przypadku. W zależności od tego, jak się sprawy potoczą, łatwiejsze może być wywołanie funkcji check_arguments w samych poleceniach (ponieważ każde polecenie może wymagać wywołania funkcji sprawdzającej z różnymi argumentami, z powodu różnej liczby argumentów, różnych typów poleceń itp.)
tl; dr: Nie ma najlepszego sposobu na rozwiązanie wszystkich problemów. Z punktu widzenia „spraw, aby rzeczy działały”, skoncentruj się na tworzeniu abstrakcji w sposób, który wymusza ważne niezmienniki i chroni cię przed błędami. Z perspektywy „przyszłości” należy pamiętać o tym, które części kodu mogą zostać rozszerzone.
źródło
Nigdy nie korzystałem z go, jako programista ac # prawdopodobnie podążę następującą linią, mam nadzieję, że ta architektura będzie pasować do tego, co robisz.
Dla każdego stworzyłbym małą klasę / obiekt z główną funkcją do wykonania, każdy powinien znać jego ciąg znaków. Daje to możliwość podłączenia, które brzmi tak, jakbyś chciał, ponieważ liczba funkcji wzrośnie. Zauważ, że nie użyłbym statyki, chyba że naprawdę potrzebujesz, nie dają one żadnej przewagi.
Miałbym wtedy fabrykę, która wie, jak odkrywać te klasy w czasie wykonywania, zmieniając je, aby były ładowane z różnych zestawów itp. Oznacza to, że nie potrzebujesz ich wszystkich w tym samym projekcie.
Dzięki temu jest bardziej modułowy do testowania i powinien sprawić, że kod będzie ładny i mały, co będzie łatwiejsze do późniejszego utrzymania.
źródło
Jak wybierasz swoje polecenia / funkcje?
Jeśli istnieje jakiś „sprytny” sposób wyboru prawidłowej funkcji, to jest to właściwy sposób - oznacza, że możesz dodać nowe wsparcie (być może w bibliotece zewnętrznej) bez modyfikowania podstawowej logiki.
Ponadto testowanie poszczególnych funkcji jest łatwiejsze w izolacji niż ogromna instrukcja przełączania.
Wreszcie, jest używany tylko w jednym miejscu - może się okazać, że gdy osiągniesz 50, różne bity różnych funkcji mogą być ponownie użyte?
źródło
Nie mam pojęcia, jak działa Go, ale architektura, której użyłem w ActionScript, to mieć podwójnie połączoną listę, która działa jak łańcuch odpowiedzialności. Każdy link ma funkcję determinResponsibility (którą zaimplementowałem jako wywołanie zwrotne, ale możesz napisać każdy link osobno, jeśli działa to lepiej dla tego, co próbujesz zrobić). Gdyby łącze określiło, że ponosi odpowiedzialność, wywołałoby meetResponsibility (ponownie oddzwanianie), co spowodowałoby zakończenie łańcucha. Gdyby nie miał odpowiedzialności, przekazałby żądanie do następnego linku w łańcuchu.
Różne przypadki użycia można łatwo dodawać i usuwać, dodając nowe łącze między linkami (lub na końcu) istniejącego łańcucha.
Jest to podobne, ale subtelnie różne od twojego pomysłu na mapę funkcji. Fajną rzeczą jest to, że po prostu przekazujesz prośbę i robi to bez konieczności robienia czegokolwiek innego. Wadą jest to, że nie działa dobrze, jeśli musisz zwrócić wartość.
źródło