Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

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

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

  • Обеспечение неизменяемых циклических ссылок. Языки, такие как С# и 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 ); }
    }
    
  • Доступность и удобство обслуживания. Циркулярные ссылки по своей сути являются хрупкими и легко разбиваются. Это частично связано с тем, что чтение и понимание кода, который включает циклические ссылки, сложнее, чем код, который их избегает. Обеспечение того, что ваш код легко понять и поддерживать, помогает избежать ошибок и позволяет сделать изменения более легко и безопасно. Объекты с круговыми ссылками сложнее unit test, потому что они не могут быть протестированы изолированно друг от друга.

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

Ответ 3

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

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

Ответ 4

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

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

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

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

Ответ 5

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

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

Ответ 6

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

Ответ 7

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

Проблема №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:

В этом случае я изменился на неконтролируемый С++-пример, потому что реализация .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 для его простоты.

Ответ 8

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

Ответ 9

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

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

Ответ 10

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

Ответ 11

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