Контейнер зависимостей: как создать экземпляры объекта

Я использую Unity как движок впрыска зависимостей. Он содержит интерфейсы / классы для классов типа «репозиторий» и «менеджер». Эти репозитории отвечают за получение / сохранение / обновление данных из / в БД, а менеджеры «знают» об отношениях объектов с другими компонентами / классами.

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

Есть 2 способа создания бизнес-сущностей:

  1. Когда данные извлекаются из репозитория БД, создаются экземпляры объектов и инициализируются соответствующими данными БД, среди прочего инициализируется поле «Контейнер».
  2. Когда объект создается в коде, который будет помещен в БД, среди прочего, он принимает параметр «IUnityContainer».

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

Постановка проблемы: на прошлой неделе я прочитал несколько вопросов / ответов по SO, в которых говорится примерно следующее:

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

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

Мне кажется разумным, но:

  • Мне потребуется, чтобы любой объект предоставлял ВСЕ интерфейсы, которые ему нужны (обычно каждому менеджеру / репозиторию / сущности требуется 3-5 из них);

  • Очень много случаев, когда требуется всего 1-2 интерфейса (поэтому я не хочу создавать много других);

Вопрос 1: действительно ли это хороший способ «скрыть» контейнер? Почему? (на самом деле, мне кажется, я знаю почему, но если у вас есть хороший ответ, сообщите, пожалуйста).

Вопрос 2. Как лучше всего решить эту проблему?

Вопрос 3: в моей конкретной реализации, когда мои объекты извлекаются из БД (я использую Linq2Sql и думаю о переключении на EF), я не могу создавать объекты с помощью DependencyContainer, я должен сделать это сам (потому что объекты создаются с использованием выражения, которое выполняется на сайте SQL, будет вызываться конструктор без параметров со следующими данными, назначенными общедоступным свойствам; и контейнер зависимостей недоступен на стороне SQL). Есть ли обходной путь для этого?

Технические детали моей реализации:

Пример конструктора базового класса Manager:

public abstract class ManagerBase : IManager
{
    protected ManagerBase(IUnityContainer container)
    {
        DependancyContainer = container;
    }

    protected readonly IUnityContainer DependancyContainer;

    ...
}

Пример базового класса репозитория:

public abstract class RepositoryBase<T, TDb> : IRepository<T>
    where T : IEntity
    where TDb : class, IDbEntity, new()
{
    protected abstract ITable<TDb> GetTable();

    public IQueryable<T> GetAll()
    {
        return GetTable().Select(GetConverter());
    }

Пример извлечения данных из БД: репозиторий создает экземпляры объектов и инициализирует их соответствующими данными БД, среди прочего инициализируется поле «Контейнер»:

public class CountryRepository
    : RepositoryBase<ICountry, DbData.Country>, ICountryRepository
{   
    protected override Expression<Func<DbData.Country, ICountry>> GetConverter()
    {
        return dbEntity => new Country
        {
            DependancyContainer = DependancyContainer,

            Code = dbEntity.CountryCode,
            Name = dbEntity.CountryName,
        };
    }

Когда требуется получить данные из БД, вызывается следующее:

public class CountryManager : ManagerBase
{
    ICountry GetCountryById(int countryId)
    {
        ICountryRepository repository = DependancyContainer.Resolve<ICountryRepository>();
        return repository.GetAll()
            .Where(country=>country.Id==countryId)
            .SingleOrDefault()
            ;
    }
}

person Budda    schedule 17.03.2011    source источник
comment
Я хочу ответить на ваш вопрос, но не совсем понятно, что вы имеете в виду под '* Есть много случаев, когда требуется только 1-2 интерфейса (поэтому я не хочу создавать много других); ... create много другого, что в частности?   -  person James Webster    schedule 18.03.2011
comment
Также я думаю, что ваш пример CountryManager должен расширять ManagerBase?   -  person James Webster    schedule 18.03.2011
comment
CountryManager унаследован от managerBase. Это правильно.   -  person Budda    schedule 19.03.2011
comment
'Во многих случаях требуется только 1-2 интерфейса (поэтому я не хочу создавать много других); - это означает, что некоторым объектам необходимо знать 5 интерфейсов ( например): допустим, что объекту «Клиент» требуется доступ к «CustomerContacts» и «CustomerOrders». В одних случаях объект клиента создается для работы с заказом (потребуется интерфейс ICustomerOrderManager), в других - для работы с контактами (требуется интерфейс ICustomerContactManager). если эти интерфейсы необходимо будет передать в конструктор, мне всегда нужно будет инициализировать их оба ...   -  person Budda    schedule 19.03.2011


Ответы (1)


Q1: Другие могут не согласиться, но, как правило, вам не следует предоставлять контейнер DI своим моделям / классам объектов предметной области. Я вижу для этого две основные причины:

  1. Передача контейнера классу сущности скрывает зависимости класса сущности. Если вы полагаетесь на внедрение конструктора, вы можете увидеть, каковы его зависимости, посмотрев на конструктор. Если вы полагаетесь на внедрение свойств, вы можете увидеть, каковы его зависимости, посмотрев на свойства, хотя это менее ясно, чем внедрение конструктора (т.е. некоторые свойства являются данными класса сущности, некоторые свойства являются интерфейсами для других сервисы, относящиеся к классу сущности; разделение этих двух категорий несет некоторую когнитивную нагрузку). Если вы предоставите контейнер сущности, то зависимости будут разбросаны по всему классу.
  2. Проще проверить. Если вы передадите свой контейнер классу сущности, то для его тестирования вам придется имитировать / заглушить контейнер, а также интерфейсы, которые он извлекает из контейнера. Вы всегда можете настроить легкий контейнер, но это все еще больше работы, чем просто предоставление имитированных / заглушенных интерфейсов напрямую.

Q2: некоторые скажут, что хорошая практика (в DDD, откуда исходит шаблон репозитория / шаблон сильной модели предметной области) в первую очередь не подвергает ваш класс сущности интерфейсам. Скорее они должны содержать любую бизнес-логику, которую можно реализовать, не полагаясь на другие интерфейсы, а более сложная логика должна быть в классе обслуживания (на языке DDD).

Q3: вы не говорите, какой инструмент объектно-реляционного сопоставления вы используете, но можно получить некоторый контроль над процессом создания экземпляра объекта сущности с помощью какого-то типа перехватчика / шаблона аспектно-ориентированного программирования. . Это, тем не менее, привязывает вашу кодовую базу к тому, чтобы полагаться на этот конкретный преобразователь O / R и его возможности (то есть сложнее отказаться от него в будущем, хотя я обычно стараюсь не беспокоиться об этом при выборе архитектуры).

Еще несколько наблюдений о вашем примере кода, которые могут или не могут быть показательными для остальной части вашей кодовой базы:

  • Вы смотрели AutoMapper? Вы можете найти эту лучшую альтернативу ручному отображению, которое вы делаете в классе CountryRepository.GetConverter().
  • Похоже, вы создаете параллельные классы сущностей для каждого класса в пространстве имен DbData, предположительно потому, что вы не можете вставить нужные службы в классы пространства имен DbData. Возможно, мой ответ на Q2 направит вас в лучшую сторону (полагайтесь на службы и расширяйте DbData классы, если это возможно).
  • Я не совсем уверен, какую роль играет Manager ... в идеале все ваши методы запросов должны быть в репозитории, чтобы они действительно могли выполнять SELECT ... FROM Country where Id = ? запрос к базе данных, а не возвращать всю таблицу и выполнять запрос в памяти через LINQ, в конце концов, это то, в чем базы данных хороши! (несмотря на преднамеренную оптимизацию кэширования для характеристик производительности вашего приложения). Я считаю, что GetCountryById метод должен быть на самом CountryRepository.

Надеюсь, это поможет!

person James Webster    schedule 17.03.2011
comment
Задача репозитория - обеспечить отображение. Наверное, вы правы, и мне лучше перенести функции типа GetCountryById из менеджеров в репозитории. Но цель моих менеджеров - вернуть ПОЛНЫЙ объект. И если я верну объект Order, он не будет согласованным без всех записей заказов ... поэтому OrderManager будет делать две вещи: получать данные из БД для самого заказа (из OrderRepository) и получать из записей заказов DB (из OrderRecordRespotitory ). OrderRepository выполняет только сопоставление объекта заказа. - person Budda; 19.03.2011
comment
В качестве преобразователя данных я использую Linq2Sql. Кстати, спасибо, ждем дополнительных идей, если мои комментарии были ценными - person Budda; 19.03.2011
comment
Возможно, вам стоит прочитать еще о дизайне, управляемом доменами, существует концепция, называемая ограниченными контекстами и агрегированными корнями. Не каждый класс в вашем домене должен иметь собственный репозиторий (IMHO), а скорее определять, какие совокупные корни (то, что вы называете FULL-объектом) находятся в вашем домене, и предоставлять репозитории только для них. Такие репозитории будут отвечать за сохранение и извлечение всех моделей, составляющих общий корень. - person Robbert Draaisma; 14.11.2015