„Wszystko jest mapą”, czy robię to dobrze?

69

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:

  1. 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:
    1. Nie musisz się martwić kolejnością argumentów
    2. 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ą.
    3. Nie musisz definiować schematu
  2. W przeciwieństwie do przekazywania obiektu nie ma ukrywania danych. Ale twierdzi, że ukrywanie danych może powodować problemy i jest przereklamowane:
    1. Występ
    2. Łatwość wdrożenia
    3. 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.
  3. 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:

    1. Łatwy do przetestowania
    2. Łatwo spojrzeć na te funkcje osobno
    3. Łatwo skomentować jedną linijkę tego i zobaczyć, jaki jest wynik, usuwając jeden krok
    4. 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.
    5. 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ą gameStateobiekt. 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ż:

  1. Nigdy nie znasz struktury mapy wymaganej przez tę funkcję
  2. 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 :)

Daniel Kaplan
źródło
2
Fajne pytanie (+1)! Uważam, że bardzo użyteczne jest ćwiczenie implementacji funkcjonalnych idiomów w niefunkcjonalnym (lub niezbyt silnie funkcjonalnym) języku.
Giorgio
15
Każdy, kto powie ci, że ukrywanie informacji w stylu OO (z właściwościami i funkcjami akcesorium) jest złą rzeczą z powodu (zwykle nieistotnego) spadku wydajności, a następnie każe ci zamienić wszystkie parametry w mapę, która daje ci (znacznie większy) narzut związany z wyszukiwaniem wartości skrótu za każdym razem, gdy próbujesz odzyskać wartość, można bezpiecznie zignorować.
Mason Wheeler,
4
@MasonWheeler pozwala powiedzieć, że masz rację. Czy unieważnisz każdy inny punkt, który on czyni z powodu tego, że jedna rzecz jest zła?
Daniel Kaplan
9
W Pythonie (i uważam, że większość języków dynamicznych, w tym Javascript), obiekt jest tak naprawdę tylko cukrem składniowym dla dict / map.
Lie Ryan,
6
@EvanPlaice: Notacja Big-O może wprowadzać w błąd. Prostym faktem jest, że wszystko jest powolne w porównaniu do bezpośredniego dostępu z dwiema lub trzema indywidualnymi instrukcjami kodu maszynowego, a na czymś, co dzieje się tak często jak wywołanie funkcji, narzut bardzo szybko się sumuje.
Mason Wheeler

Odpowiedzi:

42

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.

atk
źródło
2
Twój drugi akapit ma dla mnie sens i to naprawdę brzmi jak do bani. Z trzeciego akapitu rozumiem, że tak naprawdę nie mówimy o tym samym projekcie. chodzi o „ponowne użycie”. Byłoby źle, aby tego uniknąć. I naprawdę nie mogę się odnieść do twojego ostatniego akapitu. Mam wszystkie funkcje, gameStatenie 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?
Daniel Kaplan
2
Dodałem przykład, aby spróbować uczynić go nieco jaśniejszym. Mam nadzieję, że to pomoże. Ponadto istnieje różnica między przekazywaniem dobrze zdefiniowanego obiektu stanu a przekazywaniem obiektu blob, który ustawia zmieniony przez wiele miejsc z wielu powodów, mieszanie logiki interfejsu użytkownika, logiki biznesowej i logiki dostępu do bazy danych
atk
28

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:

  • Jakie pola stateto wymaga?
  • Które pola modyfikuje?
  • Które pola są niezmienione?
  • Czy potrafisz bezpiecznie zmienić kolejność funkcji?

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.

Karl Bielefeldt
źródło
2
„zalety niezmienności idą w dół im większe stają się twoje niezmienne obiekty” Dlaczego? Czy to komentarz na temat wydajności lub łatwości konserwacji? Proszę rozwinąć to zdanie.
Daniel Kaplan
8
@tieTYT Niezmienne działają dobrze, gdy jest coś małego (na przykład typ liczbowy). Możesz je kopiować, tworzyć, odrzucać, latać przy użyciu stosunkowo niskich kosztów. Kiedy zaczniesz radzić sobie z całymi stanami gry złożonymi z głębokich i dużych map, drzew, list i dziesiątek, jeśli nie setek zmiennych, koszt ich kopiowania lub usuwania wzrasta (a wagi latające stają się niepraktyczne).
3
Widzę. Czy jest to kwestia „niezmiennych danych w imperatywnych językach” czy „niezmiennych danych”? IE: Może nie jest to problem w kodzie Clojure. Ale widzę, jak to jest w JS. Trudno jest również napisać cały kod, aby to zrobić.
Daniel Kaplan
3
@MichaelT i Karl: aby być uczciwym, naprawdę powinieneś wspomnieć o drugiej stronie historii niezmienności / wydajności. Tak, naiwne użycie może być okropnie nieefektywne, dlatego ludzie wymyślili lepsze podejście. Więcej informacji można znaleźć w pracy Chrisa Okasaki.
3
@MattFenwick Osobiście lubię niezmienne. Kiedy mam do czynienia z wątkami, wiem rzeczy o niezmiennym i mogę bezpiecznie pracować i kopiować. Umieszczam go w wywołaniu parametru i przekazuję do innego, nie martwiąc się, że ktoś go zmodyfikuje, gdy do mnie wróci. Jeśli mówimy o złożonym stanie gry (pytanie posłużyło jako przykład - byłbym przerażony, gdybym pomyślał o czymś tak „prostym” jak stan gry nethack jako niezmienny), niezmienność jest prawdopodobnie niewłaściwym podejściem.
12

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ą:

function stateBind() {
    var computation = function (state) { return state; };
    for ( var i = 0 ; i < arguments.length ; i++ ) {
        var oldComp = computation;
        var newComp = arguments[i];
        computation = function (state) { return newComp(oldComp(state)); };
    }
    return computation;
}

...

stateBind(
  subprocessOne,
  subprocessTwo,
  subprocessThree,
);

Możesz nawet użyć stateBinddo 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 .

Płomień Pthariena
źródło
1
OK, przyjrzę się temu (i skomentuję to później). Ale co sądzisz o pomyśle użycia wzoru?
Daniel Kaplan
1
@tieTYT Myślę, że sam wzór jest bardzo dobrym pomysłem; monada stanu ogólnie jest użytecznym narzędziem do konstruowania kodu dla algorytmów pseudomodyfikowalnych (algorytmów niezmiennych, ale emulujących zmienność).
Ptharien's Flame
2
+1 za zauważenie, że ten wzór jest zasadniczo Monadą. Nie zgadzam się jednak, że dobrym pomysłem jest język, który faktycznie ma zmienność. Monada jest sposobem na zapewnienie możliwości stanu globalnego / zmiennego w języku, który nie pozwala na mutację. IMO, w języku, który nie wymusza niezmienności, wzór Monady jest po prostu mentalną masturbacją.
Lie Ryan,
6
@LieRyan Monady w ogóle nie mają nic wspólnego ze zmiennością lub globalizacją; robi to tylko monada państwowa (ponieważ właśnie do tego ma to służyć). Nie zgadzam się również z tym, że monada państwowa nie jest przydatna w języku ze zmiennością, chociaż implementacja oparta na zmienności poniżej może być bardziej wydajna niż niezmienna, którą podałem (chociaż wcale nie jestem tego pewien). Interfejs monadyczny może zapewniać funkcje wysokiego poziomu, które w innym przypadku nie byłyby łatwo dostępne, stateBindkombinator, który podałem, jest bardzo prostym tego przykładem.
Ptharien's Flame
1
@LieRyan I drugi komentarz Pthariena - większość monad nie dotyczy stanu ani zmienności, a nawet ta, która jest konkretna, nie dotyczy stanu globalnego . Monady faktycznie działają całkiem dobrze w językach OO / imperative / mutable.
11

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: Więc po zmniejszeniu przypadkowych zawiłości, w jaki sposób Clojure może pomóc rozwiązać problem? Na przykład wyidealizowany paradygmat zorientowany obiektowo ma na celu przyspieszenie ponownego użycia, ale Clojure nie jest klasycznie zorientowany obiektowo - jak możemy skonstruować nasz kod do ponownego użycia?

Hickey: Kłóciłbym się o OO i ponowne użycie, ale z pewnością możliwość ponownego użycia sprawia, że ​​problem jest łatwiejszy, ponieważ nie wymyślasz kół zamiast budować samochody. A Clojure będąc w JVM udostępnia wiele kół - bibliotek - dostępnych. Co czyni bibliotekę wielokrotnego użytku? Powinien zrobić jedną lub kilka rzeczy dobrze, być względnie samowystarczalny i stawiać niewiele wymagań kodowi klienta. Nic z tego nie wynika z OO i nie wszystkie biblioteki Java spełniają te kryteria, ale wiele z nich spełnia.

Kiedy spadamy do poziomu algorytmu, myślę, że OO może poważnie udaremnić ponowne użycie. W szczególności użycie obiektów do reprezentowania prostych danych informacyjnych jest niemal przestępcze w generowaniu mikrojęzyków informacji, tj. Metod klasowych, w porównaniu z metodami o wiele potężniejszymi, deklaratywnymi i ogólnymi, takimi jak algebra relacyjna. Wynalezienie klasy z własnym interfejsem do przechowywania informacji jest jak wymyślenie nowego języka do pisania każdej krótkiej historii. Jest to zapobieganie ponownemu użyciu i, moim zdaniem, powoduje eksplozję kodu w typowych aplikacjach OO. Clojure unika tego i zamiast tego opowiada się za prostym modelem asocjacyjnym dla informacji. Dzięki niemu można pisać algorytmy, które można ponownie wykorzystywać w różnych typach informacji.

Ten model asocjacyjny jest tylko jedną z kilku abstrakcji dostarczanych wraz z Clojure, i to są prawdziwe podstawy jego podejścia do ponownego użycia: funkcje na abstrakcjach. Posiadanie otwartego i dużego zestawu funkcji działających na otwartym, a niewielkim zestawie rozszerzalnych abstrakcji jest kluczem do ponownego wykorzystania algorytmów i interoperacyjności biblioteki. Zdecydowana większość funkcji Clojure jest zdefiniowana w oparciu o te abstrakcje, a autorzy bibliotek projektują również ich formaty wejściowe i wyjściowe, realizując ogromną interoperacyjność między niezależnie opracowanymi bibliotekami. Jest to zupełnie sprzeczne z DOM i innymi tego typu rzeczami widocznymi w OO. Oczywiście możesz wykonywać podobną abstrakcję w OO za pomocą interfejsów, na przykład kolekcji java.util, ale nie możesz tak łatwo, jak w java.io.

Fogus powtarza te punkty w swojej książce Funkcjonalny Javascript :

W tej książce przyjmuję podejście polegające na użyciu minimalnych typów danych do reprezentowania abstrakcji, od zestawów przez drzewa po tabele. Jednak w JavaScript, chociaż jego typy obiektów są niezwykle potężne, narzędzia do ich obsługi nie są w pełni funkcjonalne. Zamiast tego większym wzorcem użytkowania związanym z obiektami JavaScript jest dołączenie metod do celów wysyłki polimorficznej. Na szczęście można także wyświetlić nienazwany (nie zbudowany za pomocą funkcji konstruktora) obiekt JavaScript jako zwykły skojarzony magazyn danych.

Jeśli jedynymi operacjami, które możemy wykonać na obiekcie Book lub wystąpieniu typu Pracownik, są setTitle lub getSSN, to zamknęliśmy nasze dane w mikro-językach informacji (Hickey 2011). Bardziej elastycznym podejściem do modelowania danych jest technika danych asocjacyjnych. Obiekty JavaScript, nawet pomniejszone o prototypową maszynę, są idealnymi nośnikami do asocjacyjnego modelowania danych, w których nazwane wartości mogą być tworzone w celu utworzenia modeli danych wyższego poziomu, dostępnych w jednolity sposób.

Chociaż narzędzia do manipulowania obiektami JavaScript i uzyskiwania do nich dostępu, ponieważ mapy danych są rzadkie w samym JavaScript, na szczęście Underscore zapewnia zestaw przydatnych operacji. Do najprostszych funkcji do uchwycenia należą _.klucze, _.wartości i _.pluck. Zarówno _. Klucze, jak i _. Wartości są nazywane zgodnie z ich funkcjonalnością, która polega na pobraniu obiektu i zwróceniu tablicy jego kluczy lub wartości ...

pooya72
źródło
2
Czytałem wcześniej ten wywiad Fogusa / Hickeya, ale do tej pory nie byłem w stanie zrozumieć, o czym on mówił. Dzięki za odpowiedź. Nadal nie jestem pewien, czy Hickey / Fogus dałby mojemu projektowi ich błogosławieństwo. Obawiam się, że maksymalnie wykorzystałem ducha ich rad.
Daniel Kaplan
9

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ć).

Na przykład spróbuj odpowiedzieć na następujące pytania dotyczące każdej funkcji podprocesu:

  • Jakie pola stanu to wymaga?

  • Które pola modyfikuje?

  • Które pola są niezmienione?

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 to

  1. przeczytaj dokumentację
  2. spójrz na ciało funkcji
  3. spójrz na testy
  4. zgadnij i uruchom program, aby zobaczyć, czy działa.

Nie 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 functionmoż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.

  • Czy potrafisz bezpiecznie zmienić kolejność funkcji?

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 lootwtedy, good guys pick up lootczy 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óżnych updatepołączeń, jeśli zamienisz zamówienie. Jeśli masz good guys pick up lootwtedy dead guys drop loot, dobrzy będzie odebrać łup w następnym updatei 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 lootnastępnie good guys pick up lootponownie. Zajmie to mniej czasu niż napisanie tego akapitu: P

Moż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 updatestemu. 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 lootpowrotu Lootobiektu, 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.

W języku obiektowym wzór ma jeszcze mniej sensu, ponieważ stan śledzenia jest tym, co robią obiekty.

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ą”?

Korzyści z niezmienności idą w dół im większe są twoje niezmienne obiekty.

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.

Daniel Kaplan
źródło
4
„Pierwszy parametr funkcji może mieć nazwę foo, ale co to jest?” Dlatego nie nazywasz swoich parametrów „foo”, ale „powtórzenia”, „rodzic” i inne nazwy, które pokazują, czego można się spodziewać po połączeniu z nazwą funkcji.
Sebastian Redl
1
Zgadzam się z tobą we wszystkich kwestiach. Jedynym problemem, jaki Javascript rzeczywiście stwarza w przypadku tego wzorca, jest to, że pracujesz na zmiennych danych i dlatego istnieje duże prawdopodobieństwo, że zmutują państwo. Istnieje jednak biblioteka, która zapewnia dostęp do struktur danych clojure w zwykłym javascript, chociaż nie pamiętam, jak się nazywa. Przekazywanie argumentów jako obiektu nie jest niesłychane, jquery robi to wiele miejsc, ale dokumentuje, których części obiektu używają. Osobiście jednak oddzieliłbym pola UI i pola GameLogic, ale cokolwiek by Ci
pomogło
@SebastianRedl Na co mam uchodzić parent? Czy repetitionsi 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ń.
Daniel Kaplan
8

Przekonałem się, że mój kod ma następującą strukturę:

  • Funkcje, które wykonują mapy, są zwykle większe i mają skutki uboczne.
  • Funkcje, które przyjmują argumenty, są zwykle mniejsze i są czyste.

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,

function foo (post) {
  check(post, {
    text: String,
    timestamp: Date,
    // Optional, but if present must be an array of strings
    tags: Match.Optional([String])
    });

  // do stuff
}

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.

alanning
źródło
1
Myślę, że moje komentarze do @Evicatos rozwiną moje stanowisko tutaj. Tak, mutuję i funkcje nie są czyste. Ale moje funkcje są naprawdę łatwe do przetestowania, szczególnie z perspektywy czasu dla wad regresji, których nie planowałem. Połowa zasług przypada JS: bardzo łatwo jest zbudować „mapę” / obiekt, korzystając tylko z danych potrzebnych do mojego testu. To jest tak proste, jak przekazanie go i sprawdzenie mutacji. Skutki uboczne są zawsze reprezentowane w mapie, więc są one łatwe do sprawdzenia.
Daniel Kaplan
1
Uważam, że pragmatyczne użycie obu metod jest „właściwą” drogą. Jeśli testowanie jest dla Ciebie łatwe i możesz złagodzić meta problem komunikowania wymaganych pól innym programistom, to brzmi jak wygrana. Dziękuję za Twoje pytanie; Z przyjemnością przeczytałem rozpoczętą ciekawą dyskusję.
alanning 20.08.2013
5

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ć?

Mike Partridge
źródło
1
Tak, ponieważ zwykle go mutuję, jest to globalne. Po co zawracać sobie tym głowę? Nie wiem, to ważne pytanie. Ale wnętrzności mówią mi, że jeśli przestanę go przekazywać, mój program natychmiast stanie się trudniejszy do uzasadnienia. Każda funkcja może zrobić wszystko albo nic dla stanu globalnego. Tak jak jest teraz, widzisz ten potencjał w sygnaturze funkcji. Jeśli nie możesz powiedzieć, nie jestem tego pewien :)
Daniel Kaplan,
1
BTW: re: główny argument przeciwko niemu: wydaje się prawdą, czy było to w clojure czy javascript. Ale warto to zrobić. Być może wymienione korzyści znacznie przewyższają jego negatywne skutki.
Daniel Kaplan
2
Teraz wiem, dlaczego zawracam sobie tym głowę, mimo że jest to zmienna globalna: Pozwala mi pisać czyste funkcje. Jeśli zmienię gameState = f(gameState)na f(), testowanie będzie znacznie trudniejsze f. f()może zwrócić inną rzecz za każdym razem, gdy ją nazywam. Ale łatwo jest sprawić, aby f(gameState)zwrot był taki sam za każdym razem, gdy otrzyma ten sam wkład.
Daniel Kaplan
3

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

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

i

{
  ...
  handleUnitCollision: function() {
    ...
  },
  ...
  handleLoot: function() {
    ...
  },
  ...
  update: function() {
    ...
    this.handleUnitCollision()
    ...
    this.handleLoot()
    ...
  },
  ...
};

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ł.

użytkownik7610
źródło
Powiązane, bardziej ogólne pytanie [ programmers.stackexchange.com/questions/260309/… modelowanie danych w porównaniu z klasami tradycyjnymi)
user7610
„W programowaniu obiektowym obiekt boski to obiekt, który wie za dużo lub robi zbyt wiele. Obiekt boski jest przykładem anty-wzorca”. Zatem przedmiot boga nie jest dobrą rzeczą, ale wydaje się, że twoje przesłanie mówi coś wręcz przeciwnego. To dla mnie trochę mylące.
Daniel Kaplan
@tieTYT, nie robisz programowania obiektowego, więc jest w porządku
user7610
Jak doszedłeś do tego wniosku („w porządku”)?
Daniel Kaplan
Problem z obiektami Boga w OO polega na tym, że „Obiekt staje się tak świadomy wszystkiego lub wszystkie obiekty stają się tak zależne od obiektu Boga, że ​​kiedy jest zmiana lub błąd do naprawienia, staje się prawdziwym koszmarem do wdrożenia”. źródło W twoim kodzie jest inny obiekt obok obiektu Boga, więc druga część nie stanowi problemu. Jeśli chodzi o pierwszą część, twoim przedmiotem Boga jest program, a twój program powinien być świadomy wszystkiego. Więc to też jest OK.
user7610
2

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ę

{ :face :queen :suit :hearts }

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

(defn face [card] (card :face))
(defn suit [card] (card :suit))

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:

class State
  def complex-process()
    state = clone(this) ; or just use 'this' below if mutation is fine
    state.subprocess-one()
    state.subprocess-two()
    state.subprocess-three()
    return state

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.

xen
źródło
2

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:

  • Mutowanie (głęboko) zagnieżdżonej wartości w (niezmiennym) stanie
  • Ukrywanie większości stanu przed funkcjami, które go nie potrzebują i po prostu dając im trochę czasu, nad którym muszą pracować / mutować

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:

Om przywraca enkapsulację i modułowość za pomocą kursorów. Kursory zapewniają okna z możliwością aktualizacji do określonych części stanu aplikacji (podobnie jak zamki błyskawiczne), umożliwiając komponentom pobieranie odniesień tylko do odpowiednich części stanu globalnego i aktualizowanie ich w sposób bezkontekstowy.

IIRC, dotykają również niezmienności w JavaScript w tym poście.

Paweł
źródło
Wspomniane omówienie OP omawia także użycie funkcji aktualizacji w celu ograniczenia zakresu, w jakim funkcja może aktualizować do poddrzewa mapy stanu. Myślę, że nikt jeszcze tego nie poruszał.
user7610
@ user7610 Dobry haczyk, nie mogę uwierzyć, że zapomniałem o tym wspomnieć - podoba mi się ta funkcja (i assoc-ininni). 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 :)
Paul
@paul w pewnym sensie mają, ponieważ jest dostępny w ClojureScript, ale nie jestem pewien, czy to się liczy w twoim umyśle. Może istnieć w PureScript i uważam, że istnieje co najmniej jedna biblioteka, która zapewnia niezmienne struktury danych w JavaScript. Mam nadzieję, że przynajmniej jeden z nich je ma, w przeciwnym razie korzystanie z nich byłoby niewygodne.
Daniel Kaplan
@tieTYT Myślałem o natywnej implementacji JS, kiedy napisałem ten komentarz, ale masz rację co do ClojureScript / PureScript. Powinienem zajrzeć do niezmiennego JS i zobaczyć, co tam jest, nie pracowałem wcześniej.
Paul
1

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, 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.

Evicatos
źródło
1
Czy to naprawdę „bardzo, bardzo różne”? W przykładzie Clojure zastępuje swój stary stan nowym. Tak, nie dzieje się żadna prawdziwa mutacja, tożsamość się zmienia. Ale w swoim „dobrym” przykładzie, jak napisano, nie ma sposobu, aby uzyskać kopię, która została przekazana do podprocesu dwa. Identyfikator tej wartości został nadpisany. Dlatego myślę, że rzecz „bardzo, bardzo inna” to tak naprawdę tylko szczegół implementacji języka. Przynajmniej w kontekście tego, co wychowujesz.
Daniel Kaplan
2
W przykładzie Clojure dzieją się dwie rzeczy: 1) pierwszy przykład zależy od funkcji wywoływanych w określonej kolejności, i 2) funkcje są czyste, więc nie mają żadnych skutków ubocznych. Ponieważ funkcje w drugim przykładzie są czyste i mają tę samą sygnaturę, możesz zmienić ich kolejność bez martwienia się o ukryte zależności od kolejności ich wywoływania. Jeśli mutujesz stan w swoich funkcjach, nie masz takiej samej gwarancji. Mutacja stanu oznacza, że ​​twoja wersja nie jest tak łatwa do skomponowania, co było pierwotnym powodem słownika.
Evicatos
1
Będziesz musiał mi pokazać przykład, ponieważ z mojego doświadczenia mogę to dowolnie przenosić i ma to bardzo niewielki efekt. Aby to udowodnić, przesunąłem dwa losowe wywołania podprocesu w połowie mojej 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.
Daniel Kaplan
1
Twoje testy zakończone pozytywnie i niezauważenie żadnych objawów niepożądanych oznaczają, że obecnie nie mutujesz stanu, który ma nieoczekiwane skutki uboczne w innych miejscach. Ponieważ twoje funkcje nie są czyste, nie masz gwarancji, że zawsze tak będzie. Myślę, że muszę zasadniczo nie rozumieć czegoś na temat twojej implementacji, jeśli mówisz, że twoje funkcje nie są czyste, ale wyrzucasz stare dane po każdym kroku.
Evicatos
1
@Evicatos - Bycie czystym i posiadanie tego samego podpisu nie oznacza, że ​​kolejność funkcji nie ma znaczenia. Wyobraź sobie obliczanie ceny z zastosowanymi stałymi i procentowymi rabatami. (-> 10 (- 5) (/ 2))zwraca 2,5. (-> 10 (/ 2) (- 5))zwraca 0.
Zak.
1

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.

  1. Krzywa uczenia się dla boskich obiektów jest płaska (tj. Zajmuje bardzo dużo czasu, aby stać się w nich kompetentnym). Każdy dodatkowy programista będzie musiał nauczyć się wszystkiego, co wiesz, i zachować to w głowie. Zawsze będziesz mógł zatrudnić programistów, którzy są lepsi od ciebie, zakładając, że możesz zapłacić im wystarczająco dużo, by cierpieć z powodu utrzymywania potężnego boga.
  2. Testowanie obsługi administracyjnej w twoim opisie jest tylko białym polem. Musisz znać każdy szczegół obiektu Boga, a także testowanego modułu, aby skonfigurować test, uruchomić go i ustalić, że a) zrobił to dobrze, i b) nie zrobił żadnego z 10 000 niewłaściwych rzeczy. Szanse są bardzo wysokie.
  3. Dodanie nowej funkcji wymaga: a) przejścia przez każdy podproces i ustalenia, czy funkcja wpływa na dowolny kod w nim i odwrotnie, b) przejścia przez stan globalny i zaprojektowania dodatków, oraz c) przejścia przez każdy test jednostkowy i zmodyfikowania go aby sprawdzić, czy żadna testowana jednostka nie wpłynęła negatywnie na nową funkcję .

Wreszcie

  1. Zmienne obiekty boga były przekleństwem mojego istnienia programistycznego, niektóre z moich własnych działań, a niektóre z nich zostały uwięzione.
  2. Monada państwowa nie skaluje się. Stan rośnie wykładniczo, ze wszystkimi implikacjami dla testów i operacji, które implikują. Sposób, w jaki kontrolujemy stan w nowoczesnych systemach, polega na delegowaniu (podział obowiązków) i określaniu zakresu (ograniczanie dostępu tylko do podzbioru stanu). Podejście „wszystko jest mapą” jest dokładnym przeciwieństwem stanu kontrolnego.
BobDalgleish
źródło