Co jest nie tak z referencjami cyklicznymi?

160

Byłem dzisiaj zaangażowany w dyskusję programistyczną, w której wydałem kilka stwierdzeń, które zasadniczo przyjmowały aksjomatycznie, że odwołania cykliczne (między modułami, klasami, cokolwiek) są ogólnie złe. Kiedy skończyłem z boiskiem, mój współpracownik zapytał: „Co jest nie tak z okólnikami?”.

Mam w tej sprawie silne uczucia, ale trudno mi wyrazić zwięźle i konkretnie. Wszelkie wyjaśnienia, które mogę wymyślić, polegają zwykle na innych elementach, które również uważam za aksjomaty („nie mogę używać w izolacji, więc nie mogę przetestować”, „nieznane / niezdefiniowane zachowanie jako stan mutuje w uczestniczących obiektach” itp. .), ale chciałbym usłyszeć zwięzły powód, dla którego błędne są okólniki, które nie wykorzystują tego rodzaju skoków wiary, jakie robi mój własny mózg, spędzając wiele godzin w ciągu lat na rozwikłaniu ich, aby zrozumieć, naprawić, i rozszerz różne bity kodu.

Edycja: Nie pytam o homogeniczne odwołania cykliczne, takie jak te na podwójnie połączonej liście lub wskaźnik do rodzica. To pytanie naprawdę dotyczy pytań o „szerszym zakresie”, takich jak wywołanie libA libB, które wywołuje z powrotem libA. Jeśli chcesz, zamień „moduł” na „lib”. Dziękujemy za wszystkie dotychczasowe odpowiedzi!

dash-tom-bang
źródło
Czy odwołania cykliczne dotyczą bibliotek i plików nagłówkowych? W przepływie pracy nowy kod ProjectB będzie przetwarzał plik wyjściowy ze starszego kodu ProjectA. Dane wyjściowe z ProjectA są nowym wymogiem napędzanym przez ProjectB; ProjectB ma kod, który ułatwia ogólne określenie, które pola idą tam, gdzie itp. Chodzi o to, że starszy Projekt A mógłby ponownie użyć kodu w nowym ProjectB, a ProjectB byłby głupi, gdyby nie używał ponownie kodu narzędziowego w starszym Projekcie A (np. Wykrywanie i transkodowanie zestawu znaków, parsowanie rekordów, sprawdzanie poprawności i transformacja danych itp.).
Luv2code
1
@ Luv2code Staje się głupie tylko wtedy, gdy wycinasz i wklejasz kod między projektami lub ewentualnie, gdy oba projekty kompilują i łączą w tym samym kodzie. Jeśli dzielą się takimi zasobami, umieść je w bibliotece.
dash-tom-bang

Odpowiedzi:

220

W okólnikach jest wiele błędów:

  • Referencje klas okrągłych tworzą wysokie sprzężenie ; obie klasy muszą zostać ponownie skompilowane za każdym razem, gdy jedna z nich zostanie zmieniona.

  • Okrągłe montażowe odniesienia zapobieżenia sieciowania statyczne , ponieważ zależy od B, ale nie może być zmontowana, aż B jest zakończona.

  • Odwołania do obiektów okrągłych mogą powodować awarie naiwnych algorytmów rekurencyjnych (takich jak serializatory, osoby odwiedzające i ładne drukarki) z przepełnieniem stosu. Bardziej zaawansowane algorytmy będą miały wykrywanie cyklu i po prostu zawiodą z bardziej opisowym komunikatem o wyjątku / błędzie.

  • Odwołania do obiektów okrągłych uniemożliwiają także wstrzykiwanie zależności , co znacznie zmniejsza testowalność systemu.

  • Obiekty z bardzo dużą liczbą okrągłych odniesień są często obiektami Boga . Nawet jeśli nie są, mają tendencję do wprowadzania kodu spaghetti .

  • Okrągłe jednostka odniesienia (zwłaszcza w bazach danych, ale również w modelach domen) uniemożliwić korzystanie z ograniczeniami bez wartości null , które mogą ostatecznie doprowadzić do utraty danych lub co najmniej niespójność.

  • Odniesienia kołowe zwykle są po prostu mylące i drastycznie zwiększają obciążenie poznawcze, gdy próbuje się zrozumieć, jak działa program.

Proszę, pomyśl o dzieciach; w miarę możliwości unikaj okrągłych odniesień.

Aaronaught
źródło
32
Szczególnie doceniam ostatni punkt, „ładunek poznawczy” jest czymś, czego jestem bardzo świadomy, ale nigdy nie miałem na to zbyt zwięzłego określenia.
dash-tom-bang
6
Dobra odpowiedź. Byłoby lepiej, gdybyś powiedział coś o testowaniu. Jeżeli moduły A i B są od siebie zależne, muszą zostać przetestowane razem. Oznacza to, że tak naprawdę nie są to osobne moduły; razem stanowią jeden zepsuty moduł.
kevin cline
5
Wstrzykiwanie zależności nie jest niemożliwe przy referencjach kołowych, nawet przy automatycznym DI. Trzeba będzie tylko wstrzyknąć właściwość, a nie parametr konstruktora.
BlueRaja - Danny Pflughoeft
3
@ BlueRaja-DannyPflughoeft: Uważam, że to anty-wzorzec, podobnie jak wielu innych praktyków DI, ponieważ (a) nie jest jasne, czy właściwość jest faktycznie zależnością, oraz (b) „wstrzykiwany” obiekt nie może łatwo śledzić własne niezmienniki. Co gorsza, wiele najbardziej wyrafinowanych / popularnych frameworków, takich jak Castle Windsor, nie może dać użytecznych komunikatów o błędach, jeśli zależności nie można rozwiązać; otrzymujesz denerwujące odwołanie zerowe zamiast szczegółowego wyjaśnienia, w której zależności nie można rozwiązać konstruktora. Tylko dlatego, że może , nie oznacza, że powinna .
Aaronaught
3
Nie twierdziłem, że to dobra praktyka, po prostu wskazałem, że nie jest to niemożliwe, jak stwierdzono w odpowiedzi.
BlueRaja - Danny Pflughoeft
22

Referencja kołowa to dwukrotne sprzężenie referencji nieokrągłej.

Jeśli Foo wie o Baru, a Bar wie o Foo, masz dwie rzeczy, które wymagają zmiany (gdy pojawi się wymaganie, że Foos i Bary nie mogą się już więcej o sobie wiedzieć). Jeśli Foo wie o Baru, ale Bar nie wie o Foo, możesz zmienić Foo bez dotykania Bar.

Cykliczne odniesienia mogą również powodować problemy z ładowaniem, przynajmniej w środowiskach, które trwają długo (wdrożone usługi, środowiska programistyczne oparte na obrazach), gdzie Foo zależy od Bar działającego w celu załadowania, ale Bar zależy również od Foo działającego w celu obciążenie.

Frank Shearar
źródło
17

Gdy połączysz dwa bity kodu, masz jeden duży fragment kodu. Trudność utrzymania odrobiny kodu to co najmniej kwadrat jego wielkości, a być może nawet większy.

Ludzie często patrzą na złożoność pojedynczej klasy (/ function / file / etc.) I zapominają, że naprawdę powinieneś wziąć pod uwagę złożoność najmniejszej możliwej do rozdzielenia (kapsułkowania) jednostki. Posiadanie zależności cyklicznej zwiększa rozmiar tej jednostki, być może niewidocznie (dopóki nie zaczniesz próbować zmienić pliku 1 i nie zauważysz, że wymaga to również zmian w plikach 2-127).

Alex Feinman
źródło
14

Mogą być złe nie same w sobie, ale jako wskaźnik możliwego złego projektu. Jeśli Foo zależy od Bar, a Bar zależy od Foo, uzasadnione jest pytanie, dlaczego są to dwa zamiast unikalnego FooBar.

mouviciel
źródło
10

Hmm ... to zależy od tego, co rozumiesz przez zależność cykliczną, ponieważ w rzeczywistości istnieją pewne zależności cykliczne, które moim zdaniem są bardzo korzystne.

Rozważmy DOM XML - sensowne jest, aby każdy węzeł miał odwołanie do swojego rodzica, a każdy rodzic miał listę swoich dzieci. Struktura jest logicznie drzewem, ale z punktu widzenia algorytmu wyrzucania elementów bezużytecznych lub podobnego struktura jest okrągła.

Billy ONeal
źródło
1
czy to nie byłoby drzewo?
Conrad Frix,
@ Conrad: Przypuszczam, że można to uznać za drzewo, tak. Dlaczego?
Billy ONeal
1
Nie uważam drzewa za okrągłe, ponieważ możesz nawigować w dół jego potomków i zakończy się (niezależnie od odniesienia rodzica). Chyba że węzeł miał dziecko, które było również przodkiem, co w mojej opinii czyni z niego wykres, a nie drzewo.
Conrad Frix
5
Odwołanie cykliczne byłoby, gdyby jedno z potomków węzła zapętliło się z powrotem do przodka.
Matt Olenik
To nie jest tak naprawdę zależność cykliczna (przynajmniej nie w sposób, który powoduje jakiekolwiek problemy). Wyobraź sobie na przykład, że Nodejest to klasa, która zawiera w sobie inne odniesienia do Nodedzieci. Ponieważ odwołuje się tylko do siebie, klasa jest całkowicie samodzielna i nie jest sprzężona z niczym innym. --- Za pomocą tego argumentu można argumentować, że funkcja rekurencyjna jest zależnością cykliczną. To jest (bez przerwy), ale nie w złym tego słowa znaczeniu.
byxor
9

Jest jak problem z kurczakiem lub jajkiem .

W wielu przypadkach odwołanie cykliczne jest nieuniknione i przydatne, ale na przykład w następującym przypadku nie działa:

Projekt A zależy od projektu B, a B zależy od A. Kompilacja A wymaga użycia w B, która wymaga kompilacji B przed A, która wymaga kompilacji B przed A, która ...

Victor Hurdugaci
źródło
6

Chociaż zgadzam się z większością komentarzy tutaj, chciałbym przedstawić specjalny przypadek dotyczący okólnika „rodzic” / „dziecko”.

Klasa często musi wiedzieć coś o swojej klasie nadrzędnej lub właścicielskiej, być może domyślnym zachowaniu, nazwie pliku, z którego pochodzą dane, instrukcji sql, która wybrała kolumnę, lub lokalizacji pliku dziennika itp.

Możesz to zrobić bez odwołania cyklicznego, posiadając klasę zawierającą, dzięki czemu to, co wcześniej było „rodzicem”, jest teraz rodzeństwem, ale nie zawsze jest to możliwe, aby ponownie uwzględnić istniejący kod, aby to zrobić.

Inną alternatywą jest przekazanie wszystkich danych, których dziecko może potrzebować w swoim konstruktorze, co w końcu jest po prostu okropne.

James Anderson
źródło
W powiązanej notatce istnieją dwa typowe powody, dla których X może zawierać odniesienie do Y: X może chcieć poprosić Y o zrobienie rzeczy w imieniu X, lub Y może oczekiwać, że X zrobi rzeczy w imieniu Y, w imieniu Y. Jeśli jedyne odniesienia, które istnieją do Y, służą do celów innych obiektów, które chcą robić rzeczy w imieniu Y, to posiadacze takich odniesień powinni zostać poinformowani, że usługi Y nie są już potrzebne i że powinni porzucić swoje odniesienia do Y w ich wygoda.
supercat
5

W kategoriach bazy danych cykliczne odwołania z odpowiednimi relacjami PK / FK uniemożliwiają wstawianie lub usuwanie danych. Jeśli nie możesz usunąć z tabeli a, chyba że rekord zniknie z tabeli b i nie możesz usunąć z tabeli b, chyba że rekord zniknie z tabeli A, nie możesz usunąć. To samo z wkładkami. dlatego wiele baz danych nie pozwala na konfigurowanie kaskadowych aktualizacji lub usuwanie, jeśli istnieje cykliczne odwołanie, ponieważ w pewnym momencie staje się to niemożliwe. Tak, możesz ustanowić tego rodzaju relacje bez formalnego zadeklarowania PK / Fk, ale wtedy (według mojego doświadczenia) będziesz miał problemy z integralnością danych. To po prostu zły projekt.

HLGEM
źródło
4

Przyjmę to pytanie z punktu widzenia modelowania.

Dopóki nie dodasz żadnych relacji, których tak naprawdę nie ma, jesteś bezpieczny. Jeśli je dodasz, uzyskasz mniej integralności danych (ponieważ występuje nadmiarowość) i ściślej powiązany kod.

W przypadku odnośników cyklicznych chodzi konkretnie o to, że nie widziałem przypadku, w którym byłyby one faktycznie potrzebne, z wyjątkiem jednego odniesienia do siebie. Jeśli modelujesz drzewa lub wykresy, potrzebujesz tego i wszystko jest w porządku, ponieważ samoodniesienie jest nieszkodliwe z punktu widzenia jakości kodu (bez dodanej zależności).

Uważam, że w chwili, gdy zaczynasz potrzebować odniesienia innego niż ja, natychmiast powinieneś zapytać, czy nie możesz modelować go jako wykresu (zwinąć wiele bytów w jeden - węzeł). Być może jest jakiś przypadek, w którym tworzysz odniesienie kołowe, ale modelowanie go jako wykresu jest nieodpowiednie, ale bardzo w to wątpię.

Istnieje niebezpieczeństwo, że ludzie myślą, że potrzebują okólnika, ale w rzeczywistości nie potrzebują. Najczęstszym przypadkiem jest „przypadek jednego z wielu”. Na przykład masz klienta z wieloma adresami, z których jeden powinien być oznaczony jako adres podstawowy. Modelowanie tej sytuacji jest bardzo kuszące, ponieważ dwie osobne relacje has_address i is_primary_address_of, ale nie są poprawne. Powodem jest to, że bycie adresem podstawowym nie jest oddzielną relacją między użytkownikami a adresami, ale jest atrybutem relacji ma adres. Dlaczego? Ponieważ jego domena jest ograniczona do adresów użytkowników, a nie do wszystkich dostępnych adresów. Wybierz jeden z linków i oznacz go jako najsilniejszy (główny).

(Zamierzam teraz mówić o bazach danych) Wiele osób wybiera rozwiązanie dwóch relacji, ponieważ rozumie, że „podstawowy” jest unikalnym wskaźnikiem, a klucz obcy jest rodzajem wskaźnika. Więc klucz obcy powinien być odpowiedni, prawda? Źle. Klucze obce reprezentują relacje, ale „pierwotny” nie jest relacją. Jest to zdegenerowany przypadek zamówienia, w którym jeden element jest przede wszystkim, a reszta nie jest uporządkowana. Gdybyś musiał modelować całkowite uporządkowanie, oczywiście wziąłbyś to za atrybut relacji, ponieważ w zasadzie nie ma innego wyboru. Ale w chwili, gdy go degenerujesz, istnieje wybór - i to okropny - modelowanie czegoś, co nie jest relacją jako relacją. Nadchodzi więc - nadmiar związku, którego z pewnością nie należy lekceważyć.

Nie pozwoliłbym więc na pojawienie się cyklicznego odniesienia, chyba że jest absolutnie jasne, że pochodzi ono z tego, co modeluję.

(uwaga: jest to nieco stronnicze w stosunku do projektu bazy danych, ale założę się, że ma to również zastosowanie w innych obszarach)

klimat
źródło
2

Odpowiem na to pytanie innym pytaniem:

Jaką sytuację możesz mi podać, gdzie utrzymanie kołowego modelu odniesienia jest najlepszym modelem tego, co próbujesz zbudować?

Z mojego doświadczenia wynika, że ​​najlepszy model prawie nigdy nie będzie zawierał okrągłych odniesień w sposób, który według mnie masz na myśli. To powiedziawszy, istnieje wiele modeli, w których przez cały czas używasz referencji okrągłych, jest to po prostu bardzo podstawowe. Rodzic -> Relacje podrzędne, dowolny model wykresu itp., Ale są to dobrze znane modele i myślę, że odnosisz się do czegoś zupełnie innego.

Joseph
źródło
1
Może być tak, że cyklicznie połączona lista (pojedynczo lub podwójnie połączona) byłaby doskonałą strukturą danych dla centralnej kolejki zdarzeń dla programu, który powinien „nigdy się nie zatrzymywać” (trzymać ważne N rzeczy w kolejce za pomocą ustaw flagę „nie usuwaj”, a następnie po prostu przejdź przez kolejkę, aż będzie pusta; gdy potrzebne są nowe zadania (przejściowe lub stałe), trzymaj je w odpowiednim miejscu w kolejce; za każdym razem, gdy podajesz parzystą bez flagi „nie usuwaj” , zrób to, a następnie zdejmij z kolejki).
Vatine
1

Odnośniki cykliczne w strukturach danych są czasem naturalnym sposobem wyrażania modelu danych. Pod względem kodowania zdecydowanie nie jest idealny i może być (do pewnego stopnia) rozwiązany przez wstrzyknięcie zależności, przenosząc problem z kodu na dane.

Vatine
źródło
1

Okrągła konstrukcja odniesienia jest problematyczna, nie tylko z punktu widzenia projektu, ale także z punktu widzenia wychwytywania błędów.

Rozważ możliwość awarii kodu. Nie umieściłeś właściwego wychwytywania błędów w żadnej z klas, albo dlatego, że nie opracowałeś jeszcze swoich metod, albo jesteś leniwy. Tak czy inaczej, nie masz komunikatu o błędzie informującego o tym, co się wydarzyło, i musisz go debugować. Jako dobry projektant programów wiesz, jakie metody są powiązane z procesami, więc możesz zawęzić je do metod związanych z procesem, który spowodował błąd.

Dzięki referencjom cyklicznym Twoje problemy się podwoiły. Ponieważ twoje procesy są ściśle powiązane, nie masz możliwości dowiedzenia się, która metoda mogła spowodować błąd lub skąd wziął się błąd, ponieważ jedna klasa jest zależna od drugiej, jest zależna od drugiej. Musisz teraz poświęcić czas na przetestowanie obu klas w celu ustalenia, która z nich jest naprawdę odpowiedzialna za błąd.

Oczywiście prawidłowe wychwytywanie błędów rozwiązuje ten problem, ale tylko wtedy, gdy wiadomo, kiedy wystąpi błąd. A jeśli używasz ogólnych komunikatów o błędach, nadal nie ma się o wiele lepiej.

Zibbobz
źródło
1

Niektóre śmieciarki mają problemy z ich czyszczeniem, ponieważ do każdego obiektu odwołuje się inny.

EDYCJA: Jak zauważono w komentarzach poniżej, jest to prawdą tylko w przypadku wyjątkowo naiwnej próby wyrzucania śmieci, a nie takiej, którą można spotkać w praktyce.

shmuelp
źródło
11
Hmm .. jakikolwiek śmieci wyrzucony przez to nie jest prawdziwym śmieciarzem.
Billy ONeal
11
Nie znam żadnego nowoczesnego śmieciarza, który miałby problemy z okrągłymi referencjami. Odnośniki cykliczne stanowią problem, jeśli używasz liczników odwołań, ale większość śmieciarek ma styl śledzenia (w którym zaczynasz od listy znanych odniesień i podążasz za nimi, aby znaleźć wszystkie inne, zbierając wszystko inne).
Dean Harding
4
Zobacz sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdf, który wyjaśnia wady różnych rodzajów śmieciarek - jeśli weźmiesz markę i zamiatasz i zaczniesz optymalizować ją, aby skrócić długie czasy pauzy (np. Tworzenie pokoleń) zaczynasz mieć problemy ze strukturami kołowymi (gdy obiekty kołowe są w różnych generacjach). Jeśli weźmiesz pod uwagę liczbę referencji i zaczniesz naprawiać problem z referencją kołową, w końcu wprowadzisz długie czasy przerwy charakterystyczne dla znaku i przeciągnięcia.
Ken Bloom
Jeśli śmieciarz spojrzał na Foo i zwolnił pamięć, która w tym przykładzie odwołuje się do Bar, powinien zająć się usunięciem Bar. Zatem w tym momencie nie ma potrzeby, aby śmieciarz kontynuował i usuwał pasek, ponieważ już to zrobił. Lub odwrotnie, jeśli usunie Bar, który odwołuje się do Foo, powinien również usunąć Foo, a zatem nie będzie musiał usuwać Foo, ponieważ zrobił to po usunięciu Bar? Proszę, popraw mnie jeśli się mylę.
Chris
1
W celu-c cykliczne odwołania sprawiają, że liczba odwołań nie osiąga zera po zwolnieniu, co powoduje wyrzucanie śmieci.
DexterW,
-2

Moim zdaniem posiadanie nieograniczonych referencji ułatwia projektowanie programów, ale wszyscy wiemy, że niektóre języki programowania nie obsługują ich w niektórych kontekstach.

Wspomniałeś o referencjach między modułami lub klasami. W takim przypadku jest to rzecz statyczna, predefiniowana przez programistę, i programista może wyraźnie szukać struktury, która nie ma okrągłości, chociaż może nie pasować do problemu.

Prawdziwy problem pojawia się w postaci cykliczności w strukturach danych w czasie wykonywania, gdzie niektórych problemów nie można tak naprawdę zdefiniować w sposób, który eliminuje cykliczność. W końcu - to problem, który powinien dyktować i wymagać czegoś innego, zmusza programistę do rozwiązania niepotrzebnej układanki.

Powiedziałbym, że to problem z narzędziami, a nie zasada.

Josh S.
źródło
Dodanie zdania w jednym zdaniu nie wpływa znacząco na post ani nie wyjaśnia odpowiedzi. Czy mógłbyś to rozwinąć?
Cóż, dwa punkty, w rzeczywistości wspomniano odniesienia między modułami lub klasami. W takim przypadku jest to rzecz statyczna, predefiniowana przez programistę, i programista może wyraźnie szukać struktury, która nie ma okrągłości, chociaż może nie pasować do problemu. Prawdziwy problem pojawia się w postaci cykliczności w strukturach danych w czasie wykonywania, gdzie niektórych problemów nie można tak naprawdę zdefiniować w sposób, który eliminuje cykliczność. W końcu - to problem, który powinien dyktować i wymagać czegoś innego, zmusza programistę do rozwiązania niepotrzebnej układanki.
Josh S
Odkryłem, że ułatwia to uruchomienie programu, ale ogólnie rzecz biorąc, ostatecznie utrudnia utrzymanie oprogramowania, ponieważ okazuje się, że trywialne zmiany mają efekt kaskadowy. A wykonuje połączenia do B, które wywołują połączenia z powrotem A, które wywołują połączenia z powrotem do B ... Trudno mi było naprawdę zrozumieć skutki zmian tego rodzaju, zwłaszcza gdy A i B są polimorficzne.
dash-tom-bang