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



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

873   11  

11 ответов:

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

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

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

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

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

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

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

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

    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++), циклические ссылки могут привести к утечкам памяти. Поскольку каждый объект держится за другой, их количество ссылок никогда не достигнет нуля и, следовательно, никогда не станет кандидаты для сбора и очистки.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Comments

    Ничего не найдено.