Podczas korzystania ze składnika opartego na zdarzeniu często odczuwam pewien ból w fazie podtrzymywania.
Ponieważ cały wykonywany kod jest podzielony, ustalenie, jaka część kodu będzie zaangażowana w czasie wykonywania, może być dość trudne.
Może to prowadzić do subtelnych i trudnych do debugowania problemów, gdy ktoś doda nowe moduły obsługi zdarzeń.
Edycja z komentarzy: nawet przy niektórych dobrych praktykach na pokładzie, takich jak szyna zdarzeń obsługująca całą aplikację i programy obsługi delegujące biznes do innej części aplikacji, jest moment, w którym kod staje się trudny do odczytania, ponieważ jest dużo zarejestrowani treserzy z wielu różnych miejsc (szczególnie prawdziwe, gdy jest autobus).
Następnie schemat sekwencji zaczyna wyglądać na skomplikowany, czas spędzony na ustaleniu, co się dzieje, wzrasta, a sesja debugowania staje się niechlujna (punkt przerwania w menedżerze procedur obsługi podczas iteracji w procedurach obsługi, szczególnie radosny z obsługi programu asynchronicznego i niektórych filtrów na nim).
//////////////
Przykład
Mam usługę, która pobiera niektóre dane z serwera. Na kliencie mamy podstawowy komponent, który wywołuje tę usługę za pomocą wywołania zwrotnego. Aby zapewnić punkt rozszerzenia użytkownikom komponentu i uniknąć sprzężenia między różnymi komponentami, uruchamiamy niektóre zdarzenia: jedno przed wysłaniem zapytania, jedno, gdy odpowiedź wraca, a drugie w przypadku niepowodzenia. Mamy podstawowy zestaw programów obsługi, które są wstępnie zarejestrowane i zapewniają domyślne zachowanie komponentu.
Teraz użytkownicy komponentu (i my też jesteśmy jego komponentem) mogą dodawać moduły obsługi, aby wprowadzić pewne zmiany w zachowaniu (modyfikować zapytanie, logi, analizę danych, filtrowanie danych, masowanie danych, fantazyjną animację interfejsu użytkownika, łańcuch wielu sekwencyjnych zapytań , cokolwiek). Dlatego niektóre procedury obsługi muszą zostać wykonane przed / po niektórych innych i są zarejestrowane z wielu różnych punktów wejścia w aplikacji.
Po chwili może się zdarzyć, że zarejestrowanych jest kilkanaście osób, a praca z tym może być żmudna i niebezpieczna.
Ten projekt powstał, ponieważ używanie dziedziczenia zaczynało być kompletnym bałaganem. System zdarzeń jest używany w pewnym rodzaju kompozycji, w której nie wiesz jeszcze, jakie będą twoje kompozyty.
Koniec przykładu
//////////////
Zastanawiam się więc, jak inni ludzie radzą sobie z tego rodzaju kodem. Zarówno podczas pisania, jak i czytania.
Czy masz jakieś metody lub narzędzia, które pozwalają pisać i utrzymywać taki kod bez większego bólu?
źródło
Odpowiedzi:
Przekonałem się, że przetwarzanie zdarzeń przy użyciu stosu zdarzeń wewnętrznych (a dokładniej kolejki LIFO z arbitralnym usuwaniem) znacznie upraszcza programowanie sterowane zdarzeniami. Umożliwia podzielenie przetwarzania „zdarzenia zewnętrznego” na kilka mniejszych „zdarzeń wewnętrznych”, z dobrze zdefiniowanym stanem pomiędzy nimi. Aby uzyskać więcej informacji, zobacz moją odpowiedź na to pytanie .
Tutaj przedstawiam prosty przykład, który rozwiązuje ten wzór.
Załóżmy, że używasz obiektu A do wykonania niektórych usług, i oddzwaniasz do niego, aby poinformować Cię, kiedy to się zakończy. Jednak A jest takie, że po oddzwonieniu może być konieczne wykonanie dodatkowej pracy. Zagrożenie powstaje, gdy w ramach tego oddzwaniania zdecydujesz, że nie potrzebujesz już A, i zniszczysz go w taki czy inny sposób. Ale jesteś wezwany z A - jeśli A, po powrocie oddzwonienia, nie może bezpiecznie dowiedzieć się, że został zniszczony, może wystąpić awaria, gdy spróbuje wykonać pozostałą pracę.
UWAGA: Prawdą jest, że można dokonać „zniszczenia” w inny sposób, na przykład zmniejszając wartość refkonta, ale to po prostu prowadzi do stanów pośrednich oraz dodatkowego kodu i błędów z ich obsługi; lepiej, żeby A po prostu przestał działać całkowicie, gdy już go nie potrzebujesz, niż kontynuować w stanie pośrednim.
Zgodnie z moim wzorcem A po prostu planuje dalszą pracę, którą musi wykonać, wypychając zdarzenie wewnętrzne (zadanie) do kolejki LIFO pętli zdarzeń, a następnie kontynuuje wywoływanie wywołania zwrotnego i natychmiast wraca do pętli zdarzeń . Ten fragment kodu nie stanowi już zagrożenia, ponieważ A właśnie powraca. Teraz, jeśli wywołanie zwrotne nie zniszczy A, zadanie wypychane zostanie ostatecznie wykonane przez pętlę zdarzeń w celu wykonania dodatkowej pracy (po zakończeniu wywołania zwrotnego i wszystkich jego zadań wypychanych rekurencyjnie). Z drugiej strony, jeśli wywołanie zwrotne zniszczy A, funkcja destruktora lub deinit A może usunąć zadanie wypychane ze stosu zdarzeń, co pośrednio uniemożliwia wykonanie zadania wypychanego.
źródło
Myślę, że prawidłowe rejestrowanie może bardzo pomóc. Upewnij się, że każde rzucone / obsłużone zdarzenie jest gdzieś zarejestrowane (możesz do tego użyć ram rejestrowania). Podczas debugowania możesz przejrzeć dzienniki, aby zobaczyć dokładną kolejność wykonania kodu w momencie wystąpienia błędu. Często to naprawdę pomaga zawęzić przyczyny problemu.
źródło
Model programowania sterowanego zdarzeniami w pewnym stopniu upraszcza kodowanie. Prawdopodobnie ewoluował jako zamiennik dużych instrukcji Select (lub case) używanych w starszych językach i zyskał popularność we wczesnych środowiskach programowania wizualnego, takich jak VB 3 (Nie cytuj mnie o historii, nie sprawdziłem go)!
Model staje się trudny, jeśli sekwencja zdarzeń ma znaczenie i gdy 1 akcja biznesowa jest podzielona na wiele zdarzeń. Ten styl procesu narusza zalety tego podejścia. Za wszelką cenę staraj się, aby kod akcji był zamknięty w odpowiednim zdarzeniu i nie wywoływać zdarzeń z poziomu zdarzeń. To staje się znacznie gorsze niż Spaghetti wynikające z GoTo.
Czasami programiści chętnie udostępniają funkcjonalność GUI, która wymaga takiej zależności od zdarzeń, ale tak naprawdę nie ma prawdziwej alternatywy, która byłaby znacznie prostsza.
Najważniejsze jest to, że technika nie jest zła, jeśli jest mądrze stosowana.
źródło
Chciałem zaktualizować tę odpowiedź, ponieważ dostałem kilka chwil eureki od czasu „spłaszczenia” i „spłaszczenia” przepływów kontrolnych i sformułowałem kilka nowych przemyśleń na ten temat.
Złożone skutki uboczne vs. złożone przepływy kontrolne
Odkryłem, że mój mózg może tolerować złożone działania niepożądane lub złożone przepływy kontroli podobne do grafu, jak zwykle w przypadku obsługi zdarzeń, ale nie kombinacja obu.
Mogę łatwo zrozumieć kod, który powoduje 4 różne skutki uboczne, jeśli są stosowane z bardzo prostym przepływem kontrolnym, takim jak sekwencyjny
for
pętla . Mój mózg może tolerować sekwencyjną pętlę, która zmienia rozmiar i zmienia położenie elementów, animuje je, przerysowuje i aktualizuje jakiś status pomocniczy. Łatwo to zrozumieć.Potrafię również zrozumieć złożony przepływ sterowania, jak w przypadku kaskadowania zdarzeń lub przechodzenia przez złożoną strukturę danych podobną do grafu, jeśli w procesie zachodzi bardzo prosty efekt uboczny, w którym porządek nie ma najmniejszego znaczenia, np. Znakowanie elementów przetwarzane w odroczony sposób w prostej sekwencyjnej pętli.
Zgubiłem się, zdezorientowałem i przytłoczony jest wtedy, gdy masz złożone przepływy kontroli powodujące złożone skutki uboczne. W takim przypadku złożony przepływ kontroli utrudnia przewidywanie z góry, gdzie się skończysz, a złożone skutki uboczne utrudniają dokładne przewidzenie, co się wydarzy i w jakiej kolejności. Tak więc połączenie tych dwóch rzeczy sprawia, że jest to tak niewygodne, że nawet jeśli kod działa teraz doskonale, strasznie jest go zmienić bez obawy spowodowania niepożądanych efektów ubocznych.
Złożone przepływy kontrolne utrudniają rozumowanie, kiedy / gdzie coś się wydarzy. To staje się naprawdę wywołujące ból głowy tylko wtedy, gdy te złożone przepływy kontrolne wywołują złożoną kombinację efektów ubocznych, w których ważne jest, aby zrozumieć, kiedy / gdzie coś się dzieje, np. Efekty uboczne, które mają pewien rodzaj zależności od kolejności, w której jedna rzecz powinna wystąpić przed drugą.
Uprość kontrolę przepływu lub efekty uboczne
Co więc robisz, gdy napotykasz powyższy scenariusz, który jest tak trudny do zrozumienia? Strategia polega na uproszczeniu przepływu kontroli lub skutkach ubocznych.
Powszechnie stosowaną strategią upraszczania skutków ubocznych jest sprzyjanie odroczeniu przetwarzania. Używając zdarzenia zmiany rozmiaru GUI jako przykładu, normalną pokusą może być ponowne zastosowanie układu GUI, zmiana położenia i zmiana rozmiarów podrzędnych widżetów, uruchamianie kolejnej kaskady aplikacji do układania oraz zmiana rozmiaru i zmiana położenia w dół hierarchii, wraz z odświeżaniem elementów sterujących, ewentualnie wyzwalając niektóre unikalne zdarzenia dla widżetów, które mają niestandardowe właściwości zmiany rozmiaru, które wyzwalają więcej zdarzeń prowadzących do tego, kto wie, gdzie itp. Zamiast próbować zrobić to wszystko za jednym razem lub spamując kolejkę zdarzeń, jednym z możliwych rozwiązań jest zejście w dół hierarchii widżetów i zaznacz, które widżety wymagają aktualizacji układów. Następnie w późniejszym, odroczonym przebiegu, który ma prosty sekwencyjny przepływ sterowania, ponownie zastosuj wszystkie układy dla widżetów, które tego potrzebują. Następnie możesz zaznaczyć, które widżety należy przemalować. Ponownie w sekwencyjnym odroczonym przejściu z bezpośrednim przepływem kontrolnym przemaluj widżety oznaczone jako wymagające przerysowania.
Powoduje to zarówno uproszczenie przepływu sterowania, jak i skutki uboczne, ponieważ przepływ sterowania staje się uproszczony, ponieważ nie kaskaduje zdarzeń rekurencyjnych podczas przechodzenia przez wykres. Zamiast tego kaskady występują w odroczonej pętli sekwencyjnej, która może być następnie obsługiwana w innej odroczonej pętli sekwencyjnej. Efekty uboczne stają się proste tam, gdzie się liczy, ponieważ podczas bardziej złożonych przepływów kontrolnych podobnych do wykresów, wszystko, co robimy, to po prostu zaznaczenie, co musi zostać przetworzone przez odroczone sekwencyjne pętle, które wyzwalają bardziej złożone efekty uboczne.
Wiąże się to z pewnym narzutem przetwarzania, ale może następnie otworzyć drzwi do, powiedzmy, wykonywania tych odroczonych przejść równolegle, potencjalnie umożliwiając uzyskanie jeszcze bardziej wydajnego rozwiązania niż rozpoczęte, jeśli wydajność stanowi problem. Generalnie wydajność nie powinna jednak stanowić większego problemu w większości przypadków. Co najważniejsze, choć może to wydawać się sporą różnicą, o wiele łatwiej jest mi się zastanowić. Znacznie łatwiej jest przewidzieć, co się dzieje i kiedy, i nie mogę przecenić wartości, jaką może mieć łatwiejsze zrozumienie tego, co się dzieje.
źródło
Dla mnie to sprawiło, że każde wydarzenie stało się samodzielne, bez odniesienia do innych wydarzeń. Jeśli nadchodzą asynchronicznie, nie masz sekwencji, więc próba ustalenia, co się dzieje w takiej kolejności, jest bezcelowa, poza tym niemożliwa.
W efekcie powstaje kilka struktur danych, które są odczytywane i modyfikowane oraz tworzone i usuwane przez tuzin wątków w określonej kolejności. Musisz wykonać wyjątkowo prawidłowe programowanie wielowątkowe, co nie jest łatwe. Musisz także pomyśleć o wątkach wielowątkowych, jak w „Z tym wydarzeniem zamierzam przyjrzeć się danym, które mam w danym momencie, bez względu na to, co wcześniej było o mikrosekundę, bez względu na to, co właśnie to zmieniło. i bez względu na to, co zrobi 100 wątków, które czekają na mnie, aby zwolnić blokadę. Następnie dokonam zmian na podstawie tego, nawet tego i tego, co widzę. Potem skończę ”.
Jedną z rzeczy, które robię, jest skanowanie konkretnej kolekcji i upewnianie się, że zarówno referencja, jak i sama kolekcja (jeśli nie jest wątkowo bezpieczna) są poprawnie zablokowane i poprawnie zsynchronizowane z innymi danymi. W miarę dodawania kolejnych wydarzeń ten obowiązek rośnie. Ale gdybym śledził relacje między zdarzeniami, ta praca urosłaby znacznie szybciej. Ponadto czasami wiele blokad można izolować własną metodą, co upraszcza kod.
Traktowanie każdego wątku jako całkowicie niezależnej jednostki jest trudne (z uwagi na wielowątkowość hard-core), ale wykonalne. „Skalowalne” może być słowem, którego szukam. Dwa razy więcej wydarzeń zajmuje tylko dwa razy więcej pracy, a może tylko 1,5 razy więcej. Próba skoordynowania większej liczby zdarzeń asynchronicznych szybko Cię pochowa.
źródło
Wygląda na to, że szukasz maszyn stanowych i działań sterowanych zdarzeniami .
Możesz jednak przyjrzeć się również przykładowemu przepływowi pracy znaczników maszyn stanowych .
Oto krótki przegląd implementacji automatu stanów. Maszyna stan przepływu pracy składa się z państw. Każdy stan składa się z co najmniej jednego modułu obsługi zdarzeń. Każda procedura obsługi zdarzeń musi zawierać opóźnienie lub IEventActivity jako pierwsze działanie. Każda procedura obsługi zdarzeń może również zawierać działanie SetStateActivity, które służy do przejścia z jednego stanu do drugiego.
Każdy przepływ pracy maszyny stanów ma dwie właściwości: InitialStateName i CompletedStateName. Po utworzeniu wystąpienia przepływu pracy automatu stanów jest ono umieszczane we właściwości InitialStateName. Gdy automat stanowy osiągnie właściwość CompletedStateName, kończy wykonywanie.
źródło
Kod sterowany zdarzeniami nie jest prawdziwym problemem. Faktycznie nie mam problemu ze stosowaniem logiki w nawet sterowanym kodzie, w którym oddzwanianie jest wyraźnie zdefiniowane lub używane są oddzwaniania w linii. Na przykład wywołania zwrotne w stylu generatora w Tornado są bardzo łatwe do naśladowania.
Naprawdę trudno jest debugować dynamicznie generowane wywołania funkcji. Wzorzec (anty?), Który nazwałbym Call-back Factory z piekła rodem. Jednak tego rodzaju fabryki funkcji są równie trudne do debugowania w tradycyjnym przepływie.
źródło