Что не так с циклическими ссылками?


160

Сегодня я участвовал в дискуссии по программированию, где сделал несколько заявлений, в которых аксиоматически предполагалось, что циклические ссылки (между модулями, классами и т. Д.), Как правило, плохие. Как только я закончил свою работу, мой коллега спросил: «Что не так с круговыми ссылками?»

У меня есть сильные чувства по этому поводу, но мне трудно выразить словами кратко и конкретно. Любое объяснение, которое я могу придумать, имеет тенденцию полагаться на другие элементы, которые я тоже рассматриваю аксиомами («не могу использовать изолированно, поэтому не могу проверить», «неизвестное / неопределенное поведение, поскольку состояние изменяется в участвующих объектах» и т. Д.). ..., но я бы хотел услышать краткую причину того, почему циркулярные ссылки плохи, потому что они не совершают прыжков веры, которые делает мой собственный мозг, потратив много часов в течение многих лет, распутывая их, чтобы понять, исправить, и расширить различные биты кода.

Редактировать: я не спрашиваю об однородных циклических ссылках, таких как те, что в двусвязном списке или указатель на родителя. Этот вопрос действительно задает циклические ссылки «большего объема», например, когда libA вызывает libB, а затем обращается к libA. Замените «модуль» на «lib», если хотите. Спасибо за все ответы до сих пор!


Циркулярная ссылка относится к библиотекам и заголовочным файлам? В рабочем процессе новый код ProjectB будет обрабатывать файл, который выводится из устаревшего кода ProjectA. Этот вывод ProjectA является новым требованием, предъявляемым ProjectB; ProjectB имеет код, который облегчает общее определение того, куда и куда идут поля и т. Д. Суть в том, что унаследованный ProjectA может повторно использовать код в новом ProjectB, и ProjectB будет глупо не использовать повторно код утилит в унаследованном ProjectA (например, обнаружение набора символов и транскодирование, анализ записей, проверка и преобразование данных и т. д.).
Luv2code

1
@ Luv2code Глупо становится только тогда, когда вы вырезаете и вставляете код между проектами или, возможно, когда оба проекта компилируются и связываются в одном и том же коде. Если они делятся такими ресурсами, поместите их в библиотеку.
дэш-том-бэнг

Ответы:


220

С циклическими ссылками не так много вещей:

  • Круговые ссылки на классы создают высокую связь ; оба класса должны быть перекомпилированы каждый раз, когда любой из них изменяется.

  • Ссылки круговой сборки предотвращают статическое связывание , потому что B зависит от A, но A не может быть собран, пока B не будет завершен.

  • Круговые ссылки на объекты могут приводить к сбою наивных рекурсивных алгоритмов (таких как сериализаторы, посетители и симпатичные принтеры) с переполнением стека. Более продвинутые алгоритмы будут иметь обнаружение цикла и просто потерпят неудачу с более описательным сообщением об исключении / ошибке.

  • Круговые ссылки на объекты также делают невозможным внедрение зависимостей , что значительно снижает тестируемость вашей системы.

  • Объекты с очень большим количеством циклических ссылок часто являются объектами Бога . Даже если они не, они имеют тенденцию вести к Кодексу Спагетти .

  • Круговые ссылки на сущности (особенно в базах данных, но также и в моделях предметной области) предотвращают использование ограничений , не допускающих обнуление , что в конечном итоге может привести к повреждению данных или, по крайней мере, к несогласованности.

  • Циркулярные ссылки в целом просто сбивают с толку и резко увеличивают когнитивную нагрузку, пытаясь понять, как функционирует программа.

Пожалуйста, подумайте о детях; избегайте циклических ссылок, когда можете.


32
Я особенно ценю последнее замечание: «когнитивная нагрузка» - это то, что я очень хорошо осознаю, но у меня никогда не было хорошего краткого термина.
dash-tom-bang

6
Хороший ответ. Было бы лучше, если бы вы сказали что-то о тестировании. Если модули A и B взаимозависимы, они должны быть проверены вместе. Это означает, что они не являются отдельными модулями; вместе они один сломанный модуль.
Кевин Клайн

5
Внедрение зависимостей не является невозможным при использовании циклических ссылок, даже при автоматическом DI Нужно просто ввести свойство, а не параметр конструктора.
BlueRaja - Дэнни Пфлугхофт

3
@ BlueRaja-DannyPflughoeft: Я считаю, что анти-паттерн, как и многие другие практики DI, потому что (а) не ясно, является ли свойство на самом деле зависимостью, и (б) объект, который «вводится», не может быть легко отслеживать свои собственные инварианты. Хуже того, многие из самых сложных / популярных фреймворков, таких как Castle Windsor, не могут выдавать полезные сообщения об ошибках, если зависимость не может быть разрешена; в итоге вы получите раздражающую нулевую ссылку вместо подробного объяснения того, какая именно зависимость в каком конструкторе не может быть разрешена. То, что ты можешь , не означает, что ты должен .
Aaronaught

3
Я не утверждал, что это хорошая практика, я просто указывал, что это не невозможно, как утверждается в ответе.
BlueRaja - Дэнни Пфлюгофт

22

Круговая ссылка - это двойная связь некруговой ссылки.

Если Foo знает о Bar, а Bar знает о Foo, у вас есть две вещи, которые нужно изменить (когда возникает требование, что Foos и Bars больше не должны знать друг о друге). Если Foo знает о Bar, но Bar не знает о Foo, вы можете изменить Foo, не касаясь Bar.

Циклические ссылки также могут вызывать проблемы с начальной загрузкой, по крайней мере, в средах, которые длятся долго (развернутые сервисы, среды разработки на основе изображений), где Foo зависит от работы Bar для загрузки, но Bar также зависит от работы Foo для нагрузки.


17

Когда вы связываете два бита кода вместе, вы фактически получаете один большой кусок кода. Трудность поддержки небольшого количества кода - по крайней мере квадрат его размера, и возможно выше.

Люди часто смотрят на сложность одного класса (/ function / file / etc.) И забывают, что вы действительно должны учитывать сложность наименьшего отделяемого (инкапсулируемого) модуля. Наличие циклической зависимости увеличивает размер этой единицы, возможно, незаметно (пока вы не начнете пытаться изменить файл 1 и не поймете, что это также требует изменений в файлах 2-127).


14

Они могут быть плохими не сами по себе, а как показатель возможного плохого дизайна. Если Foo зависит от Bar, а Bar зависит от Foo, то уместно задать вопрос, почему их два, а не уникальный FooBar.


10

Хм ... это зависит от того, что вы подразумеваете под круговой зависимостью, потому что на самом деле есть некоторые круговые зависимости, которые, я думаю, очень полезны.

Рассмотрим XML DOM - для каждого узла имеет смысл иметь ссылку на своего родителя, а для каждого родителя - список его потомков. Структура логически представляет собой дерево, но с точки зрения алгоритма сборки мусора или подобного, структура круговая.


1
разве это не дерево?
Конрад Фрикс

@ Конрад: Полагаю, это можно представить как дерево, да. Почему?
Билли ONEAL

1
Я не думаю, что дерево круговое, потому что вы можете перемещаться по его дочерним элементам и завершаться (независимо от родительской ссылки). Если только у узла не было ребенка, который был также предком, который, по моему мнению, делает его графом, а не деревом.
Конрад Фрикс

5
Циркулярная ссылка будет, если один из потомков узла вернется к предку.
Мэтт Оленик

На самом деле это не круговая зависимость (по крайней мере, не таким образом, который вызывает какие-либо проблемы). Например, представьте, что Nodeэто класс, Nodeвнутри которого есть другие ссылки на детей. Поскольку он ссылается только на себя, класс полностью автономен и не связан ни с чем другим. --- С этим аргументом вы можете утверждать, что рекурсивная функция является циклической зависимостью. Это является (на участке), но не в плохом смысле.
до

9

Это как проблема с курицей или яйцом .

Во многих случаях циклическая ссылка неизбежна и полезна, но, например, в следующем случае она не работает:

Проект A зависит от проекта B, а B зависит от A. Необходимо компилировать A для использования в B, который требует компиляции B перед A, который требует компиляции B перед A, который ...


6

Хотя я согласен с большинством комментариев здесь, я хотел бы сослаться на особый случай для круговой ссылки «родитель» / «ребенок».

Классу часто нужно что-то знать о своем родительском или принадлежащем ему классе, возможно, поведении по умолчанию, имени файла, из которого получены данные, операторе sql, который выбрал столбец, или расположении файла журнала и т. Д.

Вы можете сделать это без циклической ссылки, имея содержащий класс, так что то, что раньше было «родителем», теперь является родным братом, но не всегда возможно перефакторировать существующий код, чтобы сделать это.

Другой альтернативой является передача всех данных, которые могут понадобиться дочернему элементу, в его конструкторе, что в итоге просто ужасно.


В примечании, связанном с этим, есть две распространенные причины, по которым X может содержать ссылку на Y: X может захотеть попросить Y сделать что-либо от имени X, или Y может ожидать, что X сделает что-то с Y от имени Y. Если единственные ссылки, которые существуют на Y, предназначены для других объектов, желающих сделать что-то от имени Y, то держателям таких ссылок следует сказать, что услуги Y больше не нужны, и что они должны отказаться от своих ссылок на Y в их удобство.
суперкат

5

В терминах базы данных циклические ссылки с правильными отношениями PK / FK делают невозможным вставку или удаление данных. Если вы не можете удалить из таблицы a, если запись не удалена из таблицы b, и вы не можете удалить из таблицы b, если запись не удалена из таблицы A, вы не можете удалить ее. То же самое со вставками. Вот почему многие базы данных не позволяют вам устанавливать каскадные обновления или удаления, если есть циклическая ссылка, потому что в какой-то момент это становится невозможным. Да, вы можете установить такие отношения без официального объявления PK / Fk, но тогда у вас (в моем опыте 100% случаев) будут проблемы с целостностью данных. Это просто плохой дизайн.


4

Я возьму этот вопрос с точки зрения моделирования.

Пока вы не добавите никаких отношений, которых на самом деле нет, вы в безопасности. Если вы добавите их, вы получите меньше целостности данных (поскольку существует избыточность) и более тесно связанный код.

Особенность циклических ссылок заключается в том, что я не видел ни одного случая, когда они были бы действительно необходимы, кроме одной ссылки на себя. Если вы моделируете деревья или графики, вам это нужно, и с этим все в порядке, потому что самоссылка безвредна с точки зрения качества кода (без добавления зависимости).

Я полагаю, что в тот момент, когда вы начинаете нуждаться в не-самостоятельной ссылке, сразу же вы должны спросить, не можете ли вы смоделировать ее как граф (сверните несколько сущностей в один узел). Может быть, есть случай между тем, когда вы делаете круговую ссылку, но моделирование ее в виде графика не подходит, но я в этом сильно сомневаюсь.

Существует опасность, что люди думают, что им нужна круговая ссылка, но на самом деле это не так. Наиболее распространенный случай - это случай «один из многих». Например, у вас есть клиент с несколькими адресами, из которых один должен быть помечен как основной. Очень заманчиво смоделировать эту ситуацию как два отдельных отношения has_address и is_primary_address_of, но это не правильно. Причина в том, что, будучи основным адресом, это не отдельная связь между пользователями и адресами, а вместо этого это атрибут отношения имеет адрес, Почему это? Поскольку его домен ограничен адресами пользователя, а не всеми адресами, которые там есть. Вы выбираете одну из ссылок и отмечаете ее как самую сильную (основную).

(Теперь поговорим о базах данных) Многие люди выбирают решение для двух отношений, потому что они понимают «первичный» как уникальный указатель, а внешний ключ является своего рода указателем. То есть внешний ключ должен быть тем, что нужно использовать, верно? Неправильно. Внешние ключи представляют отношения, но «первичные» не являются отношениями. Это вырожденный случай упорядочения, когда один элемент превыше всего, а остальные не упорядочены. Если вам нужно смоделировать общий порядок, вы, конечно, будете рассматривать его как атрибут отношения, потому что другого выбора нет. Но в тот момент, когда вы его дегенерируете, есть выбор, и довольно ужасный - моделировать что-то, что не является отношениями, как отношения. Итак, вот оно, избыточность отношений, которую нельзя недооценивать.

Так что я бы не допустил циклическую ссылку, если не будет абсолютно ясно, что она исходит от того, что я моделирую.

(примечание: это слегка смещает дизайн базы данных, но я бы поспорил, что это вполне применимо и к другим областям)


2

Я бы ответил на этот вопрос другим вопросом:

Какую ситуацию вы можете мне представить, когда сохранение круговой эталонной модели является лучшей моделью для того, что вы пытаетесь построить?

Исходя из моего опыта, лучшая модель почти никогда не будет включать циклические ссылки в том смысле, в каком, я думаю, вы это имеете в виду. При этом существует множество моделей, в которых вы постоянно используете циклические ссылки, это просто чрезвычайно просто. Родительские -> дочерние отношения, любая графовая модель и т. Д., Но это хорошо известные модели, и я думаю, что вы имеете в виду нечто совсем другое.


1
МОЖЕТ быть, что круговой связанный список (односвязный или двусвязный) будет отличной структурой данных для центральной очереди событий для программы, которая должна «никогда не останавливаться» (вставьте важные N вещей в очередь с помощью Установите флаг «не удалять», затем просто обойдите очередь до тех пор, пока она не станет пустой, а когда требуются новые задачи (временные или постоянные), вставьте их в подходящее место в очереди, когда вы выполняете операцию «даже» без флага «не удалять». , сделай это, затем убери это из очереди).
Ватин

1

Циркулярные ссылки в структурах данных иногда являются естественным способом выражения модели данных. С точки зрения кодирования, это определенно не идеально и может быть (в некоторой степени) решено путем внедрения зависимостей, перенося проблему из кода в данные.


1

Круговая ссылочная конструкция проблематична не только с точки зрения проектирования, но и с точки зрения обнаружения ошибок.

Рассмотрим возможность сбоя кода. Вы не поместили правильный перехват ошибок ни в один из классов, либо потому, что вы еще не разработали свои методы, либо вы ленивы. В любом случае, у вас нет сообщения об ошибке, чтобы сообщить вам, что произошло, и вам нужно отладить его. Как хороший разработчик программ, вы знаете, какие методы связаны с какими процессами, поэтому вы можете сузить его до тех методов, которые относятся к процессу, вызвавшему ошибку.

С круговыми ссылками ваши проблемы теперь удвоились. Поскольку ваши процессы тесно связаны, у вас нет возможности узнать, какой метод, в каком классе могла быть вызвана ошибка, или откуда возникла ошибка, потому что один класс зависит от другого, зависит от другого. Теперь вам нужно потратить время на тестирование обоих классов одновременно, чтобы выяснить, какой из них действительно ответственен за ошибку.

Конечно, правильная перехват ошибок разрешает это, но только если вы знаете, когда ошибка может произойти. И если вы используете общие сообщения об ошибках, вам все равно не намного лучше.


1

У некоторых сборщиков мусора возникают проблемы с их очисткой, потому что на каждый объект ссылается другой.

РЕДАКТИРОВАТЬ: Как отмечено в комментариях ниже, это верно только для чрезвычайно наивной попытки сборщика мусора, а не той, с которой вы когда-либо сталкивались на практике.


11
Хм ... любой сборщик мусора, которого это сбило, не настоящий сборщик мусора.
Билли ONEAL

11
Я не знаю ни одного современного сборщика мусора, который имел бы проблемы с циклическими ссылками. Циркулярные ссылки являются проблемой, если вы используете счетчики ссылок, но большинство сборщиков мусора используют стиль трассировки (когда вы начинаете со списка известных ссылок и следите за ними, чтобы найти все остальные, собирая все остальное).
Дин Хардинг

4
См. Sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdf, который объясняет недостатки различных типов сборщиков мусора - если взять метку и развернуть и начать оптимизировать его, чтобы сократить длительные паузы (например, создание поколений) Вы начинаете испытывать проблемы с круговыми структурами (когда круговые объекты находятся в разных поколениях). Если вы берете подсчет ссылок и начинаете исправлять проблему с круговыми ссылками, вы в конечном итоге вводите длинные времена паузы, характерные для метки и развертки.
Кен Блум

Если сборщик мусора посмотрел на Foo и освободил его память, которая в этом примере ссылается на Bar, он должен обработать удаление Bar. Таким образом, на данный момент нет необходимости для сборщика мусора идти вперед и удалять панель, потому что это уже было сделано. Или наоборот, если он удаляет Bar, который ссылается на Foo, он также должен удалить Foo, и, следовательно, ему не нужно будет удалять Foo, потому что он сделал это, когда удалял Bar? Пожалуйста, поправьте меня, если я ошибаюсь.
Крис

1
В target-c циклические ссылки делают это таким образом, чтобы счетчик ссылок не достиг нуля при отпускании, что приводит к отключению сборщика мусора.
DexterW

-2

По моему мнению, наличие неограниченных ссылок облегчает разработку программ, но мы все знаем, что некоторые языки программирования не поддерживают их в некоторых контекстах.

Вы упомянули ссылки между модулями или классами. В этом случае это статическая вещь, предопределенная программистом, и для программиста вполне возможно найти структуру, в которой отсутствует округлость, хотя она может не полностью решить проблему.

Настоящая проблема заключается в цикличности в структурах данных времени выполнения, где некоторые проблемы не могут быть определены таким образом, чтобы избавиться от цикличности. В конце концов - проблема, которая должна продиктовать, а требование чего-то еще заставляет программиста решить ненужную головоломку.

Я бы сказал, что проблема в инструментах, а не в принципе.


Добавление одного предложения не вносит существенного вклада в публикацию и не объясняет ответ. Не могли бы вы уточнить это?

Ну, два вопроса, на самом деле автор упоминал ссылки между модулями или классами. В этом случае это статическая вещь, предопределенная программистом, и для программиста вполне возможно найти структуру, в которой отсутствует округлость, хотя она может не полностью решить проблему. Настоящая проблема заключается в цикличности в структурах данных времени выполнения, где некоторые проблемы не могут быть определены таким образом, чтобы избавиться от цикличности. В конце концов - проблема, которая должна продиктовать, а требование чего-то еще заставляет программиста решить ненужную головоломку.
Джош С

Я обнаружил, что это облегчает запуск и запуск вашей программы, но, вообще говоря, это в конечном итоге затрудняет обслуживание программного обеспечения, поскольку вы обнаружите, что тривиальные изменения имеют каскадные эффекты. A делает вызовы в B, который делает обратные вызовы в A, который делает обратные вызовы в B ... Я обнаружил, что трудно по-настоящему понять последствия изменений такого рода, особенно когда A и B полиморфны.
dash-tom-bang
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.