Фасад доступа к данным — Подготовка

На основе LINQ to SQL

Сложность по шкале Microsoft 300-400

Сразу признаюсь, что я люблю Linq2Sql и не люблю Entity Framework. Люблю компактные API и не люблю развесистые классы с кучей методов служебных выставленных наружу. Может по этим причинам, а может быть по каким-то еще, но я использую схему доступа к данным, которую собираюсь описать ниже, уже несколько лет. По большей части меня она радует и выполняет свои задачи хорошо для большинства заданий возникающих на работе.

Вводная часть

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

Шаблон Facade (Фасад) — Шаблон проектирования, позволяющий скрыть сложность системы путем сведения всех возможных внешних вызовов к одному объекту, делегирующему их соответствующим объектам системы.

Однако сам по себе фасад достаточно тривиален и ограничиться только описанием применимости паттерна в случае баз данных было бы не очень интересно и информативно. Я планирую рассмотреть всю цепочку классов и данных, которые участвуют в получении данных, от записей в базы данных до доменных классов. Описать пути преобразования данных, оптимизацию запросов, сложные моменты в реализации, ограничения технологии, случаи, когда данный подход не оправдает себя.

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

Т.е. надо будет только указать фасаду какой тип доменных данных вы хотите получить и с какими ограничениями. В данном случае ограничений на данные нет, т.е. в результате выполнения запроса получим всех заказчиков из базы.

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

До начала работы

Думаю, что надо перечислить все те компоненты и технологии, что будут участвовать в работе тестового приложения. Проект написан в Visual Studio 2010, .Net 4.0, так же желательно наличие NuGet, хотя это не обязательный пункт по идее все недостающие компоненты должны загрузиться в процессе первой сборки проекта. Скрипты написаны изначально для SQL Server 2008, но я уверен, что они заработают и на младших версиях SQL Server вплоть до SQL Server 2000. По поводу базовых компонентов кажется все.

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

С идеологической точки зрения, программа построена по DDD (опять же см посты), так что есть доменная часть, для работы с пользовательскими сценариями используется паттерн UnitOfWork, который кратко будет рассмотрен ниже. Общение с базой данных идет только через Linq 2 Sql, при этом без использования хранимых процедур (о хранимых процедурах будет упомянуто отдельно), т.е. вся основная логика приложения должна находиться в C# коде.

Тестовое приложение можно забрать с Assembla из SVN репозитария.

Домен

Структура проекта

Пару слов про домен, который получился в тестовом проекте очень маленький и простой. В данном случае на него внимание не уделялось, и он принял утилитарный вид. Класс Customer полностью повторяет таблицу из базы данных. Класс Ware в некотором роде объединил InvoiceContent и Ware. Класс Invoice будет повторять таблицу. В целом никакой магии нет, кроме того, что Ware может создаваться через копирование с указанием количества заказанного товара.

В дальнейшем рассмотрении так же будет участвовать спецификация на получение активных заказчиков.

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

База данных

База данных у нас будет состоять из Заказчиков, Заказов, Товаров и Содержимого заказа (Customer, Invoice, Ware, InvoiceContent – соответственно).

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

Остановимся лишь для чуть более подробного рассмотрения таблиц InvoiceContent и Ware. Свойствами товара является только название и цена в данный момент времени, по которому он будет продаваться или покупаться. Артикулы товара опущены, для нас хватит того, что все первичные ключи являются GUID полями. Состав заказа должен содержать ссылку на товар, но при этом содержать количество заказанных товаров и цену по которой совершена сделка, для исторического анализа.

Так как это тестовый пример, то здесь нет некоторых необходимых полей, как например, удалена ли запись или нет.  Как правило, в больших системах не стоит удалять данные, их нужно помечать как удаленные. Это позволяет избежать многих проблем с фрагментированием базы, анализом данных, удаление связанных данных само по себе может быть большой проблемой. Но сейчас это не тема для разговора.

Скрипт для создания базы DatabaseScript.sql находится в проекте Infrastructure\Scripts. Останавливаться на нем подробно, я думаю, не имеет смысла, там все просто до безобразия.

После того как вы создали базу данных из скрипта, стоит обратить внимание на концепцию UnitOfWork.

Unit of Work

Теоретическая часть. Интерфейс.

Посмотрим, что нам скажет по поводу этого паттерна Мартин Фаулер, так как википедия молчит:

Unit of Work (единица работы) – Обслуживает набор объектов, изменяемых в бизнес-транзакции (бизнес-действии) и управляет записью изменений и разрешением проблем конкуренции данных.

Когда необходимо писать и читать из БД, важно следить за тем, что вы изменили и если не изменили — не записывать данные в БД. Также необходимо вставлять данные о новых объектах и удалять данные о старых.

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

Реализация паттерна Unit of Work (здесь и далее UoW) следит за всеми действиями приложения, которые могут изменить БД в рамках одного бизнес-действия. Когда бизнес-действие завершается, UoW выявляет все изменения и вносит их в БД.

К слову, сам Linq2Sql внутри себя реализует UoW и работает с ним, чтобы отслеживать изменения пользователей.

Классическая реализация выглядят следующим образом:

В целом она не сильно отличается от того, что предложу я или MSDN.

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

Интерфейс UoW находится в сборке Domain, тогда как реализация в Infrastructure – реализуем принцип инверсии зависимостей. К тому же нам в работе инфраструктурного фасада понадобятся некоторые специфичные методы, которые лучше всего объявить с модификатором Internal.

Немного об ответственности методов UoW.

  • Save – сохраняет новые объекты, созданные пользователем, а так же отвечает за то, чтобы пометить существующие объекты как обновленные. Так же в этом методе можно вернуть к жизни удаленные объекты, об это стоит помнить при реализации.
  • Delete – помечает объект на удаление. При этом, если объект новый, то его можно просто удалить из списков «живых» объектов, т.е. никаких операций с базой в дальнейшем произведено не будет.
  • Commit – применение всех изменений к базе.
  • Rollback – откат всех изменений в UoW, т.е. фактически очищаются списки новых, удаленных, обновленных элементов.
  • Clear – полное очищение UoW, в целом достаточно редкая операция, может быть полезна при полной перезагрузки данных.

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

Реализация Unit of Work

Реализация на самом деле может быть сделана многими способами. Даже сам интерфейс может быть дополнительно оптимизирован в зависимости от того, что у вас содержится в базе. Если у вас первичные ключи исключительно суррогатные и целочисленные, то можно ввести дополнительные параметры для регистрации, поиску и обновлению по этому ключу. Но помятуя размышления на тему суррогатных целочисленных ключей, лучше рассмотреть более общий случай.

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

Начнем наверно с базового метода, который будет использоваться в дальнейшем – с регистрации объектов в UoW, тех, что приходят из базы данных. Для этих целей удобно будет использовать два метода: для регистрации одиночных объектов и для регистрации массивов, что ускорит использование UoW.

Тут есть нюанс, что надо четко понимать какой метод будет вызван в каждой ситуации, так как IList<T>, Enumerabler<T> и прочее это все же объект, а не коллекция List<T>.

В целом код достаточно примитивный, кроме того, что используется метод CheckTypeExistence(), который инициализирует коллекции для пришедшего типа, если он не зарегистрирован.

Метод в реализации тоже прост, как лом в разрезе и в дополнительных пояснениях не нуждается. Главное чтобы у вас складывалась цельная картина происходящего, что ничего экстраординарного в этом методе не происходит не только на мой, но и на ваш взгляд.  Однако метод сам по себе важен для последующей работы: лучше заранее инициализировать все нужные коллекции в словарях, чтобы потом не терять времени. Хотя данный метод будет вызываться при любых изменениях данных, но лучше перебдеть, чем недобдеть и потом искать обходные пути с хаками.

Как вариант можно предоставить метод RegistryType() для того, чтобы разработчики заранее инициализировали все типы данных. Но это может вылиться в то, что будет создан класс, где будут регистрироваться все доменные классы вне зависимости от необходимости. Плюс ко всему, и что самое важное (!!!) это добавляет энтропии в класс и заставляет задумываться разработчика (используйте «принцип наименьшего удивления» и «бритву Оккама»):

  • надо ли заранее регистрировать тип?
  • обязательное ли это действие?
  • есть автоматическое регистрирование?
  • в чем профиты использования?

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

Сохранение и удаление элементов в фасаде:

Как я уже упоминал при описании методов в рассмотрении интерфейса, при сохранении надо поместить элемент в сопутствующую библиотеку и «воскресить» элемент, если тот был удален. С удалением тоже фокусов нет.

В дальнейшем для работы нам еще понадобятся методы, которые не вошли в интерфейс: получение списка «живых» элементов, а так же методы для совершения общих действий над коллекциями.

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

Для коллекций, нам понадобятся методы:

  • ForEachInserted<T>(Action<T> action)
  • ForEachUpdated<T>(Action<T> action)
  • ForEachDeleted<T>(Action<T> action)

 

Общая реализация выглядит примерно следующим образом для каждого метода:

Можно проводить многие оптимизации и создавать специфические вещи в зависимости от ваших задач и типов данных, здесь приведена наверно одна из простейших реализаций на базе .Net 4.

О применении

Пару слов о применении данного шаблона разработки. Первое что приходит в голову, и в связи с чем его используют, это следующая ситуация: представьте что у вас есть приложение с несколькими экранами, каждый делает определенную работу, на каждый есть свой UseCase – определенный способ использования. Так вот перед началом работы с каждым сценарием работы вы создаете или запрашиваете от фасада UoW с необходимыми данными, и все изменения до финального сохранения записываются в UoW. При такой модели меньше действий происходит с БД и меньше вероятности запороть базу.  Если в процессе работы вышла ошибка, то обрывочные или не целостные данные в базу не уйдут (если конечно ошибка была не в процессе записи в базу).

UoW может использоваться для передачи данных между сценариями работы (экранами, UseCases), что может облегчить общее взаимодействие систем приложения. Более подробно можно почитать в книге «Шаблоны корпоративных приложений» Мартина Фаулера.

Инфраструктурный фасад, который мы начнем рассматривать чуть позже, в данном конкретном случае опирается на UoW, для минимизации обращений к базе и отслеживании состояния объектов.

2 комментарий на “Фасад доступа к данным — Подготовка

    • Вообще не делать так. Т.е. можно много чего допиливать под ситуации наиболее частые. За всю практику мою, я такие списки не видел и не составлял.

Оставить комментарий