Obejrzałem przemówienie Stuarta Sierra „ Thinking In Data ” i wziąłem z niego jeden z pomysłów jako zasadę projektowania w tej grze, którą tworzę. Różnica polega na tym, że pracuje w Clojure, a ja w JavaScript. Widzę kilka głównych różnic między naszymi językami w tym, że:
- Clojure to idiomatycznie funkcjonalne programowanie
- Większość stanów jest niezmienna
Pomysł wziąłem ze slajdu „Wszystko jest mapą” (od 11 minut, 6 sekund do> 29 minut w). Niektóre rzeczy, które mówi, to:
- Ilekroć zobaczysz funkcję, która przyjmuje 2-3 argumenty, możesz uzasadnić przekształcenie jej w mapę i po prostu przekazanie mapy. Ma to wiele zalet:
- Nie musisz się martwić kolejnością argumentów
- Nie musisz się martwić o dodatkowe informacje. Jeśli są dodatkowe klucze, to nie jest tak naprawdę nasza troska. Po prostu przepływają, nie przeszkadzają.
- Nie musisz definiować schematu
- W przeciwieństwie do przekazywania obiektu nie ma ukrywania danych. Ale twierdzi, że ukrywanie danych może powodować problemy i jest przereklamowane:
- Występ
- Łatwość wdrożenia
- Natychmiast po nawiązaniu komunikacji przez sieć lub procesy, obie strony muszą uzgodnić reprezentację danych. To dodatkowa praca, którą możesz pominąć, jeśli pracujesz tylko na danych.
Najbardziej odpowiedni do mojego pytania. To 29 minut: „Spraw, by twoje funkcje były złożone”. Oto przykładowy kod, którego używa do wyjaśnienia tej koncepcji:
;; Bad (defn complex-process [] (let [a (get-component @global-state) b (subprocess-one a) c (subprocess-two a b) d (subprocess-three a b c)] (reset! global-state d))) ;; Good (defn complex-process [state] (-> state subprocess-one subprocess-two subprocess-three))
Rozumiem, że większość programistów nie zna Clojure, więc przepiszę to w imperatywnym stylu:
;; Good def complex-process(State state) state = subprocess-one(state) state = subprocess-two(state) state = subprocess-three(state) return state
Oto zalety:
- Łatwy do przetestowania
- Łatwo spojrzeć na te funkcje osobno
- Łatwo skomentować jedną linijkę tego i zobaczyć, jaki jest wynik, usuwając jeden krok
- Każdy podproces może dodać więcej informacji o stanie. Jeśli podproces wymaga przekazania czegoś do podprocesu trzeciego, jest to tak proste, jak dodanie klucza / wartości.
- Brak płyty kotłowej do wyodrębnienia potrzebnych danych ze stanu, aby można je było zapisać z powrotem. Wystarczy przekazać cały stan i pozwolić podprocesowi przypisać to, czego potrzebuje.
Wracając do mojej sytuacji: wziąłem tę lekcję i zastosowałem ją w mojej grze. Oznacza to, że prawie wszystkie moje funkcje wysokiego poziomu przyjmują i zwracają gameState
obiekt. Ten obiekt zawiera wszystkie dane gry. EG: Lista badGuys, lista menu, łupy na ziemi itp. Oto przykład mojej funkcji aktualizacji:
update(gameState)
...
gameState = handleUnitCollision(gameState)
...
gameState = handleLoot(gameState)
...
Chcę tu zapytać, czy stworzyłem jakąś obrzydliwość, która wypaczyła pomysł, który jest praktyczny tylko w funkcjonalnym języku programowania? JavaScript nie jest idiomatycznie funkcjonalny (choć może być napisany w ten sposób) i pisanie niezmiennych struktur danych jest naprawdę trudne. Jedno, co mnie niepokoi, to to , że zakłada, że każda z tych podprocesów jest czysta. Dlaczego należy przyjąć takie założenie? Rzadko zdarza się, że którakolwiek z moich funkcji jest czysta (przez to znaczy, że często modyfikują gameState
. Nie mam żadnych innych skomplikowanych efektów ubocznych poza tym). Czy te pomysły się rozpadają, jeśli nie masz niezmiennych danych?
Martwię się, że któregoś dnia się obudzę i zdam sobie sprawę, że cały ten projekt jest fikcją i naprawdę właśnie wdrożyłem anty-wzór Big Ball Of Mud .
Szczerze mówiąc, pracowałem nad tym kodem od miesięcy i był świetny. Wydaje mi się, że czerpię wszystkie korzyści, o które twierdził. Mój kod jest dla mnie bardzo łatwy do uzasadnienia. Ale jestem zespołem jednoosobowym, więc mam przekleństwo wiedzy.
Aktualizacja
Kodowałem ponad 6 miesięcy za pomocą tego wzoru. Zazwyczaj do tego czasu zapominam o tym, co zrobiłem i właśnie tam „napisałem to w czysty sposób?” wchodzi w grę. Gdybym tego nie zrobił, naprawdę walczyłbym. Jak dotąd wcale nie walczę.
Rozumiem, jak potrzebny byłby inny zestaw oczu, aby potwierdzić jego łatwość utrzymania. Mogę tylko powiedzieć, że zależy mi przede wszystkim na łatwości konserwacji. Zawsze jestem najgłośniejszym ewangelistą dla czystego kodu, bez względu na to, gdzie pracuję.
Chcę odpowiedzieć bezpośrednio na te, które już mają złe osobiste doświadczenia z tym sposobem kodowania. Nie wiedziałem wtedy, ale myślę, że tak naprawdę mówimy o dwóch różnych sposobach pisania kodu. Sposób, w jaki to zrobiłem, wydaje się bardziej uporządkowany niż to, czego doświadczyli inni. Gdy ktoś ma złe osobiste doświadczenia z „Wszystko jest mapą”, mówi o tym, jak trudno jest go utrzymać, ponieważ:
- Nigdy nie znasz struktury mapy wymaganej przez tę funkcję
- Każda funkcja może mutować dane wejściowe w sposób, którego nigdy się nie spodziewałeś. Musisz rozejrzeć się po całej bazie kodu, aby dowiedzieć się, jak dany klucz dostał się na mapę lub dlaczego zniknął.
Dla osób z takim doświadczeniem, być może podstawa kodu brzmiała: „Wszystko wymaga 1 z N typów map”. Moim zdaniem jest „Wszystko zajmuje 1 z 1 typu mapy”. Jeśli znasz strukturę tego typu 1, znasz strukturę wszystkiego. Oczywiście, struktura ta zwykle rośnie z czasem. Dlatego...
Jest jedno miejsce, w którym można znaleźć implementację referencyjną (tj. Schemat). Ta referencyjna implementacja jest kodem używanym przez grę, więc nie może się przedawnić.
Jeśli chodzi o drugi punkt, nie dodam / nie usuwam kluczy do mapy poza implementacją referencyjną, po prostu mutuję to, co już tam jest. Mam również duży pakiet automatycznych testów.
Jeśli ta architektura ostatecznie upadnie pod własnym ciężarem, dodam drugą aktualizację. W przeciwnym razie załóż, że wszystko idzie dobrze :)