Дилемма фабричного шаблона и времени жизни внедренных зависимостей

Это беспокоило меня в течение долгого времени, и я не мог найти правильный ответ.

Проблема.

Представьте, что у вас есть фабричный интерфейс (пример C#):

interface IFooFactory
{
    IFoo Create();
}

и его реализация зависит от службы:

class FooFactory : IFooFactory
{
    private readonly IBarService _barService;
    public FooFactory(IBarService barService)
    {
        _barService = barService;
    }
}

где интерфейс службы реализует IDisposable для корректного завершения работы:

interface IBarService : IDisposable
{
    ...
}

Теперь реальный класс, созданный фабрикой, имеет 2 зависимости — сам сервис (прошедший через фабрику) и еще один объект, созданный фабрикой:

class Foo : IFoo
{
    public Foo(IBarService barService, IQux qux)
    {
        ...
    }
}

и фабрика может создать его так:

class FooFactory : IFooFactory
{
    public IFoo Create()
    {
        IQux qux = new Qux();
        return new Foo(_barService, qux);
    }
}

Наконец, IFoo и IQux также реализуют IDisposable, поэтому класс Foo реализует:

class Foo : IFoo
{
    public void Dispose()
    {
        _qux.Dispose();
    }
}

Но почему мы размещаем Qux только на Foo.Dispose()? Обе зависимости внедряются, и мы просто полагаемся на знание точной реализации фабрики, где Bar — это общая служба (тип отношения ассоциации), а Qux используется исключительно Foo (тип отношения композиции). Можно легко избавиться от них обоих по ошибке. И в обоих случаях Foo логически не владеет ни одной из зависимостей, поэтому удаление любой из них кажется неправильным. Помещение создания Qux внутри Foo сведет на нет внедрение зависимостей, так что это не вариант.

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

Возможное решение.

Итак, вот одно из возможных не очень красивых решений:

class FooFactory : IFooFactory
{
    private readonly IBarService _barService;
    public FooFactory(IBarService barService)
    {
        _barService = barService;
    }
    public IFoo Create()
    {
        // This lambda can capture and use any input argument.
        // Also creation can be complex and involve IO.
        var quxFactory = () => new Qux();
        return new Foo(_barService, quxFactory);
    }
}
class Foo : IFoo
{
    public Foo(IBarService barService, Func<IQux> quxFactory)
    {
        // Injected - don't own.
        _barService = barService;
        // Foo creates - Foo owns.
        _qux = quxFactory();
    }
    public void Dispose()
    {
        // Now it's clear what Foo owns from the code in the constructor.
        _qux.Dispose();
    }
}

Я не сторонник вызова возможно сложной логики в конструкторе, особенно если это async, и вызов его по запросу (ленивая загрузка) также может привести к неожиданным ошибкам позднего выполнения (по сравнению с быстрым сбоем).

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


person Serge Semenov    schedule 20.01.2018    source источник


Ответы (2)


Прежде всего это:

interface IBarService : IDisposable
{
    ...
}

является дырявой абстракцией. Мы знаем только, что BarService имеет одноразовую зависимость, внедренную конструктором. Это не гарантирует, что каждая реализация IBarService должна быть одноразовой. Чтобы устранить дырявую абстракцию, IDisposable следует применять только к тем конкретным реализациям, которые действительно требуют этого.

interface IBarService
{
    ...
}

class BarService : IBarService, IDisposable
{
    ...
}

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

В общем случае это означает, что при использовании DI-контейнера контейнер DI отвечает как за создание экземпляра, так и за удаление экземпляра.

Однако шаблон внедрения зависимостей также позволяет подключать компоненты без контейнера внедрения зависимостей. Таким образом, чтобы быть на 100% тщательными, мы должны проектировать компоненты с учетом одноразовых зависимостей.

Вкратце это означает:

  1. Нам нужно сделать Foo одноразовым, так как он имеет одноразовую зависимость
  2. Поскольку вызывающий объект фабрики отвечает за создание экземпляра Foo, нам нужно предоставить вызывающему объекту фабрики способ удаления Foo

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

interface IFooFactory
{
     IFoo Create();
     void Release(IFoo foo);
}

class FooFactory : IFooFactory
{
    private readonly IBarService _barService;
    private readonly IQux qux;
    public FooFactory(IBarService barService, IQux qux)
    {
        _barService = barService;
        _qux = qux;
    }
    public IFoo Create()
    {
        return new Foo(_barService, _qux);
    }
    public void Release(IFoo foo)
    {
        // Handle both disposable and non-disposable IFoo implementations
        var disposable = foo as IDisposable;
        if (disposable != null)
            disposable.Dispose();
    }
}

class Foo : IFoo, IDisposable
{
    public Foo(IBarService barService, IQux quxFactory)
    {
        _barService = barService;
        _qux = quxFactory;
    }

    public void Dispose()
    {
        _barService.Dispose();
        _qux.Dispose();
    }
}

И тогда схема использования factory выглядит примерно так:

// Caller creates the instance, the caller owns
var foo = factory.Create();
try
{
    // do something with foo
}
finally
{
    factory.Release(foo);
}

Метод Release на 100 % гарантирует, что одноразовые компоненты будут правильно утилизированы, независимо от того, подключено ли приложение-потребитель с помощью контейнера DI или с использованием чистого DI.

Обратите внимание, что именно фабрика решает, утилизировать IFoo или нет. Таким образом, при использовании контейнера DI реализация может быть опущена. Однако, поскольку вызов Dispose() более одного раза должен быть безопасным, мы могли бы просто оставить его на месте (и, возможно, использовать логическое значение, чтобы гарантировать, что dispose не вызывается более одного раза для ошибочных компонентов, которые не работают). не допускаю возможности).

person NightOwl888    schedule 20.01.2018
comment
Мне нравится идея дырявой абстракции, но я хотел бы больше сосредоточиться на зависимости Qux. Я не согласен с приведенным выше примером, потому что я не хочу (1) удалять _barService преждевременно и (2) опять же, Foo не владеет ни одной из внедренных зависимостей и не должен их удалять - это плохая практика, которая приводит к очень опасным результаты. Это моя самая большая проблема, и в приведенном выше примере она все еще присутствует. И да, если создается DI-фреймворк, он должен его утилизировать. Возвращаясь к Qux - его нужно создавать каждый раз и привязывать к времени жизни Foo. - person Serge Semenov; 20.01.2018

Вы обновляете Qux каждый раз, если вы можете передать это DI-фреймворку, тогда вам не нужно будет вызывать dispose, фреймворк будет работать, когда нужно.

person sramalingam24    schedule 20.01.2018
comment
Позвольте мне уточнить, Qux должен создаваться каждый раз, и его время жизни привязано к времени жизни Foo. Я не знаю, как DI-фреймворк может помочь в этом. - person Serge Semenov; 20.01.2018
comment
Мне кажется, что Qux - это данные времени выполнения. Данные среды выполнения — это не то, что вы должны позволять создавать в контейнере внедрения зависимостей, но вы также не должны использовать данные времени выполнения для создания компонентов, потому что это приводит к ситуации, в которой вы сейчас находитесь, что вызывает добавлена ​​сложность фабричных абстракций. - person Steven; 22.01.2018
comment
Я ценю обратную связь @Steven, но не уверен, что это сработает. Я собираюсь изучить, как это может сопоставляться с распределенными службами и рабочими процессами (причина, по которой я задаю этот вопрос SO), потому что не существует такой вещи, как распределенный контейнер IoC. - person Serge Semenov; 20.02.2018