Как спроектировать класс, чтобы круговые зависимости не вызывали производные члены перед созданием?

(Я пометил это и как C#, и как Java, поскольку это один и тот же вопрос на обоих языках.)

Скажем, у меня есть эти классы

interface IKernel
{
    // Useful members, e.g. AvailableMemory, TotalMemory, etc.
}

class Kernel : IKernel
{
    private /*readonly*/ FileManager fileManager;  // Every kernel has 1 file manager
    public Kernel() { this.fileManager = new FileManager(this); /* etc. */ }

    // implements the interface; members are overridable
}

class FileManager
{
    private /*readonly*/ IKernel kernel;  // Every file manager belongs to 1 kernel
    public FileManager(IKernel kernel) { this.kernel = kernel; /* etc. */ }
}

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

Эта проблема не возникает в языках, в которых можно определить настоящие конструкторы (а не инициализаторы, такие как C#/Java), поскольку в этом случае подклассы даже не существуют до их конструкторы вызываются... но здесь возникает эта проблема.

Итак, каков наилучший/правильный дизайн/практика, чтобы этого не произошло?

Редактировать:

Я не обязательно говорю, что мне нужны циклические ссылки, но дело в том, что и Kernel, и FileManager зависят друг от друга. Если у вас есть предложение, как решить эту проблему без использования циклических ссылок, это тоже здорово!


person user541686    schedule 18.05.2012    source источник
comment
Вы можете пояснить это утверждение? Эта проблема не возникает в языках, где вы можете определить настоящие конструкторы (а не инициализаторы, такие как C#)... но здесь это происходит.   -  person asawyer    schedule 19.05.2012
comment
@asawyer: Да, смотрите мое обновление. (Я имею в виду Python/C++, хотя в этом отношении они немного отличаются друг от друга.) В C++ производные члены не существуют до тех пор, пока не будет вызван конструктор подкласса; в Python есть методы класса, называемые конструкторами, которые возвращают новый член класса (а не просто инициализируют новый экземпляр). Оба они как бы «обходят» эту проблему, хотя у них есть свои подводные камни.   -  person user541686    schedule 19.05.2012
comment
Должен признаться, я никогда не сталкивался с этой проблемой. Все время, когда у меня было что-то подобное, второй объект («подкомпонент») никогда ничего не делал с основным объектом в своем конструкторе, кроме сохранения его для будущего использования, что не создает проблем. Думаю, если бы я столкнулся с этим, я бы поместил «проблемный» код (где подкомпонент вызывает компонент обратно) вне конструктора подкомпонента и вызывал его только тогда, когда основной компонент готов.   -  person zmbq    schedule 19.05.2012


Ответы (4)


Лично я не люблю циклические ссылки. Но если вы решите их оставить, можно добавить немного лени:

interface IKernel
{
    // Useful members, e.g. AvailableMemory, TotalMemory, etc.
}

class Kernel : IKernel
{
    private readonly Lazy<FileManager> fileManager;  // Every kernel has 1 file manager
    public Kernel() { this.fileManager = new Lazy<FileManager>(() => new FileManager(this)); /* etc. */ }

    // implements the interface; members are overridable
}

class FileManager
{
    private /*readonly*/ IKernel kernel;  // Every file manager belongs to 1 kernel
    public FileManager(IKernel kernel) { this.kernel = kernel; /* etc. */ }
}  

Ленивость здесь позволяет гарантировать, что реализация IKernel будет полностью инициализирована, когда будет запрашиваться экземпляр FileManager.

person Dennis    schedule 18.05.2012
comment
+1 кажется хорошим вариантом. (Как-то глупо я не видел этого решения...) - person user541686; 19.05.2012
comment
Это работает только до тех пор, пока в конструкторе не используется fileManager. Так что теперь, вместо того, чтобы быть осторожным в конструкторе FileManager, вы должны быть осторожны в конструкторе Kernel. Проблема просто смещает свое слабое место. Также обратите внимание, что подклассы Kernel одинаково уязвимы. - person Dmytro Shevchenko; 19.05.2012
comment
@Shedal: если быть точнее ... Если конструктор ядра не вызывает fileManager.Value, это работает нормально. Кроме того, это не универсальное лекарство. Думаю, Мехрдад это понимает. - person Dennis; 21.05.2012

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

Я думаю, вы должны решить, какой объект является основным, а какой является предметом для агрегации или даже композиции. Затем создайте вторичный объект внутри основного или, альтернативно, внедрите его как зависимость от основного объекта. Затем позвольте основному объекту зарегистрировать свои методы обратного вызова во вторичном объекте, который будет вызывать их всякий раз, когда ему нужно будет связаться с «внешним миром».

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

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

Вот пример того, что я имею в виду:

class Program
{
    static void Main( )
    {
        FileManager fm = new FileManager( );
        Kernel k = new Kernel( fm );
        fm.DoSomething( 10 );
    }
}

class Kernel
{
    private readonly FileManager fileManager;
    public Kernel( FileManager fileManager )
    {
        this.fileManager = fileManager;
        this.fileManager.OnDoSomething += OnFileManagerDidSomething;
    }

    ~Kernel()
    {
        this.fileManager.OnDoSomething -= OnFileManagerDidSomething;
    }

    protected virtual void OnFileManagerDidSomething( int i )
    {
        Console.WriteLine( i );
    }
}

class FileManager
{
    public event Action<int> OnDoSomething;

    public void DoSomething( int i )
    {
        // ...

        OnDoSomething.Invoke( i );
    }
}
person Dmytro Shevchenko    schedule 18.05.2012
comment
Не могли бы вы привести пример того, что вы имеете в виду (например, код)? (Кстати, здесь Kernel определенно является основным, а FileManager — подкомпонентом.) - person user541686; 19.05.2012
comment
А, спасибо. Хм... если вы используете события, то как убедиться, что что-то не вызывается после выполнения OnDoSomething += OnFileManagerDidSomething;, но до завершения конструктора? (Обратите внимание, что FileManager может что-то сделать, когда вы говорите +=, возможно, чтобы получить какой-то хеш-ключ или что-то еще из вашего ядра.) Я думаю, что окончательный ответ может быть просто осторожным :) так что если это ответ, то это тоже работает... - person user541686; 19.05.2012
comment
@Mehrdad Ну, главное, что FileManager ничего не знает о ядре. Таким образом, ядру на самом деле все равно, что делает FileManager, так как FileManager может повлиять на него только через обработчики событий, полностью контролируемые ядром. - person Dmytro Shevchenko; 19.05.2012
comment
Верно, но как узнать, что обработчики событий не переопределены? - person user541686; 19.05.2012
comment
@Mehrdad События являются многоадресными в C#. Таким образом, для одного события может быть зарегистрировано несколько обработчиков событий. Вы можете пойти дальше и разрешить только добавление новых обработчиков, но не отменять регистрацию существующих... или добавить к этому более сложную логику. Все через синтаксис add {...} remove {...}. - person Dmytro Shevchenko; 19.05.2012
comment
Нено, я не это имею в виду. Я хочу сказать, что OnFileManagerDidSomething является виртуальным, так что не вызывает ли это ту же проблему, которую мы пытались решить? - person user541686; 19.05.2012
comment
@Mehrdad Тогда самое простое решение — использовать инъекцию через свойство вместо инъекции через конструктор. Тогда вы точно знаете, что объект полностью построен. - person Dmytro Shevchenko; 19.05.2012
comment
Кстати, разве нет только двух возможных способов все испортить? 1) FileManager сразу же вызывает событие при его добавлении (плохо, этого не должно происходить); 2) Доступ к FileManager осуществляется из отдельного потока прямо при добавлении события (думаю, есть над чем подумать). - person Dmytro Shevchenko; 19.05.2012
comment
Нет, есть третий способ: FileManager вызывает kernel для получения некоторой информации при добавлении ее как события. (Не забывайте, что события C# необязательно являются простыми средствами добавления/удаления поверх делегатов. Это только в реализации по умолчанию. Например, в Windows Forms используется хэш-таблица — и любой механизм этого Для сортировки потребуется вычисление хеш-значения для владельца (ядра), что может быть вызовом виртуального метода.) - person user541686; 19.05.2012
comment
Как FileManager может позвонить Kernel за какой-то информацией, если он ничего не знает о Kernel? Он видит только добавленный метод и не знает, к чему он относится. Или вы имеете в виду, что CLR может попытаться получить доступ к объекту Kernel в результате действий FileManager? Вы меня заинтриговали, можно поподробнее? - person Dmytro Shevchenko; 19.05.2012
comment
Я думал, что произойдет, если сумматор событий FileManager будет использовать свойство Delegate.Target для доступа к ядру, но я думаю, что правильный код не сделает этого (если только он действительно не знает, что делает), так что неважно, я думаю, что это работает, спасибо . :) +1 - person user541686; 19.05.2012

Если вам нужно сохранить пары объектов со ссылками друг на друга, вы должны предоставить утилиту для их правильного построения. Используйте шаблон Factory и уменьшите сложность построения, спрятав конструкцию и сборку за методами шаблона Factory.

В Java поместите конструктор в пакет и сделайте конструкторы внутреннего компонента и методы начальной установки присваивания "пакетными частными"

public Kernel newKernel() {
  Kernel kernel = new Kernel();
  Filesystem filesystem = new Filesystem();
  kernel.setFilesystem(filesystem);
  filesystem.setKernel(kernel);
  return kernel;
}

public Filesystem newFilesystem() {
  Kernel kernel = new Kernel();
  Filesystem filesystem = new Filesystem();
  kernel.setFilesystem(filesystem);
  filesystem.setKernel(kernel);
  return filesystem;
}

Аналогичная идея может быть реализована в C++ с продуманным использованием private и friend.

person Edwin Buck    schedule 18.05.2012
comment
+1 Я думал фабрика, но не видел, как это будет работать... хотя, похоже, это может сработать. - person user541686; 19.05.2012

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

  1. A leak-safe constructor may use leak-safe parameters only to call call nested leak-safe constructors (where the parameters must also be passed as leak-safe), or store them in its own fields.
  2. A leak-safe constructor may not dereference any leak-safe parameters, nor any field that is loaded from a leak-safe parameter.
  3. A leak-safe constructor may not pass the object under construction anywhere except as a leak-safe parameter to a nested leak-safe constructor.
  4. An object constructed by calling passing a leak-safe parameter or the object under construction to a leak-safe constructor will be subject to all of the constraints of a leak-safe parameter.

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

person supercat    schedule 19.08.2012