Слой доступа к данным в .net приложениях

При разработке приложений, работающих с базой данных, неизменно встает вопрос проектирования слоя доступа к этим самым данным. Использование «чистого» ADO.Net в рамках больших и сложных приложений рассматривается всё реже, и использование ORM становится уже стандартом де-факто.

Однако и после выбора ORM вопросы проектирования не заканчиваются — встает проблема собственно организации слоя доступа к данным. Здесь есть несколько ставших уже классическими подходов:

  1. Репозиторий. Стандартный шаблон, где на каждую сущность БД (точнее, на каждый корень агрегации) создается репозиторий, инкапсулирующий запросы к БД и возвращающий что-то вроде IEnumerable<Entity>. Выглядит это обычно так:
    public IEnumerable<Account> GetActiveAccounts();
    public IEnumerable<Account> GetSuspendedAccounts();
    public IEnumerable<Account> GetAccountsByFirstName(string firstName);
    

    Основная проблема в таком подходе — разрастание репозитория (подобных функций становится много) и нарушение принципа Interface Segregation (ради использования одной функции репозитория обычно запрашивается репозиторий целиком).

  2. CQS-подход. Применительно к запросам, этот подход предлагает выделение функций репозитория (подобных вышеприведенным) в отдельные классы-Query, ответственность которых — в выборке данных, необходимых для какой-либо вьюшки. Преимущество подхода в соблюдении SRP принципа, легкости наследования и удобства наложения сквозной логики вроде кэширования или авторизации.
    Проблема, на мой взгляд, состоит в некотором over-engineering’e, если данный подход используется как единственный во всём приложении. Для простых запросов вроде Session.Customers.Where(customer => customer.Email.Contains(«gmail.com»)) создание отдельных классов выглядит избыточным.
  3. IQueryable-репозиторий с использованием спецификаций. Совмещение первого и второго подхода: присутствуют репозитории на каждую сущность, с набором функций типа:

    public IQueryable<Account> Find(ISpecification<Account> spec);
    public Account Single(ISpecification<Account> spec);
    public IQueryable<Account> FindAll();
    ...
    

    Репозитории остаются лёгкими, возможные дубликации кода устраняются использованием спецификаций, view-специфичная логика(вроде eager-loading, сортировки и пагинации) остается ближе к вьюшке и не захламляет ни Query-классы, ни репозитории. Собственно, подход выглядит оптимальным, но не лишен некоторых неудобств (например, если класс активно работает с несколькими типами БД-сущностей, то для каждого типа нужен отдельный специфичный репозиторий).

  4. UnitOfWork как слой доступа к данным. Подход представляет собой нечто среднее между IQueryable-репозиторием и прямым доступом к Session из приложения (к слову, за такой тип доступа ратует Ayende, аргументируя замечательной фразой: Getting data from the database is a common operation, and should be treated as such). Над Сессией (ObjectContext в случае EF) создается враппер, включающий в себя функции IQueryable-репозитория и стандартные для UnitOfWork Commit\Dispose операции.
    Плюсы подхода — в применении спецификаций (дающих возможность повторно использовать запросы), абстрагировании от ORM (запросы, выполненные через спецификации, легко можно перевести на другую ORM, хоть это и не самоцель) и сохранении простоты кода (минимум слоев абстракции).
    К слову, в качестве спецификаций я использую Linq Query Specifications и конструкцию LinqSpec.For<Entity>(), которая позволяет создавать спецификации в одну строчку, вместо создания отдельного класса для каждой спецификации.

Как показывает практика, последний подход гладко ложится на 90% случаев. Остальные 10 — это сложные запросы, инкапсуляция которых в спецификацию сделала бы спецификации слишком громоздкими (например, запросы со сложными ограничениями на join-таблицы). Для таких случаев оправданным выглядит применение CQS-подхода, который, к тому же, упростит тестирование данной сложной выборки.

Опубликовать в Facebook
Опубликовать в Google Plus

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *