Procedure Façade
В прошлый раз, когда речь шла об инфраструктурном слое, я упомянул, что в текущем своем виде он не подходит для удобного использования процедур, если таковые имеются. Однако многие проекты до сих пор могут вынужденно использовать процедуры как наследие предыдущих версий. Впрочем, с некоторым удивлением можно обнаружить и новые проекты, которые активно используют процедуры вплоть до CRUD реализаций.
В этом посте я хочу рассказать, как можно сделать использование процедур как можно более простым для вашего кода и лишить себя многой тупой работы и кода. Приведенный ниже пример будет основываться на Linq2Sql контексте, но общая идея, я уверен, подойдет и для EF. Для тех, кто не читал цикл о фасаде доступа к данным, рекомендую сначала прочитать его, так все примеры ниже будут основаны на описанном примере и внимание будет уделено только работе с процедурами.
Идея
Общая идея совсем не нова и основывается на принципе Convention over Configuration. Т.е. все работает «как бы магическим образом», основываясь только на специальных правилах именования. Когда-то давно это не казалось хорошей идеей, однако такая договоренность значительно упрощает жизнь и сейчас данный принцип используется во многих проектах и фреймворках. Взять хотя бы тот же ASP.NET MVC, который полностью построен на специальных названиях, папках и тому подобное, никто ведь не жалуется, даже скорее наоборот. Конечно, работа проектов на соглашениях кажется «магией», пока не начинаешь работать с ними и глубоко разбираться в механизмах лежащих глубоко внутри. Впрочем, для правильного и эффективного использования в любом случае придется овладеть «магией», которая превратится в простую ловкость рук.
Make me async
Почти месяц назад я написал статью «Эволюция сервисных методов». В ней я пришел к реализации библиотеки MakeMeAsync, которая позволяет декорировать вызовы методов, делая их асинхронными. При этом код остаётся максимально читабельным и чистым, на мой взгляд.
Вот уже месяц как библиотека используется в реальном боевом проекте и показывает себя с лучшей стороны. Сначала удалось сократить размер самих сервисов, так как большая часть кода была инфраструктурной, затем, когда код стал ясным, я легко увидел, что сервисы не в полной мере выполняют свои задачи, а скорее делегируют их на слой ниже. Это меня огорчило, так как реализация является неправильным подходом; но порадовало, так как я смог это увидеть, и мне было психологически легче перенести код в нужное место, так как сервис выглядел компактным и понятным.
Кроме появления более четкой организации логики кода, значительно облегчилась задача по отображению ошибок, которые могли появиться во время выполнения задач в асинхронном режиме. Мне не надо было для каждой задачи прописывать/указывать метод обработки ошибки, можно было задать такой обработчик один раз в начале работы программы и все.
Итак, я думаю, что для начала стоит обновить информацию о том, как используется библиотека MakeMeAsync.
Обработка исключений при работе с классом Task
Сложность по шкале Microsoft 200
В последних своих постах я активно использую класс Task для асинхронного выполнения задач. С учетом того, что основной вкусностью новой версии С# будет метод директива async, которая как раз и предназначена более всего для работы с классом Task, то будет уместно рассказать о том, как ловить и обрабатывать исключения, которые могут возникнуть при работе вашего метода в асинхронном режиме.
Далее будет рассмотрена работа с исключениями не только в теле метода, который был явным образом указан в качестве жертвы асинхронной работы, но и как получать исключения из вложенных асинхронных методов. Можете думать об этом как о сне во сне, и вспоминать фильм «Начало».
Все последующие примеры не открывают Америки и все прекрасно описано в книги J.Albahari “C# in the Nutshell 4.0”, но пока не увидишь, как не надо делать, сложно бывает запомнить как надо.
Эволюция сервисных методов
Сложность по шкале Microsoft 200-300
Давно хотел изложить свои мысли по поводу эволюции сервисных методов, но до последнего момента правильная, на мой взгляд, идея имела плохую реализацию – слишком трудоемкую и развесистую. На данный момент я пришел к некоторому сокращению кода и готов поделиться мыслями по поводу эволюции сервисных методов. Сразу скажу, что речь пойдет о доменных сервисах по обработке данных, так как чаще всего именно они занимаются обработкой данных в течение продолжительного времени. Однако описанные подходы наверно можно применять и к другим классам с «долгоиграющими» методами.
В данном случае под эволюцией подразумевается то, как инфраструктурно выглядит метод, а не его функциональная эволюция (рефакторинг). Описывать, как всегда, я собираюсь свой личный опыт, то, на чем я набил шишки.
Простейшие
Начиная со школы или университета, и в продолжение достаточно долгого времени мы пишем только однопоточные приложения. Все действия в них выполняются последовательно, синхронно. В этом нет ничего плохого в целом, многие методы и библиотеки работают синхронно и это хорошо. Не стоит городить сложность там, где она не нужна. Итак, простейший сервис может выглядеть следующим образом:
public class Service {
public void Method() {
// do some complicated actions
}
}
Не будем разводить дискуссий по поводу вырожденности примера, мол, большую часть времени есть аргументы и возвращаемое значение – суть от этого не поменяется. Сколько бы раз я не написал вызов этого метода, он будет выполнен последовательно, один за другим.
var service = new Service();
service.Method();
service.Method();
service.Method();
...
Со временем начинает расти недовольство тем, что интерфейс программы, если он есть, замирает в результате выполнения цепочки последовательных методов и в этот момент ни отменить операции, ни что-то сделать еще. С одной стороны есть «плюс», не надо делать уведомление о том, что операция закончилась, так как экран «отмерзнет», но лучше это делать более цивилизованными способами.
Зато, кстати, не надо было задумываться о том, что этот код может выполняться сразу в нескольких потоках, никаких проверок, что метод уже запущен и работает с данными. Второй раз с интерфейса все равно не запустишь его, если есть такая связь.
Фасад доступа к данным — Оптимизация, Заключение
Построение запросов
Уже традиционно покажу сразу UML схему, чтобы вы смогли ощутить какие классы будут рассмотрены и как они зависят друг от друга.
Хорошо уметь загружать связанные данные, хорошо уметь восстанавливать только нужные данные, но было бы еще замечательно уметь получать только нужные данные от базы данных, чтобы не выкачивать многие тысячи строк, которые нам может и не нужны вовсе. Для этих целей и существует в нашей схеме построители (или оптимизаторы) запросов.
Linq2Sql позволяет гибко работать с запросами в стиле fluent interface переводя их в sql запросы. Было бы упущением не воспользоваться этой возможностью в полной мере. Особенно учитывая то, что мы уже работаем с логическими таблицами L2S, которые реализуют интерфейс IQueryable. Да, когда мы в адаптере чтения обращаемся к контексту с методом GetTable<TData>(), мы получаем объект, который можно донастраивать с помощью методов Where(), разбирая пользовательские спецификации. Собственно в этом и состоит идея.
Следуя только что сказанному, интерфейс для построения запросов может выглядеть следующим образом:
public interface IQueryBuilder<TDomain, TData> {
IQueryable<TData> Optimize(IQueryable<TData> query, Specification<TDomain> specification);
}
Модифицировали запрос, если надо, и отдали его обратно. Отлично, теперь рассмотрим как это может быть реализовано.
Фасад доступа к данным — Адаптер записи, Трансляторы
Адаптеры записи
Как и в случае с адаптерами для чтения, большую часть работы можно вынести в абстрактный класс. Интерфейс IWriteAdapter необходим опять же в целях обнаружения классов.
Если вы взглянете на метод InfrastructureFacade.Commit() то увидите, что получение всех адаптеров для записи получается из StructureMap скопом по одному только интерфейсу, что весьма удобно, так как они нужны всегда все разом для определения очередности работы.
IWriteAdapter
В интерфейс вынесены метод и свойство необходимые для работы в классе фасада, а именно Save() и Sequence.
public interface IWriteAdapter {
void Save(UnitOfWork unitOfWork);
int Sequence { get; }
}
Теперь можно переходить к рассмотрению «мяса», основной работы по сохранению данных.
Фасад доступа к данным — Фасад и Адаптер чтения
Фасад
Теория
Кажется, все приготовления прошли, необходимые концепции упомянул и теперь можно перейти непосредственно к реализации фасада. Интерфейс фасада состоит всего из двух методов, которых нам хватит для всех целей приложения.
namespace Domain {
public interface IInfrastructureFacade {
IUnitOfWork UnitOfWork { get; }
List<T> Get<T>(Specification<T> specification);
void Commit(IUnitOfWork unitOfWork);
}
}
Как вы можете заметить, интерфейс фасада содержится в модуле домена, тогда как реализация будет в модуле инфраструктуры – применяем инверсию зависимостей, так как домен не должен зависеть от других частей системы.
Про Unit of Work (UoW) рассказано было ранее, паттерн Specification так же уже рассматривался, так что на этих типах данных останавливаться не будем.
Общий принцип работы такой:
- Получаем фасад;
- Вызываем метод получения доменных данных, в который передаем ограничивающие условия, которым должны соответствовать элементы;
- Что-то делаем с данными, работаем с UoW;
- Вызываем метод сохранения UoW.
Фасад доступа к данным — Подготовка
На основе LINQ to SQL
Сложность по шкале Microsoft 300-400
Сразу признаюсь, что я люблю Linq2Sql и не люблю Entity Framework. Люблю компактные API и не люблю развесистые классы с кучей методов служебных выставленных наружу. Может по этим причинам, а может быть по каким-то еще, но я использую схему доступа к данным, которую собираюсь описать ниже, уже несколько лет. По большей части меня она радует и выполняет свои задачи хорошо для большинства заданий возникающих на работе.
Вводная часть
Как уже ясно из названия, речь пойдет о паттерне Фасад. Вот что нам говорит википедия по поводу этого шаблона:
Шаблон Facade (Фасад) — Шаблон проектирования, позволяющий скрыть сложность системы путем сведения всех возможных внешних вызовов к одному объекту, делегирующему их соответствующим объектам системы.
Однако сам по себе фасад достаточно тривиален и ограничиться только описанием применимости паттерна в случае баз данных было бы не очень интересно и информативно. Я планирую рассмотреть всю цепочку классов и данных, которые участвуют в получении данных, от записей в базы данных до доменных классов. Описать пути преобразования данных, оптимизацию запросов, сложные моменты в реализации, ограничения технологии, случаи, когда данный подход не оправдает себя.
Итогом работы будет являться использование фасада в таком духе:
var facade = new InfrastructureFacade(); var customers = facade.Get(Specification<Customer>.Null);
Т.е. надо будет только указать фасаду какой тип доменных данных вы хотите получить и с какими ограничениями. В данном случае ограничений на данные нет, т.е. в результате выполнения запроса получим всех заказчиков из базы.
Причем это будет практически единственный доступный способ получить данные из базы – простой для чтения и использования. Все служебные классы будут спрятаны от пользователей, в данном случае от других модулей.
Автообнаружение и регистрация классов в StructureMap
Уровень подготовки по шкале MS: 400
Некоторое время назад я рассказывал про чудесный IoC\DI контейнер StructureMap. На тот момент я описал основные приемы работы, скорее как справочник, показавший возможности, без углубления в конкретные темы. На этот раз я хочу рассказать о возможности автоматического обнаружения и регистрации классов средствами StructureMap.
Уже по самому названию понятно, что это хорошо и несет благодать, так как обещает сократить трудовые расходы и написание тривиального кода, что всегда скучно и всегда забываешь его дописать где-то. Авторегистрация решит все эти проблемы, хотя, конечно, сначала придется попыхтеть.
Дальнейшие примеры покажут, как работать с обнаружением только по интерфейсу\базовому классу, а так же как работать при именовании классов по соглашениям (Convention over Configuration).
Вводная
Сначала достаточно простой пример. Пусть у нас есть множество классов для трансляции данных из базы, все они наследованы от базового класса. Т.е. что-то в духе:
public class ClientAdapter : ReadOnlyAdapter<Client, vClient> {
...
}
public class InvoiceAdapter : WriteAdapter<Invoice, vInvoice> {
...
}
И таких классов достаточно много, всех их регистрировать вручную не очень хочется, т.к. это выглядит ужасно в коде и писать долго. Регистрация таких адаптеров может занять экран, а то и два. Гораздо лучше поднапрячь извилины и записать это в несколько строк.
Итак, StructureMap предоставляет метод Scan, который пробегает по интересующим нас сборкам или папкам и регистрирует подходящие объекты. Для того чтобы метод Scan нашел и зарегистрировал типы необходимо соблюдение нескольких условий:
- Тип должен быть явным, дженерик типы не регистрируются автоматически;
- Тип должен иметь публичный конструктор;
- Конструктор не может иметь аргументов примитивных типов;
- Множественное регистрирование не допускается.
Указание сборки для сканирования можно задать несколькими способами:
- Явно прописать имя сборки или же передать ее саму;
- Обратиться к вызывающей сборке;
- Найти сборку содержащую определенный тип;
- Найти сборки по определенному пути.
Целые vs GUID vs Естественные и Суррогатные ключи
В мире баз данных есть несколько аргументов, которые провоцируют очень сложный выбор правильного первичного ключа из вариантов представленных в заголовке. Что будет лучше, выбрать естественный или суррогатный ключ? Если мы выбираем суррогатный ключ то, что выбрать: целочисленный ряд или же GUID? В прошлом мне пришлось с жаром отстаивать все точки зрения, так что могу рассмотреть плюсы и минусы каждого подхода.
Естественный vs Суррогатный ключи
Джо Целко (Joe Celko) утверждает что мы должны использовать естественные ключи, созданные из атрибутов записи, а не заниматься присвоением произвольных значений записям в таблицах. Я согласен с ним. Но! В то же время мы должны стараться хранить ключи легкими/маленькими, вместо того, чтобы создавать массивные составные ключи, чьи значения в свою очередь должны будут храниться в других таблицах для возможности соединения таблиц, поддержания их целостности. Как я выбираю какой способ использовать? Легко, я использую оба.
Сейчас я поясню. В базе данных первичный ключ является естественным, однако я создаю суррогатный ключ, который может быть использован для соединения таблиц. В этом случае получаем хорошую производительность как для объединения таблиц, так и для выборок. Однако я усиливаю естественный ключ суррогатным для того, чтобы два значения не вставились одновременно.


