Как распоряжаться ресурсами с помощью внедрения зависимостей - PullRequest
25 голосов
/ 19 декабря 2010

Я использую StructureMap для разрешения ссылок на мой класс репозитория. Мой интерфейс хранилища реализует IDisposable, например,

public interface IMyRepository : IDisposable
{
  SomeClass GetById(int id);
}

Реализация интерфейса с использованием Entity Framework:

public MyRepository : IMyRepository
{
    private MyDbContext _dbContext;

    public MyDbContext()
    {
        _dbContext = new MyDbContext();
    }

    public SomeClass GetById(int id)
    {
        var query = from x in _dbContext
                    where x.Id = id
                    select x;
        return x.FirstOrDefault();
    }

    public void Dispose()
    {
        _dbContext.Dispose();
    }
}

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

Ответы [ 3 ]

22 голосов
/ 20 декабря 2010

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


В то время как DI-фреймворки могут управлять временем жизни объектов для вас, а некоторые могут даже утилизировать объекты для вас после того, как вы поработаете с ними, это делает удаление объектов просто слишкомнеявный.Интерфейс IDisposable создан из-за необходимости детерминированной очистки ресурсов.Поэтому, в контексте DI, я лично хотел бы сделать эту очистку очень явной.Когда вы делаете это явным образом, у вас есть в основном две опции: 1. Сконфигурируйте DI, чтобы он возвращал временные объекты и располагал эти объекты самостоятельно.2. Сконфигурируйте фабрику и проинструктируйте фабрику о создании новых экземпляров.

Я предпочитаю второй подход по сравнению с первым, потому что, особенно при выполнении Dependency Injection, ваш код не так чист, как мог бы.Посмотрите, например, на этот код:

public sealed class Client : IDisposable
{
    private readonly IDependency dependency;

    public Client(IDependency dependency)
    {
        this. dependency = dependency;
    }

    public void Do()
    {
        this.dependency.DoSomething();
    }

    public Dispose()
    {
        this.dependency.Dispose();
    }
}

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

Из-за этого я предпочитаю использовать фабрику.Посмотрите, например, на этот пример:

public sealed class Client
{
    private readonly IDependencyFactory factory;

    public Client(IDependencyFactory factory)
    {
        this.factory = factory;
    }

    public void Do()
    {
        using (var dependency = this.factory.CreateNew())
        {
            dependency.DoSomething();
        }
    }
}

Этот пример ведет себя точно так же, как и в предыдущем примере, но посмотрите, как класс Client больше не должен реализовывать IDisposable, потому что он создаети утилизирует ресурс в методе Do.

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

RPM1984 запросил более конкретный пример.

У меня не было бы репозитория, реализующего IDisposable, но у меня была бы единица работы, которая реализует IDisposable, контролирует / содержит репозитории и имела бы фабрику, которая знает, как создаватьновая единица работ.Имея это в виду, приведенный выше код будет выглядеть следующим образом:

public sealed class Client
{
    private readonly INorthwindUnitOfWorkFactory factory;

    public Client(INorthwindUnitOfWorkFactory factory)
    {
        this.factory = factory;
    }

    public void Do()
    {
        using (NorthwindUnitOfWork db = 
            this.factory.CreateNew())
        {
            // 'Customers' is a repository.
            var customer = db.Customers.GetById(1);

            customer.Name = ".NET Junkie";

            db.SubmitChanges();
        }
    }
}

В проекте, который я использую, и описал здесь , я использую конкретный класс NorthwindUnitOfWork, который переноситIDataMapper, который является шлюзом для основного поставщика LINQ (такого как LINQ to SQL или Entity Framework).В общих чертах, проект выглядит следующим образом:

  1. * * * * * * * * В клиенте вводится INorthwindUnitOfWorkFactory.
  2. Конкретная реализация этой фабрики создает конкретный класс NorthwindUnitOfWork и внедряетO / RM специфический IDataMapper класс в нем.
  3. NorthwindUnitOfWork на самом деле является типобезопасной оболочкой для IDataMapper, а NorthwindUnitOfWork запрашивает IDataMapper для репозиториев и перенаправляет запросыотправить изменения и избавиться от сопоставителя.
  4. IDataMapper возвращает Repository<T> классы, а репозиторий реализует IQueryable<T>, чтобы позволить клиенту использовать LINQ поверх репозитория.
  5. Конкретная реализацияIDataMapper содержит ссылку на определенную единицу работы O / RM (например, EF ObjectContext).По этой причине IDataMapper должен реализовывать IDisposable.

Это приводит к следующему дизайну:

public interface INorthwindUnitOfWorkFactory
{
    NorthwindUnitOfWork CreateNew();
}

public interface IDataMapper : IDisposable
{
    Repository<T> GetRepository<T>() where T : class;

    void Save();
}

public abstract class Repository<T> : IQueryable<T>
    where T : class
{
    private readonly IQueryable<T> query;

    protected Repository(IQueryable<T> query)
    {
        this.query = query;
    }

    public abstract void InsertOnSubmit(T entity);

    public abstract void DeleteOnSubmit(T entity);

    // IQueryable<T> members omitted.
}

NorthwindUnitOfWork - это конкретный класс, который содержит свойства дляконкретные репозитории, такие как Customers, Orders и т. д .:

public sealed class NorthwindUnitOfWork : IDisposable 
{
    private readonly IDataMapper mapper;

    public NorthwindUnitOfWork(IDataMapper mapper)
    {
        this.mapper = mapper;
    }

    // Repository properties here:    
    public Repository<Customer> Customers
    {
        get { return this.mapper.GetRepository<Customer>(); }
    }

    public void Dispose()
    {
        this.mapper.Dispose();
    }
}

Остается конкретная реализация INorthwindUnitOfWorkFactory и конкретная реализация IDataMapper.Вот один для Entity Framework:

public class EntityFrameworkNorthwindUnitOfWorkFactory
    : INorthwindUnitOfWorkFactory
{
    public NorthwindUnitOfWork CreateNew()
    {
        var db = new ObjectContext("name=NorthwindEntities");
        db.DefaultContainerName = "NorthwindEntities";
        var mapper = new EntityFrameworkDataMapper(db);
        return new NorthwindUnitOfWork(mapper);
    }
}

И EntityFrameworkDataMapper:

public sealed class EntityFrameworkDataMapper : IDataMapper
{
    private readonly ObjectContext context;

    public EntityFrameworkDataMapper(ObjectContext context)
    {
        this.context = context;
    }

    public void Save()
    {
        this.context.SaveChanges();
    }

    public void Dispose()
    {
        this.context.Dispose();
    }

    public Repository<T> GetRepository<T>() where T : class
    {
        string setName = this.GetEntitySetName<T>();

        var query = this.context.CreateQuery<T>(setName);
        return new EntityRepository<T>(query, setName);
    }

    private string GetEntitySetName<T>()
    {
        EntityContainer container =
            this.context.MetadataWorkspace.GetEntityContainer(
            this.context.DefaultContainerName, DataSpace.CSpace);

        return (
            from item in container.BaseEntitySets
            where item.ElementType.Name == typeof(T).Name
            select item.Name).First();
    }

    private sealed class EntityRepository<T>
        : Repository<T> where T : class
    {
        private readonly ObjectQuery<T> query;
        private readonly string entitySetName;

        public EntityRepository(ObjectQuery<T> query,
            string entitySetName) : base(query)
        {
            this.query = query;
            this.entitySetName = entitySetName;
        }

        public override void InsertOnSubmit(T entity)
        {
            this.query.Context.AddObject(entitySetName, entity);
        }

        public override void DeleteOnSubmit(T entity)
        {
            this.query.Context.DeleteObject(entity);
        }
    }
}

Вы можете найти больше информации об этой модели здесь .

ОБНОВЛЕНИЕ Декабрь 2012

Это обновление написано через два года после моего первоначального ответа. За последние два года многое изменилось в том, как я пытаюсь проектировать системы, над которыми я работаю. Хотя это подходило мне в прошлом, я больше не люблю использовать фабричный подход при работе с шаблоном Unit of Work. Вместо этого я просто внедряю экземпляр Unit of Work непосредственно в потребителей. Однако то, насколько это возможно для вас, во многом зависит от того, как спроектирована ваша система. Если вы хотите узнать больше об этом, взгляните на мой новый ответ Stackoverflow: Один DbContext на веб-запрос ... почему?

11 голосов
/ 20 декабря 2010

Если вы хотите сделать все правильно, я бы посоветовал пару изменений:

1 - Не иметь частных экземпляров контекста данных в хранилище.Если вы работаете с несколькими репозиториями, вы получите несколько контекстов.

2 - Чтобы решить вышесказанное - оберните контекст в Единицу работы .Передайте единицу работы в хранилище через ctor: public MyRepository(IUnitOfWork uow)

3 - Сделайте так, чтобы единица работы навесного оборудования была одноразовой.Единица работы должна быть «обновлена», когда запрос начинается, и, следовательно, должна быть ликвидирована, когда запрос заканчивается.Репозиторий не должен реализовывать IDisposable, так как он не работает напрямую с ресурсами - он просто смягчает их.DataContext / Unit of Work должен реализовывать IDispoable.

4 - Если вы используете веб-приложение, вам не нужно явно вызывать dispose - я повторяю, вам не нужно явно вызывать disposeметод .StructureMap имеет метод с именем HttpContextBuildPolicy.DisposeAndClearAll();.Это вызывает метод «Dispose» для любых объектов в области HTTP, которые реализуют IDisposable.Вставьте этот звонок в Application_EndRequest (Global.asax).Кроме того - я считаю, что есть обновленный метод, называемый ReleaseAllHttpScopedObjects или что-то еще - не могу вспомнить имя.

0 голосов
/ 19 декабря 2010

Вместо добавления Dispose в IMyRepository, вы можете объявить IMyRepository следующим образом:

public interface IMyRepository: IDisposable
{
  SomeClass GetById(int id);
} 

Таким образом, вы гарантируете, что весь репозиторий будет иногда вызывать Dispose, и вы можете использовать шаблон C # «using» дляОбъект репозитория:

using (IMyRepository rep = GetMyRepository(...))
{
   ... do some work with rep
}
...