Заменяем QueryFactory на бестелесный IQueryFactory

7 мая 2012 г.

В статье Проблемный шаблон Repository и судя по комментариям многим не понравилась та часть, где объекты *Query скрываются за IQueryFactory. С первого взгляда кажется, что QueryFactory превращается в очередной god-object.

Я уже отвечал, что это не так, потому что в самой QueryFactory нет логики и она только создает объекты запросы. Теперь я могу дать ссылки на две статьи, где от QueryFactory остался только интерфейс, который реализуется с помощью IoC.

Первая статья моего коллеги Тимура Рахматиллаева совсем недавно выложена на Хабре Получение экземпляра класса запроса по сигнатуре его интерфейса.

Вторая статья моего бывшего коллеги Дмитрия Крючкова была выложена в его блоге Alternative to repository pattern: query objects using Castle Windsor с примеры кода на GitHub'е

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

25 комментариев:

  1. Преимущества всегда очевидны. Дьявол прячется в недостатках.

    Александр, можно вопрос именно к тебе, как к пустившему волну? Ты же придерживаешься принципа SRP при проектировании? Можешь сформулировать responsibility класса LoginCriterion в примере Тимура?

    ОтветитьУдалить
  2. А мне вообще непонятен ажиотаж вокруг этого ServiceLocator-подобного IQueryFactory. Насколько я понимаю, вещь сугубо опциональная. Предпочитаешь явно объявлять зависимости - прокидывай Query напрямую, не любишь прокидывать много зависимостей - прокидывай IQueryFactory, хотя это 1. практически прокидывание IoC контейнера 2. сделает стаббинг/мокинг зависимостей менее интуитивным

    В любом случае, IQueryFactory никак не влияет на достоинства/недостатки Query - он является надстройкой (пусть, на мой взгляд, и сомнительной) над ним.

    ОтветитьУдалить
  3. @Pasha

    Это можно сказать контекст запроса, а фактически DTO для передачи критериев запроса.

    ОтветитьУдалить
  4. @Idsa

    Еще есть один нюанс - уменьшается количество кода. Опять же на любителя.

    ОтветитьУдалить
  5. Александр, ты о каком коде? О тех трех строчках на каждую зависимость:

    privat readonly IQuery1 _query1;

    public SomeType(IQuery1 query1)
    {
    _query1 = query1;
    }

    Да, неприятный момент. Меня напрягает, когда прокидывание зависимостей - портянка на полэкрана. Кажется, Мартин писал, что если больше 3-4 зависимостей, то это уже говнокод, и его нужно рефакторить. Но рефактиронг в сторону IQueryFactory - еще больший говнокод (пусть и удобный). Хм...

    ОтветитьУдалить
  6. @Idsa

    Вот это откуда:

    > О тех трех строчках на каждую зависимость:
    privat readonly IQuery1 _query1;

    public SomeType(IQuery1 query1)
    {
    _query1 = query1;
    }

    ОтветитьУдалить
  7. @Александр

    Это просто пример пробрасывания непосредственно Query, а не QueryFactory

    ОтветитьУдалить
  8. @Idsa

    Так не получится, потому что IQuery* будет очень много.

    ОтветитьУдалить
  9. @Александр

    Их будет столько же, сколько и методов в IQueryFactory.

    ОтветитьУдалить
  10. @Idsa

    В ссылках, которые я привел у IQueryFactory есть только один метод не зависимо от кол-ва объектов Query*

    ОтветитьУдалить
  11. Ну ок. Если отталкиваться от статей, то будет прокидываться не IQuery1, а IQuery<SomeCriterion, SomeResult>.

    ОтветитьУдалить
  12. @Idsa

    Я не понимаю куда это будет прокидываться :)

    Контроллеры, например, будут принимать только IQueryFactory, как и все остальные объекты в системе.

    ОтветитьУдалить
  13. @Александр

    Так я рассматриваю ситуацию, когда QueryFactory нет

    ОтветитьУдалить
  14. @Idsa

    А, когда ее нет слишком много инжектировать придется. Для однотипных объектов всё-таки надо использовать фабрику.

    ОтветитьУдалить
  15. @Александр

    В общем случае ты, наверное, прав. Но так ли много Query будет в одном сервисе/контроллере? 2, 3, 5? Насколько для нас важно явно видеть, от каких запросов зависит сервис (тестировать такие классы все же удобнее)? От ответов на эти вопросы, думаю, зависело бы мое решение, если бы я вдруг собрался переползти с IQueryable+Specifications на Queries.

    ОтветитьУдалить
  16. @Idsa

    Допустим в начала их будет по 3-4 в каждом контроллере, а потом по 10-12. Будешь рефакторить?

    Очевидно же, что кол-во запросов увеличивается с ростом проекта.

    ОтветитьУдалить
  17. >>Будешь рефакторить?

    Скорее всего, да

    >>Очевидно же, что кол-во запросов увеличивается с ростом проекта.

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

    ОтветитьУдалить
  18. @Idsa

    Я за фабрику с самого начала :)

    ОтветитьУдалить
  19. @Александр

    Пожалуй, ты меня убедил :)

    ОтветитьУдалить
  20.  Уважаемый Александр и коллеги!
    В ответ на публикацию "Проблемный шаблон Repository" хочу дать свой ответ
    с противоположным названием "Безпроблемный шаблон Repository".
    Приведенный ниже пример слоя Persistance / Repository лишь частичной мною
    подкорректирован. Все основные идеи взяты из статьи
    http://weblogs.asp.net/shijuvarghese/archive/2011/01/06/developing-web-apps-using-asp-net-mvc-3-razor-and-ef-code-first-part-1.aspx
    Но само решение настолько простое, логичное и универсальное, что не могу его не привести
    после чтения статьи Александра.

    Слой Persistance состоит из 2х подслоёв - фабрики баз данных и репозитория данных

    // базовый интерфейс для подслоя фабрики баз данных
    public interface IDatabaseFactory
    {
        DataContext Get();
    }

    // интерфейс для работы с базой данных Rpr
    public interface IRprDatabaseFactory : IDatabaseFactory
    {
    }

    ///
    /// Класс который фактически является обёрткой над DataContext нужной нам бд Rpr
    ///
    public class RprDatabaseFactory : IRprDatabaseFactory, IDisposable
    {
        /// Контекст конкретной бд
        private RprDataContext context;
        /// Получение контекста бд
        public DataContext Get()
        {
            if (context == null)
            {
                context = new RprDataContext();
            }
            return context;
        }
        public void Dispose()
        {
            if (context != null)
                context.Dispose();
        }
    }

    // базовый интерфейс для подслоя репозитория
    public interface IRepository where T : class
    {
        void Add(T entity);
        void Delete(T entity);
        T Get(Expression> where);
        IEnumerable GetAll();
        IEnumerable GetMany(Expression> where);
    }

    // базовый класс для подслоя репозитория
    public class BaseRepository : IRepository where TEntity : class
    {
        /// Контекст бд
        protected DataContext context;

        /// таблицы с сущностями бд
        protected Table table;

        public BaseRepository(IDatabaseFactory dbFactory)
        {
            context = dbFactory.Get();
            table = context.GetTable();
        }

        public virtual void Add(TEntity entity)
        {
            if (entity != null)
            {
                table.InsertOnSubmit(entity);
            }
        }

        public virtual void Delete(TEntity entity)
        {
            if (entity != null)
            {
                table.DeleteOnSubmit(entity);
            }
        }

        public virtual TEntity Get(Expression> where)
        {
            return table.Where(where).FirstOrDefault();
        }

        public virtual IEnumerable GetMany(Expression> where)
        {
            return table.Where(where);
        }

        public virtual IEnumerable GetAll()
        {
            return table;
        }

    }
    // репозиторий объектов бд Rpr
    public class RprRepository : BaseRepository where TEntity : class
    {
        ///
        /// При помощи механизма фреймворка IoC получаем экземпляр этого класса
        /// (а в него вложен экземпляр DataContext нужной нам бд) с необходимым временем жизни
        /// Например со временем жизна Http запроса
        ///
        public RprRepository() : base(UnityInstance.Container.Resolve())
        {
        }
        public RprDataContext Context
        {
            get
            {
                var rprContext = (RprDataContext) context;
                return rprContext;
            }
        }
    }
    // создаём репозиторий для конкретного типа сущности модели домена
    public class ProjectRepository : RprRepository
    {
    }
    // использование методов репозитория в слое бизнес-логики
    var repository = new ProjectRepository();
    var list = repository.GetMany(item => item.parent_project_id == ProjectId).ToList();

    ОтветитьУдалить
  21. Алексей, вы пропустили всё, о чем говорилось в 
    http://blog.byndyu.ru/2011/08/repository.html и комментариях к этой статье.

    Прежде, чем вы перечитаете статью и комментарии могу сразу сказать пару вещей.

    В вашем решение IDatabaseFactory возвращает DataContext, а это значит, что работа с данными будет раз и навсегда привязана к EF. Что вы будете делать, если надо будет менять ORM или часть объектов хранить не в БД?

    Если надо сделать сложную выборку, которая будет не просто Where, а еще сортировку, join т.п., в какой метод репозитория вы это передадите?

    Если вам нужно сделать подгрузку связных сущностей (Fetch), где вы будете это указывать?

    Описанный вами подход можно назвать Generic Repository со всеми его известными проблемами. Он подходит, если у вас маленькие проекты, которые никогда не надо поддерживать, либо если у вас большие проекты, которые можно делать бесконечно долго. Это не гибкое решение, которое ведет к техническим долгам.

    ОтветитьУдалить
  22. Почему то сбивается разметка в приведенном выше коде - должно быть так

    IEnumerable GetMany(Expression> where);

    ОтветитьУдалить
  23. Всё равно сбивается

    ОтветитьУдалить