Почему циркулярные ссылки считаются вредными?

Почему это плохой дизайн для объекта для ссылки на другой объект, который ссылается на первый?

Круговые зависимости между classами не обязательно вредны. Действительно, в некоторых случаях они желательны. Например, если ваша заявка касается домашних животных и их владельцев, вы можете ожидать, что у classа Pet будет метод, чтобы получить владельца домашнего животного, а class владельца – метод, возвращающий список домашних животных. Конечно, это может затруднить управление памятью (на языке, отличном от GC). Но если круговость присуща проблеме, то попытка избавиться от нее, вероятно, приведет к большему количеству проблем.

С другой стороны, круговые зависимости между модулями вредны. Как правило, это показатель плохо продуманной структуры модуля и / или отказ от первоначальной модуляции. В общем, кодовая база с неконтролируемыми кросс-зависимостями будет сложнее понять и сложнее поддерживать, чем одна с чистой, слоистой структурой модуля. Без достойных модhive может быть намного сложнее предсказать последствия изменений. И это затрудняет техническое обслуживание и приводит к «разложению кода» в результате плохо продуманного исправления.

(Кроме того, такие инструменты сборки, как Maven, не будут обрабатывать модули (артефакты) с круговыми зависимостями.)

Циркулярные ссылки не всегда вредны – есть некоторые варианты использования, где они могут быть весьма полезными. На ум приходят двуместные списки, модели графов и грамматики на компьютерном языке. Однако, как правило, существует несколько причин, по которым вы можете избежать круговых ссылок между объектами.

  1. Консистенция данных и графов. Обновление объектов с помощью циклических ссылок может создать проблемы в обеспечении того, что во все моменты времени отношения между объектами действительны. Этот тип проблемы часто возникает в реализациях объектно-реляционного моделирования, где нередко можно найти двунаправленные циклические ссылки между объектами.

  2. Обеспечение атомных операций. Обеспечение того, что изменения в обоих объектах в кольцевой ссылке являются атомарными, может усложняться – особенно при многопоточности. Для обеспечения согласованности графического объекта, доступного из нескольких streamов, требуются специальные структуры синхронизации и операции блокировки, чтобы гарантировать, что ни один stream не видит неполный набор изменений.

  3. Проблемы физического разделения. Если два разных classа A и B обращаются друг к другу круговым способом, становится сложным отделить эти classы от независимых сборок. Конечно, возможно создать третью сборку с интерфейсами IA и IB, которые реализуют A и B; позволяя каждому ссылаться на другие через эти интерфейсы. Также возможно использовать слабо типизированные ссылки (например, объект) как способ разбить круговую зависимость, но доступ к методу и свойствам такого объекта не может быть легко доступным, что может привести к отказу от цели ссылки.

  4. Обеспечение неизменяемых круговых ссылок. Языки, такие как C # и VB, предоставляют ключевые слова, чтобы ссылки в объекте были неизменными (только для чтения). Неизменяемые ссылки позволяют программе гарантировать, что ссылка ссылается на один и тот же объект на время жизни объекта. К сожалению, нелегко использовать механизм принудительной принудительной реализации компилятора, чтобы гарантировать, что циклические ссылки не могут быть изменены. Это можно сделать только в том случае, если один объект создает экземпляр другого (см. Пример ниже).

     class A { private readonly B m_B; public A( B other ) { m_B = other; } } class B { private readonly A m_A; public A() { m_A = new A( this ); } } 
  5. Доступность программы и ремонтопригодность. Циркулярные ссылки по своей сути являются хрупкими и легко разбиваются. Это частично связано с тем, что чтение и понимание кода, который включает циклические ссылки, сложнее, чем код, который их избегает. Обеспечение того, что ваш код легко понять и поддерживать, помогает избежать ошибок и позволяет сделать изменения более легко и безопасно. Объекты с круговыми ссылками сложнее для модульного теста, потому что они не могут быть протестированы изолированно друг от друга.

  6. Управление жизненным циклом объекта. В то время как сборщик мусора .NET способен идентифицировать и обрабатывать циклические ссылки (и правильно распоряжаться такими объектами), не все языки / среды могут. В средах, которые используют подсчет ссылок для своей схемы сбора мусора (например, VB6, Objective-C, некоторые библиотеки C ++), циклические ссылки могут привести к утечкам памяти. Поскольку каждый объект держится на другом, их отсчеты ссылок никогда не достигнут нуля и, следовательно, никогда не станут кандидатами на сбор и очистку.

Потому что теперь они действительно один объект. Вы не можете тестировать ни одного отдельно.

Если вы измените его, вероятно, вы также повлияли на его спутника.

Из Википедии:

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

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

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

Такой объект может быть трудно создать и уничтожить, потому что для того, чтобы сделать либо неатомно, вы должны нарушить ссылочную целостность, чтобы сначала создать / уничтожить один, а другой (например, ваша firebase database SQL могла бы скрыть это). Это может смутить ваш сборщик мусора. Perl 5, который использует простой подсчет ссылок для сбора мусора, не может (без помощи), чтобы его утечка памяти. Если два объекта имеют разные classы, теперь они плотно связаны и не могут быть разделены. Если у вас есть менеджер пакетов для установки этих classов, круговая зависимость распространяется на него. Он должен знать, чтобы установить оба пакета перед их тестированием, который (выступая в качестве хранителя системы сборки) является PITA.

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

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

Вот несколько примеров, которые могут помочь проиллюстрировать, почему круговые зависимости плохи.

Проблема №1: Что сначала инициализируется / строится?

Рассмотрим следующий пример:

 class A { public A() { myB.DoSomething(); } private B myB = new B(); } class B { public B() { myA.DoSomething(); } private A myA = new A(); } 

Какой конструктор называется первым? Невозможно быть уверенным, потому что это совершенно двусмысленно. Один или другой метод DoSomething будет вызван на объект, который не инициализирован. Это приводит к неправильному поведению и, скорее всего, к возникновению исключения. Есть способы обойти эту проблему, но они все уродливые, и все они требуют инициализаторов неконструктора.

Проблема № 2:

В этом случае я перешел на неконтролируемый C ++-пример, потому что реализация .NET по дизайну скрывает проблему от вас. Однако в следующем примере проблема станет довольно ясной. Мне хорошо известно, что .NET не использует подсчет ссылок под капотом для управления памятью. Я использую его здесь только для иллюстрации основной проблемы. Заметим также, что я продемонстрировал здесь одно возможное решение проблемы №1.

 class B; class A { public: A() : Refs( 1 ) { myB = new B(this); }; ~A() { myB->Release(); } int AddRef() { return ++Refs; } int Release() { --Refs; if( Refs == 0 ) delete(this); return Refs; } B *myB; int Refs; }; class B { public: B( A *a ) : Refs( 1 ) { myA = a; a->AddRef(); } ~B() { myB->Release(); } int AddRef() { return ++Refs; } int Release() { --Refs; if( Refs == 0 ) delete(this); return Refs; } A *myA; int Refs; }; // Somewhere else in the code... ... A *localA = new A(); ... localA->Release(); // OK, we're done with it ... 

На первый взгляд, можно подумать, что этот код правильный. Код подсчета ссылок довольно прост и прямолинейен. Однако этот код приводит к утечке памяти. Когда A построено, вначале он имеет счетчик ссылок «1». Однако инкапсулированная переменная myB увеличивает счетчик ссылок, присваивая ему счетчик «2». Когда localA освобождается, счетчик уменьшается, но возвращается только к «1». Следовательно, объект остается висящим и никогда не удаляется.

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

Совершенно нормально иметь объекты с круговыми ссылками, например, в модели домена с двунаправленными ассоциациями. С этим можно справиться ORM с правильно написанным компонентом доступа к данным.

Обратитесь к книге Лакоса, в разработке программного обеспечения на С ++ циклическая физическая зависимость нежелательна. Существует несколько причин:

  • Это затрудняет их тестирование и невозможность повторного использования.
  • Это затрудняет людям понимание и поддержание.
  • Это увеличит стоимость времени соединения.

Циркулярные ссылки, по-видимому, представляют собой законный сценарий моделирования домена. Примером является Hibernate, и многие другие инструменты ORM поддерживают эту перекрестную связь между сущностями, чтобы обеспечить двунаправленную навигацию. Типичный пример в онлайн-аукционной системе, продавец может сохранить ссылку на Список лиц, которые он / она продает. И каждый элемент может поддерживать ссылку на соответствующего продавца.

Сборщик мусора .NET может обрабатывать циклические ссылки, поэтому нет страха утечки памяти для приложений, работающих на платформе .NET.

  • Когда следует использовать «это» в classе?
  • Обработка инкрементного моделирования данных Изменения в функциональном программировании
  • Почему мы используем интерфейс? Это только для стандартизации?
  • Разница между объектом и экземпляром
  • Какое определение «интерфейс» в объектно-ориентированном программировании
  • Зачем использовать интерфейсы, множественное наследование и интерфейсы, преимущества интерфейсов?
  • Почему инкапсуляция является важной особенностью языков ООП?
  • Заводской шаблон в C #: Как обеспечить, чтобы экземпляр объекта мог быть создан только фабричным classом?
  • Векторы и polymorphism в C ++
  • Способ литья базового типа в производный тип
  • издевательский одноэлементный class
  • Interesting Posts
    Давайте будем гением компьютера.