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 :)
źródło
Odpowiedzi:
Wcześniej wspierałem aplikację, w której „wszystko jest mapą”. To okropny pomysł. PROSZĘ nie rób tego!
Po podaniu argumentów przekazywanych do funkcji bardzo łatwo jest ustalić, jakich wartości potrzebuje funkcja. Pozwala to uniknąć przekazywania obcych danych do funkcji, która tylko rozprasza programistę - każda przekazywana wartość oznacza, że jest ona potrzebna, i sprawia, że programista obsługujący twój kod musi dowiedzieć się, dlaczego dane są potrzebne.
Z drugiej strony, jeśli przekażesz wszystko jako mapę, programista obsługujący twoją aplikację będzie musiał w pełni zrozumieć wywoływaną funkcję pod każdym względem, aby wiedzieć, jakie wartości powinna zawierać mapa. Co gorsza, bardzo kuszące jest ponowne użycie mapy przekazanej do bieżącej funkcji w celu przekazania danych do następnych funkcji. Oznacza to, że programista obsługujący twoją aplikację musi znać wszystkie funkcje wywoływane przez bieżącą funkcję, aby zrozumieć jej działanie. Jest to dokładnie odwrotność celu pisania funkcji - wyodrębnianie problemów, abyś nie musiał o nich myśleć! Teraz wyobraź sobie 5 połączeń głębokich i 5 połączeń szerokich. To naprawdę dużo do zapamiętania i mnóstwo błędów do popełnienia.
„wszystko jest mapą” również wydaje się prowadzić do używania mapy jako wartości zwracanej. Widziałem to. I znowu to ból. Wywoływane funkcje nigdy nie muszą nadpisywać wartości zwracanych przez siebie nawzajem - chyba że znasz funkcjonalność wszystkiego i wiesz, że wartość X mapy wejściowej należy wymienić na następne wywołanie funkcji. A bieżąca funkcja musi zmodyfikować mapę, aby zwróciła jej wartość, która czasami musi zastąpić poprzednią wartość, a czasem nie.
edycja - przykład
Oto przykład, gdzie było to problematyczne. To była aplikacja internetowa. Dane wejściowe użytkownika zostały zaakceptowane z warstwy interfejsu użytkownika i umieszczone na mapie. Następnie wywołano funkcje w celu przetworzenia żądania. Pierwszy zestaw funkcji sprawdzi, czy dane wejściowe są błędne. Jeśli wystąpił błąd, komunikat o błędzie zostałby umieszczony na mapie. Funkcja wywołująca sprawdziłaby mapę dla tego wpisu i zapisałaby wartość w interfejsie użytkownika, jeśli istnieje.
Następny zestaw funkcji uruchomiłby logikę biznesową. Każda funkcja pobierałaby mapę, usuwała niektóre dane, modyfikowała niektóre dane, działała na danych na mapie i umieszczała wynik na mapie itp. Kolejne funkcje oczekiwałyby wyników z poprzednich funkcji na mapie. Aby naprawić błąd w kolejnej funkcji, trzeba było sprawdzić wszystkie wcześniejsze funkcje, a także program wywołujący, aby określić wszędzie, gdzie mogła zostać ustawiona oczekiwana wartość.
Następne funkcje pobierałyby dane z bazy danych. A raczej przekażą mapę do warstwy dostępu do danych. DAL sprawdzi, czy mapa zawiera pewne wartości, aby kontrolować sposób wykonywania zapytania. Jeśli kluczem byłoby „justcount”, wówczas zapytanie brzmiałoby „policz wybierz foo z paska”. Każda z wcześniej wywoływanych funkcji może mieć tę, która dodała „justcount” do mapy. Wyniki zapytania zostaną dodane do tej samej mapy.
Wyniki pojawiłyby się po stronie dzwoniącego (logika biznesowa), która sprawdziłaby na mapie, co robić. Niektóre z nich wynikałyby z rzeczy, które zostały dodane do mapy w początkowej logice biznesowej. Niektóre pochodzą z danych z bazy danych. Jedynym sposobem, aby dowiedzieć się, skąd pochodzi, było znalezienie kodu, który go dodał. I inna lokalizacja, która może ją również dodać.
Kod był w rzeczywistości monolitycznym bałaganem, który musiałeś zrozumieć w całości, aby wiedzieć, skąd pochodzi pojedynczy wpis na mapie.
źródło
gameState
nie wiedząc nic o tym, co wydarzyło się przed lub po nim. Po prostu reaguje na podane dane. Jak doszło do sytuacji, w której funkcje stawałyby sobie na palcach? Czy możesz podać przykład?Osobiście nie poleciłbym tego wzoru w żadnym z paradygmatów. Ułatwia to pisanie na początku kosztem utrudniania późniejszego myślenia.
Na przykład spróbuj odpowiedzieć na następujące pytania dotyczące każdej funkcji podprocesu:
state
to wymaga?Przy takim wzorze nie można odpowiedzieć na te pytania bez zapoznania się z całą funkcją.
W języku obiektowym wzór ma jeszcze mniej sensu, ponieważ stan śledzenia jest tym, co robią obiekty.
źródło
To, co wydajesz się robić, to w rzeczywistości ręczna monada stanowa; Chciałbym zbudować (uproszczony) kombinator powiązań i ponownie wyrazić połączenia między twoimi logicznymi krokami za pomocą:
Możesz nawet użyć
stateBind
do zbudowania różnych podprocesów z podprocesów i kontynuować w dół drzewa łączących kombinatory, aby odpowiednio ustrukturyzować swoje obliczenia.Aby uzyskać wyjaśnienie pełnej, nieskomplikowanej monady państwowej oraz doskonałe wprowadzenie do monad w ogóle w JavaScript, zobacz ten post na blogu .
źródło
stateBind
kombinator, który podałem, jest bardzo prostym tego przykładem.Wydaje się, że istnieje wiele dyskusji między skutecznością tego podejścia w Clojure. Myślę, że warto przyjrzeć się filozofii Richa Hickeya, dlaczego stworzył Clojure do obsługi abstrakcyjnych danych w ten sposób :
Fogus powtarza te punkty w swojej książce Funkcjonalny Javascript :
źródło
Adwokat Diabła
Myślę, że to pytanie zasługuje na adwokata diabła (ale oczywiście jestem stronniczy). Myślę, że @KarlBielefeldt robi bardzo dobre punkty i chciałbym się nimi zająć. Najpierw chcę powiedzieć, że jego punkty są świetne.
Ponieważ wspomniał, że nie jest to dobry wzorzec nawet w programowaniu funkcjonalnym, wezmę pod uwagę JavaScript i / lub Clojure w moich odpowiedziach. Jednym niezwykle ważnym podobieństwem między tymi dwoma językami jest ich dynamiczne wpisywanie. Byłbym bardziej zgodny z jego argumentami, gdybym wdrażał to w statycznie typowanym języku, takim jak Java lub Haskell. Ale zamierzam rozważyć alternatywę dla wzorca „Wszystko jest mapą” jako tradycyjny projekt OOP w JavaScript, a nie w języku o typie statycznym (mam nadzieję, że nie ustawię argumentu strawman, robiąc to, proszę daj mi znać).
W jaki sposób odpowiadałbyś na te pytania w dynamicznie pisanym języku? Pierwszy parametr funkcji może być nazwany
foo
, ale co to jest? Tablica? Obiekt? Obiekt tablic obiektów? Jak się dowiadujesz? Jedyny znany mi sposób toNie sądzę, żeby wzór „Wszystko jest mapą” miał tu jakiekolwiek znaczenie. To wciąż jedyne znane mi sposoby na udzielenie odpowiedzi na te pytania.
Należy również pamiętać, że w JavaScript i większości imperatywnych językach programowania każdy
function
może wymagać, modyfikować i ignorować każdy stan, do którego ma dostęp, a podpis nie ma znaczenia: funkcja / metoda może zrobić coś ze stanem globalnym lub singletonem. Podpisy często kłamią.Nie próbuję tworzyć fałszywej dychotomii między „Everything is a Map” a źle zaprojektowanym kodem OO. Próbuję tylko wskazać, że posiadanie podpisów, które przyjmują mniej / bardziej dokładne / gruboziarniste parametry, nie gwarantuje, że wiesz, jak izolować, konfigurować i wywoływać funkcję.
Ale jeśli pozwolisz mi użyć tej fałszywej dychotomii: w porównaniu z pisaniem JavaScript w tradycyjny sposób OOP, „Everything is a Map” wydaje się lepsze. W tradycyjnym sposobie OOP funkcja może wymagać, modyfikować lub ignorować stan, który przekazujesz, lub stan, że go nie przekazujesz. Dzięki wzorowi „Wszystko jest mapą” wymagasz tylko, modyfikujesz lub ignorujesz stan, który przekazujesz w.
W moim kodzie tak. Zobacz mój drugi komentarz do odpowiedzi @ Evicatos. Być może to tylko dlatego, że tworzę grę, nie mogę powiedzieć. W grze, która aktualizuje się 60 razy na sekundę, tak naprawdę nie ma znaczenia, czy
dead guys drop loot
wtedy,good guys pick up loot
czy odwrotnie. Każda funkcja nadal wykonuje dokładnie to, co powinna, niezależnie od kolejności, w jakiej są uruchamiane. Te same dane są po prostu podawane do nich podczas różnychupdate
połączeń, jeśli zamienisz zamówienie. Jeśli maszgood guys pick up loot
wtedydead guys drop loot
, dobrzy będzie odebrać łup w następnymupdate
i to nic wielkiego. Człowiek nie będzie w stanie zauważyć różnicy.Przynajmniej takie było moje ogólne doświadczenie. Czuję się naprawdę bezbronny, przyznając to publicznie. Może uznanie tego za właściwe jest bardzo , bardzo złym posunięciem. Daj mi znać, jeśli popełniłem tutaj okropny błąd. Ale jeśli mam, to bardzo łatwo zmienić kolejność funkcji więc kolejność jest
dead guys drop loot
następniegood guys pick up loot
ponownie. Zajmie to mniej czasu niż napisanie tego akapitu: PMoże uważasz, że „martwi faceci powinni najpierw upuścić łupy. Byłoby lepiej, gdyby Twój kod egzekwował to polecenie”. Ale dlaczego wrogowie powinni upuszczać łupy, zanim będzie można je zbierać? Dla mnie to nie ma sensu. Może łup został upuszczony 100 lat
updates
temu. Nie trzeba sprawdzać, czy arbitralny złoczyńca musi zbierać łupy już na ziemi. Dlatego uważam, że kolejność tych operacji jest całkowicie dowolna.Naturalnie jest pisać kroki odsprzęgnięte za pomocą tego wzoru, ale trudno jest zauważyć sprzężone kroki w tradycyjnym OOP. Gdybym pisał tradycyjny OOP, naturalnym, naiwnym sposobem myślenia jest uczynienie z
dead guys drop loot
powrotuLoot
obiektu, do którego muszę przejśćgood guys pick up loot
. Nie byłbym w stanie zmienić kolejności tych operacji, ponieważ pierwszy zwraca dane wejściowe drugiego.Obiekty mają stan i idiomatyczne jest mutowanie stanu, dzięki czemu jego historia po prostu zniknie ... chyba że ręcznie napiszesz kod, aby go śledzić. W jaki sposób śledzenie stanowi „co robią”?
Zgadza się, jak powiedziałem: „Rzadko zdarza się, by którekolwiek z moich funkcji były czyste”. Oni zawsze tylko działać na ich parametry, ale mutować ich parametry. To kompromis, który czułem, że muszę zrobić, stosując ten wzór do JavaScript.
źródło
parent
? Czyrepetitions
i tablica liczb lub ciągów znaków, czy to nie ma znaczenia? A może powtórzenia to tylko liczba reprezentująca liczbę żądanych powtórzeń? Istnieje wiele apis, które po prostu biorą obiekt opcji . Świat jest lepszym miejscem, jeśli poprawnie nazywasz rzeczy, ale nie gwarantuje to, że będziesz wiedział, jak korzystać z interfejsu API, bez zadawania pytań.Przekonałem się, że mój kod ma następującą strukturę:
Nie zamierzałem tworzyć tego rozróżnienia, ale tak często kończy się w moim kodzie. Nie sądzę, aby używanie jednego stylu koniecznie negowało drugi.
Czyste funkcje są łatwe do przetestowania w jednostce. Większe z mapami wnikają bardziej w obszar testowy „integracji”, ponieważ zwykle zawierają więcej ruchomych części.
W javascript jedną z rzeczy, która bardzo pomaga, jest użycie czegoś takiego jak biblioteka Meteor's Match do przeprowadzenia weryfikacji parametrów. Dzięki temu jest bardzo jasne, czego oczekuje funkcja, i może obsługiwać mapy całkiem czysto.
Na przykład,
Więcej informacji można znaleźć na stronie http://docs.meteor.com/#match .
:: AKTUALIZACJA ::
Nagranie wideo Stuarta Sierra z Clojure / West „Clojure in the Large” również porusza ten temat. Podobnie jak PO, kontroluje efekty uboczne w ramach mapy, więc testowanie staje się znacznie łatwiejsze. Ma także wpis na blogu, w którym opisuje jego bieżący obieg pracy w Clojure, który wydaje się istotny.
źródło
Głównym argumentem, który mogę wymyślić przeciwko tej praktyce, jest to, że bardzo trudno jest powiedzieć, jakie dane faktycznie potrzebuje funkcja.
Oznacza to, że przyszli programiści w bazie kodu będą musieli wiedzieć, jak wywoływana funkcja działa wewnętrznie - i wszelkie zagnieżdżone wywołania funkcji - aby ją wywołać.
Im więcej o tym myślę, tym bardziej twój obiekt GameState pachnie globalnie. Jeśli tak to się używa, po co to przekazywać?
źródło
gameState = f(gameState)
naf()
, testowanie będzie znacznie trudniejszef
.f()
może zwrócić inną rzecz za każdym razem, gdy ją nazywam. Ale łatwo jest sprawić, abyf(gameState)
zwrot był taki sam za każdym razem, gdy otrzyma ten sam wkład.To, co robisz, jest bardziej odpowiednie niż Wielka kula błota . To, co robisz, nazywa się wzorem obiektów Boga . Na pierwszy rzut oka nie wygląda to w ten sposób, ale w Javascript jest bardzo mała różnica między nimi
i
To, czy jest to dobry pomysł, prawdopodobnie zależy od okoliczności. Ale z pewnością jest to zgodne ze sposobem Clojure. Jednym z celów Clojure jest usunięcie tego, co Rich Hickey nazywa „przypadkową złożonością”. Wiele obiektów komunikacyjnych jest z pewnością bardziej skomplikowanych niż pojedynczy obiekt. Jeśli podzielisz funkcjonalność na wiele obiektów, nagle musisz się martwić komunikacją i koordynacją oraz podziałem obowiązków. Są to komplikacje, które są jedynie uboczne w stosunku do twojego pierwotnego celu, jakim jest napisanie programu. Powinieneś zobaczyć przemówienie Richa Hickeya Prostota stała się łatwa . Myślę, że to bardzo dobry pomysł.
źródło
Po prostu trochę dzisiaj zmierzyłem się z tym tematem, grając z nowym projektem. Pracuję w Clojure, aby stworzyć grę w pokera. Reprezentowałem wartości nominalne i kolory jako słowa kluczowe i postanowiłem przedstawić kartę jako mapę
Równie dobrze mogłem zrobić z nich listy lub wektory dwóch elementów słowa kluczowego. Nie wiem, czy to ma wpływ na pamięć / wydajność, więc na razie korzystam z map.
W przypadku, gdy zmienię zdanie później, zdecydowałem, że większość części mojego programu powinna przejść przez „interfejs”, aby uzyskać dostęp do elementów karty, aby szczegóły implementacji były kontrolowane i ukryte. Mam funkcje
z którego korzysta reszta programu. Karty są przekazywane do funkcji jako mapy, ale funkcje używają uzgodnionego interfejsu, aby uzyskać dostęp do map i dlatego nie powinny być w stanie się zepsuć.
W moim programie karta prawdopodobnie będzie zawsze mapą o wartości 2. W pytaniu cały stan gry jest przekazywany jako mapa. Stan gry będzie o wiele bardziej skomplikowany niż pojedyncza karta, ale nie sądzę, aby podniesienie poziomu korzystania z mapy było winy. W języku imperatywnym dla obiektów równie dobrze mógłbym mieć pojedynczy duży obiekt GameState i wywoływać jego metody oraz mieć ten sam problem:
Teraz jest zorientowany obiektowo. Czy jest w tym coś szczególnie złego? Nie sądzę, po prostu delegujesz pracę do funkcji, które wiedzą, jak obsługiwać obiekt State. Niezależnie od tego, czy pracujesz z mapami czy obiektami, powinieneś uważać, kiedy podzielić je na mniejsze części. Mówię więc, że używanie map jest w porządku, pod warunkiem, że zachowujesz taką samą ostrożność, jak w przypadku obiektów.
źródło
Z tego, co (mało) widziałem, używanie map lub innych zagnieżdżonych struktur w celu utworzenia takiego globalnego niezmiennego obiektu stanu jest dość powszechne w językach funkcjonalnych, przynajmniej czystych, szczególnie gdy używa się State Monad jako Flame @ Ptharien mentioend .
Dwie przeszkody w efektywnym korzystaniu z tego, o których widziałem / czytałem (i inne wspomniane tutaj odpowiedzi) to:
Istnieje kilka różnych technik / typowych wzorców, które mogą pomóc w złagodzeniu tych problemów:
Pierwszy to zamki błyskawiczne : pozwalają one przejść do stanu mutacji głęboko w niezmiennej zagnieżdżonej hierarchii.
Kolejne to Soczewki : pozwalają skupić się na strukturze w określonym miejscu i odczytać / zmutować tam wartość. Możesz łączyć ze sobą różne obiektywy, aby skupiać się na różnych rzeczach, na przykład regulowanym łańcuchu właściwości w OOP (gdzie możesz zastępować zmienne rzeczywistymi nazwami właściwości!)
Niedawno Prismatic opublikował post na blogu na temat stosowania tego rodzaju techniki, między innymi w JavaScript / ClojureScript, którą powinieneś sprawdzić. Używają kursorów (które porównują do zamków błyskawicznych) do stanu okna dla funkcji:
IIRC, dotykają również niezmienności w JavaScript w tym poście.
źródło
assoc-in
inni). Chyba właśnie miałem Haskella w mózgu. Zastanawiam się, czy ktoś zrobił to w JavaScript? Ludzie prawdopodobnie tego nie poruszali, bo (jak ja) nie oglądali rozmowy :)To, czy jest to dobry pomysł, czy nie, naprawdę będzie zależeć od tego, co robisz ze stanem wewnątrz tych podprocesów. Jeśli dobrze rozumiem przykład Clojure, zwracane słowniki stanowe nie są tymi samymi przekazywanymi słownikami. Są to kopie, być może z dodatkami i modyfikacjami, które (zakładam) Clojure jest w stanie skutecznie utworzyć, ponieważ od tego zależy funkcjonalny charakter języka. Słowniki stanu oryginalnego dla każdej funkcji nie są w żaden sposób modyfikowane.
Jeśli mam zrozumienie poprawnie, są modyfikowanie obiektów państwowych ty przenikać do funkcji javascript zamiast zwrot kopii, co oznacza, że robisz coś bardzo, bardzo różne od tego, co ten kod Clojure robi. Jak zauważył Mike Partridge, jest to po prostu globalny, do którego jawnie przekazujesz funkcje i powracasz z nich bez prawdziwego powodu. W tym momencie myślę, że po prostu sprawia, że myślisz, że robisz coś, czym tak naprawdę nie jesteś.
Jeśli faktycznie tworzysz kopie stanu, modyfikujesz go, a następnie zwracasz zmodyfikowaną kopię, kontynuuj. Nie jestem pewien, czy to koniecznie najlepszy sposób na osiągnięcie tego, co próbujesz zrobić w Javascripcie, ale prawdopodobnie jest to „zbliżone” do tego, co robi ten przykład Clojure.
źródło
update()
funkcji. Przeniosłem jedną na górę, a drugą na dół. Wszystkie moje testy wciąż zdały, a kiedy grałem, nie zauważyłem żadnych złych efektów. Czuję, że moje funkcje są tak samo złożone jak przykład Clojure. Oboje wyrzucamy nasze stare dane po każdym kroku.(-> 10 (- 5) (/ 2))
zwraca 2,5.(-> 10 (/ 2) (- 5))
zwraca 0.Jeśli masz globalny obiekt stanu, czasami nazywany „obiektem boskim”, który jest przekazywany do każdego procesu, kończy się to pomieszaniem szeregu czynników, z których wszystkie zwiększają sprzężenie, jednocześnie zmniejszając spójność. Wszystkie te czynniki mają negatywny wpływ na długoterminową łatwość konserwacji.
Sprzężenie trampowe Wynika to z przekazywania danych różnymi metodami, które nie wymagają prawie wszystkich danych, w celu doprowadzenia ich do miejsca, w którym można sobie z tym poradzić. Ten rodzaj łączenia jest podobny do korzystania z danych globalnych, ale może być bardziej ograniczony. Sprzężenie trampowe jest przeciwieństwem „potrzeby wiedzieć”, która służy do lokalizowania efektów i ograniczania szkód, jakie jeden błędny fragment kodu może mieć w całym systemie.
Nawigacja danych Każdy podproces w twoim przykładzie musi wiedzieć, jak uzyskać dokładnie potrzebne dane, i musi być w stanie je przetworzyć i być może zbudować nowy globalny obiekt stanu. To jest logiczna konsekwencja połączenia trampowego; cały kontekst układu odniesienia jest wymagany do działania na układzie odniesienia. Ponownie, wiedza nielokalna jest złą rzeczą.
Jeśli mijacie „zamek błyskawiczny”, „obiektyw” lub „kursor”, jak opisano w poście @paul, to jedno. Będziesz miał dostęp i pozwalał suwakowi, itp., Kontrolować odczytywanie i zapisywanie danych.
Naruszenie pojedynczej odpowiedzialności Twierdzenie, że każdy z „podprocesów jeden”, „podprocesów dwa” i „podprocesów trzy” ma tylko jedną odpowiedzialność, mianowicie stworzenie nowego obiektu państwa globalnego z odpowiednimi wartościami, jest rażącym redukcjonizmem. W końcu to wszystko jest trochę, prawda?
Chodzi mi o to, że posiadanie wszystkich głównych elementów gry ma takie same obowiązki, jak gra, która nie spełnia celu przekazania i faktorowania.
Wpływ systemu
Główny wpływ twojego projektu to niewielka łatwość konserwacji. Fakt, że możesz zachować całą grę w głowie, oznacza, że prawdopodobnie jesteś doskonałym programistą. Zaprojektowałem wiele rzeczy, które mogłem zachować w głowie przez cały projekt. Jednak nie o to chodzi w inżynierii systemów. Chodzi o to, aby stworzyć system, który może pracować dla czegoś większego, niż jedna osoba może utrzymać w głowie jednocześnie .
Dodanie innego programatora, dwóch lub ośmiu spowoduje, że twój system rozpadnie się niemal natychmiast.
Wreszcie
źródło