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

71
задан Yvette Colomb 12 December 2018 в 19:51
поделиться

9 ответов

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

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

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

70
ответ дан 24 November 2019 в 12:58
поделиться

It hurts code readability. And from circular dependencies to spaghetti code there is just a tiny step.

2
ответ дан 24 November 2019 в 12:58
поделиться

From Wikipedia:

Circular dependencies can cause many unwanted effects in software programs. Most problematic from a software design point of view is the tight coupling of the mutually dependent modules which reduces or makes impossible the separate re-use of a single module.

Circular dependencies can cause a domino effect when a small local change in one module spreads into other modules and has unwanted global effects (program errors, compile errors). Circular dependencies can also result in infinite recursions or other unexpected failures.

Circular dependencies may also cause memory leaks by preventing certain very primitive automatic garbage collectors (those that use reference counting) from deallocating unused objects.

7
ответ дан 24 November 2019 в 12:58
поделиться

Such an object can be difficult to be created and destroyed, because in order to do either non-atomicly you have to violate referential integrity to first create/destroy one, then the other (for example, your SQL database might balk at this). It might confuse your garbage collector. Perl 5, which uses simple reference counting for garbage collection, cannot (without help) so its a memory leak. If the two objects are of different classes now they are tightly coupled and cannot be separated. If you have a package manager to install those classes the circular dependency spreads to it. It must know to install both packages before testing them, which (speaking as a maintainer of a build system) is a PITA.

That said, these can all be overcome and its often necessary to have circular data. The real world is not made up of neat directed graphs. Many graphs, trees, hell, a double-linked list is circular.

5
ответ дан 24 November 2019 в 12:58
поделиться

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

-2
ответ дан 24 November 2019 в 12:58
поделиться

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

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

8
ответ дан 24 November 2019 в 12:58
поделиться

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

Проблема №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 утверждает, что может справиться с этим, но я не уверен, что доверяю ему, потому что это очень сложная проблема. Go, с другой стороны, решает проблему, просто не разрешая циклические ссылки. Десять лет назад я предпочел бы подход .NET за его гибкость. В наши дни я предпочитаю подход Go из-за его простоты.

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

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

2
ответ дан 24 November 2019 в 12:58
поделиться

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

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

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

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

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

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

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

58
ответ дан 24 November 2019 в 12:58
поделиться

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

.
1
ответ дан 24 November 2019 в 12:58
поделиться
Другие вопросы по тегам:

Похожие вопросы: