Одна из труднейших задач при управлении и разработке исходного кода сложного корпоративного решения — обеспечить, чтобы кодовая база оставалась целостной, интуитивно понятной, максимально тестируемой и в то же время поддающейся сопровождению и расширению несколькими группами в рамках большого отдела разработки ПО. Основная проблема в том, что разработчики обычно следуют набору правил и руководств, а также используют некий набор библиотек, что со временем приводит к укрупнению и усложнению их решений. Это вызвано тем, что они стремятся к созданию идеальной и интуитивно понятной реализации бизнес-логики, но подгоняют ее под правила и фиксированные API. В принципе, это влечет за собой больше работы, больше ошибок, меньше стандартизации, меньше повторного использования и падение общей эффективности труда и качества кода.
Я старший разработчик в ведущем онлайновом инвестиционном банке и сам наблюдал, как эти проблемы могут ограничивать эффективность работы. Эта статья является практическим примером, который отражает то, как наша группа разработки проанализировала и преодолела эти трудные проблемы за счет инновационного применения в период выполнения генерации кода и встраивания зависимостей (Dependency Injection, DI). Возможно, вы не согласитесь с некоторыми из проектировочных решений, выбранных нашей группой, но я уверен, что вы не станете спорить с тем, что они представляют новый и эффективный способ устранения ряда распространенных проблем в архитектуре ПО.
В моей компании имеется большой отдел внутренних разработок ПО (работающий на двух континентах), который постоянно поддерживает и расширяет нашу огромную кодовую базу для Microsoft .NET Framework. Эта кодовая база сконцентрирована на ряде критически важных для бизнеса Windows-служб, которые образуют высокопроизводительную и имеющую низкую латентность торговую систему, размещенную в наших информационных центрах. У нас есть несколько групп, отвечающих за платформу (кодовую базу и исполняющую среду), плюс множество групп, занимающихся проектами, которые непрерывно (и параллельно) совершенствуют и расширяют эту систему.
Я работал в группах платформы в течение нескольких лет и на себе прочувствовал недостатки несогласованной и чрезмерно сложной кодовой базы при многочисленных ревизиях платформы и оказании технической поддержки. Два года назад мы решили заняться этим вопросом, и я нашел следующие проблемы.
- У нас было слишком много решений одних и тех же фундаментальных проблем. Хороший пример тому — большинство наших Windows-служб собственным уникальным способом комбинировали различные API в простой сервис с должной поддержкой протоколирования, трассировки, доступа к базе данных и т. д.
- Наши реализации бизнес-логики были либо простыми (но не поддающимися модульному тестированию, слишком наивными и не соответствующими руководствам), либо чрезмерно сложными из-за большого объема инфраструктурного кода. Распространенный пример: простой код, который работал напрямую с .NET SQL Server API, и сложный код, содержащий гораздо больше строк тривиального инфраструктурного кода для поддержки автоматических повторных попыток выполнения операций, кеширования и прочего, чем в самой бизнес-логике.
- У нас были вспомогательные библиотеки, поддерживающие большинство наших архитектурных принципов и правил кодирования, но они были реализованы в нескольких разных стилях и развивались независимо. Поэтому даже при их использовании так, как это диктовали правила, каждое решение получалось в итоге слишком объемным из-за большого количества сборок, на которое оно ссылалось, и весьма уязвимым перед изменениями в API. Это в свою очередь сильно усложняло введение новой функции в производственную среду, а также затрудняло обновление вспомогательных библиотек.
- Общий набор применяемых правил и руководств и используемых вспомогательных библиотек был настолько велик, что лишь самые опытные из наших разработчиков могли в полной мере разобраться в них; барьер вхождения для новых разработчиков был крайне высок. Это означало, что была написана уйма нестандартного кода, который позднее либо отбрасывался, либо попадал в производственную среду и лишь увеличивал степень несогласованности.
- В нескольких из наших базовых Windows-служб были централизованные «точки регистрации», где все группы, отвечающие за проекты, должны были модифицировать один и тот же код, например очень большое выражение switch, распределяющее команды или задания. Из-за этого слияние такого кода с основным превращалось в нетривиальную задачу.
Естественно, эти проблемы не были для нас чем-то новым или уникальным, и ряд общеизвестных проектировочных шаблонов описывает, как устранять подобные проблемы.
- Шаблон фасада скрывает все детали обращения к сложному ресурсу за простым интерфейсом уровня доступа. Это способствует созданию четких и тестируемых реализаций бизнес-логики, где внешние ресурсы при тестировании можно легко имитировать.
- DI, или контейнеры Inversion of Control (IoC), обеспечивают слабую связанность компонентов и тем самым облегчают их расширение, сопровождение и комбинирование. Эта методика также упрощает имитацию выбранных компонентов и тем самым увеличивает тестируемость.
- Тщательно спроектированные API вспомогательных библиотек не заставляют изменять использующий их код; вместо этого они поддерживают интуитивно понятную реализацию.
Мы давно знали об этих шаблонах и применяли их в различных формах в рамках нашей кодовой базы. Но несколько фундаментальных проблем значительно ограничивало успешное использование этих шаблонов. Во-первых, шаблон фасада не исключает необходимость в большом количестве инфраструктурного кода — он просто перемещает его в другой класс и, в целом, просто создает больше работы для разработчика. Во-вторых, если только DI-контейнер не распознает автоматически свои компоненты в период выполнения (например, через атрибуты), он по-прежнему требует централизованной регистрации и на практике лишь вводит в реализацию дополнительный уровень. Наконец, проектировать и реализовать API, которые одновременно и гибки, и интуитивно понятны, и полезны, — задача дорогостоящая и крайне трудная.
Основная особенность методики AAL в том, что с ее помощью мы получаем общее централизованное место для реализации наших наиболее эффективных правил и принципов, не загрязняя код бизнес-логики.
Зачем мы создали Adaptive Access Layers
После нескольких сеансов мозгового штурма мы пришли к единому гибкому и эффективному решению всех этих проблем. Основная идея в том, чтобы скрыть все API за помечаемыми атрибутами интерфейсами уровня доступа и создать механизм реализации, способный реализовать такие интерфейсы в период выполнения в соответствии со всеми правилами и руководствами. Мы называем эту методику «Adaptive Access Layers» (AAL), поскольку в каждом решении с высокой степенью гибкости определяются необходимые ему интерфейсы уровня доступа. Мы объединили механизм реализации AAL с управляемым атрибутами DI-контейнером Autofac с открытым исходным кодом и добились создания гибкой инфраструктуры Windows-служб, которая резко упрощает создание четких, интуитивно понятных и тестируемых реализаций. Рис. 1 иллюстрирует, насколько резко уменьшается размер и сложность решения, когда для отделения реализации базовой логики ото всех сопутствующих библиотек и API используется AAL. Более светлые элементы на схеме представляют размеры одного решения, реализованного с помощью AAL (слева) и без этой методики (справа).
Рис. 1. Adaptive Access Layers (слева) кардинально упрощают решение и уменьшают его размеры
Database API | Database API |
Adaptive Access Layers | Adaptive Access Layers |
Solution Is Pure Business Logic | Решение является чистой бизнес-логикой |
Message Bus API | Message Bus API |
WCF Service Clients | Клиенты WCF-сервисов |
Logging API | API протоколирования |
Solution Is Big and Complex | Решение является большим и сложным |
Business Logic | Бизнес-логика |
Основная особенность методики AAL в том, что с ее помощью мы получаем общее централизованное место для реализации наших наиболее эффективных правил и принципов, не загрязняя код бизнес-логики. В этом отношении AAL похожа на аспектно-ориентированное программирование (aspect-oriented programming, AOP), а также на различные методики средств перехвата и прокси. Главное отличие в том, что AAL скрывает нижележащие API от кода бизнес-логики, тогда как другие методики раскрывают их и тем самым значительно увеличивают размеры решения.
Чтобы проиллюстрировать эту идею, я рассмотрю простой уровень доступа, размещаемый между некоей бизнес-логикой и стандартным журналом событий Windows. Для примера возьмем сервис, который регистрирует поступающие заказы в какой-то базе данных. Если вызов базы данных завершается неудачей, сервис должен записать ошибку в журнал событий.
При традиционном подходе это могло бы потребовать вызова .NET-метода EventLog.WriteEntry и выглядело бы как код на рис. 2. Данный подход не оптимален по двум причинам. Во-первых, он не слишком хорошо подходит для модульного тестирования, так как тесту пришлось бы анализировать журнал событий на компьютере, выполняющем модульные тесты для проверки того, что в этот журнал действительно записан элемент с правильным текстом. А во-вторых, четыре строки инфраструктурного кода «загрязняют» базовую часть бизнес-логики.
Рис. 2. Традиционное обращение к журналу событий
public OrderConfirmation RegisterOrder(Order order)
{
try
{
// Вызов базы данных для регистрации заказа
// и возврата подтверждения
}
catch (Exception ex)
{
string msg = string.Format("Order {0} not registered due to error: {1}",
order.OrderId,
ex.Message);
_eventLog.WriteEntry(msg, 1000, EventLogEntryType.Error);
}
}
Обе проблемы устраняются введением интерфейса AAL между бизнес-логикой и нижележащим классом EventLog. Такой уровень демонстрируется в следующем коде:
[EventLogContract("OrderService")]
public interface IOrderServiceEventLog{
[EventEntryContract(1000, EventLogEntryType.Error,
"Order {0} not reg due to error: {1}"]
void OrderRegistrationFailed(int orderId, string message);
}
Этот уровень определяется использующим атрибуты интерфейсом IOrderServiceEventLog, который реализуется через динамический класс механизмом реализации в период выполнения. Сам интерфейс имеет атрибут [EventLogContract], чтобы механизм реализации мог распознать его как уровень доступа к журналу событий. Единственный параметр — имя целевого журнала событий. Никаких ограничений на имя интерфейса или количество методов в нем нет. Каждый метод должен возвращать void (при записи информации в журнал событий просто нечего возвращать) и иметь атрибут [EventEntryContract]. Этот атрибут принимает все фиксированные входные метаданные (идентификатор, уровень серьезности и форматирование) как параметры, поэтому такие метаданные больше не нужно размещать в бизнес-логике.
При использовании интерфейса уровня доступа бизнес-логика с рис. 2 становится гораздо компактнее и понятнее:
public OrderConfirmation RegisterOrder(Order order)
{
try
{
// Вызов базы данных для регистрации заказа
// и возврата подтверждения
}
catch (Exception ex)
{
_logLayer.OrderRegistrationFailed(order.Id, ex.Message);
}
}
Метод RegisterOrder в примере теперь прост, отлично читаем и гораздо более тестируемый, поскольку проверка больше не требует анализа журнала событий — вместо это используется небольшой класс-заглушка, реализующий интерфейс уровня доступа. Другое преимущество в том, что интерфейс IOrderServiceEventLog может инкапсулировать все взаимодействие сервиса заказов с журналом событий и тем самым давать простое, но полное представление того, какие элементы этого журнала пишет система.
(Примечание Недавно выпущенный Semantic Logging Application Block [SLAB] для использования поверх нового класса EventSource из .NET 4.5 охватывает те же идеи перемещения метаданных из кода в атрибуты и предоставления настраиваемых, строго типизированных методов протоколирования вместо нескольких универсальных методов. Чтобы задействовать SLAB, разработчики должны реализовать собственный класс, производный от EventSource, и использовать его во всей кодовой базе. Я считаю, что наш подход ничуть не уступает SLAB, но легче в применении, так как требует от разработчиков лишь определения интерфейса, а не реализации класса. Главная особенность класса EventSource в том, что он поддерживает структурированное протоколирование событий через конфигурируемый набор приемников (sinks). Наша реализация уровня доступа в настоящее время не поддерживает такое протоколирование, но может быть легко расширена с этой целью, поскольку у нее есть доступ к структурированной информации через параметры метода нашего интерфейса.)
Доступ к базе данных — главная часть большинства корпоративных систем, и наши системы не являются исключением.
Я еще не рассматривал тело метода RegisterOrder, а именно вызов некоторых хранимых процедур в базе данных SQL Server, чтобы сохранить заказ для последующей обработки. Если бы моя группа реализовала это через .NET SqlClient API, нам понадобилось бы минимум 10 строк тривиального кода для создания экземпляров SqlConnection и SqlCommand, заполнения команды параметрами из свойств Order, выполнения команды и считывания набора результатов. Если бы мы попытались соблюсти дополнительные требования вроде автоматических повторных попыток на случай сбоев базы данных или истечения периодов ожидания, то легко могли бы получить 15–20 строк кода только для того, чтобы выполнить довольно простой вызов. И все это потребовалось бы только потому, что мишень вызова находится в хранимой процедуре, а не во внутрипроцессном .NET-методе. С точки зрения бизнес-логики, нет абсолютно никаких причин для того, чтобы наша реализация была такой громоздкой и сложной лишь из-за обработки между двумя системами.
Введя адаптивный уровень доступа к базе данных, аналогичный уровню доступа к журналу событий, можно реализовать тело упомянутого выше метода так, чтобы оно было простым и тестируемым:
public OrderConfirmation RegisterOrder(Order order)
{
try
{
return _ordersDbLayer.RegisterOrder(order);
}
catch (Exception ex)
{
_logLayer.OrderRegistrationFailed(order.Id, ex.Message);
}
}
До сих пор я иллюстрировал идеи, гибкость и мощь AAL. Теперь перейдем к более подробному рассмотрению уровней доступа, которые мы разработали и находим очень полезными. Я начну с только что упомянутого уровня доступа к базе данных.
Уровни доступа к базам данных Доступ к базе данных — главная часть большинства корпоративных систем, и наши системы не являются исключением. Будучи ключевым участником серьезной онлайновой торговли финансовыми инструментами, мы должны соответствовать некоторым строгим требованиям к безопасности и производительности, предъявляемым нашими клиентами и финансовыми органами, а значит, вынуждены тщательно защищать наши базы данных. В принципе, мы обращаемся к базам данных только через хранимые процедуры, так как они позволяют нам применять детализированные правила безопасности и проверять все запросы к базам данных на предмет производительности и нагрузки на сервер до того, как они попадут в наши производственные системы.
Мы тщательно оценили, способны ли такие средства объектно-реляционного сопоставления (ORM), как Entity Framework, помочь нам в создании более простого и тестируемого кода без отказа от хранимых процедур. Мы пришли к заключению, что Entity Framework — крайне привлекательное решение, но слишком сильно зависимое от возможности композиции и исполнения сложных SQL-выражений в период выполнения. Эта инфраструктура может сопоставлять хранимые процедуры, но, когда сопоставление ограничивается только хранимыми процедурами, она теряет большинство своих преимуществ. По этой причине мы решили реализовать свою инфраструктуру доступа к базам данных в виде адаптивного уровня доступа.
Наша реализация поддерживает вызовы хранимых процедур, операции выборки представлений и эффективные операции массовой вставки через функциональность массового копирования в SQL Server. Она позволяет сопоставлять входные данные напрямую из свойств DTO-класса (Data Transfer Objects) с параметрами хранимой процедуры и может аналогичным образом сопоставлять поля набора результатов со свойствами класса. Это способствует четкости синтаксиса при обращении к базе данных из .NET-кода.
Следующий код показывает простой уровень, который подходит для примера сервиса регистрации заказов:
[DatabaseContract("Orders")]
public interface IOrdersDatabase{
[StoredProcedureContract("dbo.RegisterOrder",
Returns=ReturnOption.SingleRow)]
OrderConfirmation RegisterOrder(Order order);
}
Этот код сопоставляет одну хранимую процедуру и преобразует одну строку из набора результатов в экземпляр OrderConfirmation, инициализируемый на основе полей набора результатов. Параметрам сопоставленной хранимой процедуры присваиваются значения из свойств данного экземпляра Order. Эта функциональность сопоставления определена в атрибуте [StoredProcedureContract] и поэтому больше не требуется в реализации бизнес-логики, повышая ее четкость и читаемость.
Наша реализация поддерживает вызовы хранимых процедур, операции выборки представлений и эффективные операции массовой вставки через функциональность массового копирования в SQL Server.
Мы реализовали в уровне доступа к базе данных некоторые весьма продвинутые средства, так как пришли к выводу, что это простой и эффективный способ предлагать стандартную функциональность нашим разработчикам, не ограничивая им свободу в реализации своей бизнес-логики наиболее естественным и интуитивно понятным образом.
Одна из поддерживаемых функций — возможность массовой вставки строк через SQL-функциональность массового копирования. Она позволяет нашим разработчикам определять простой метод, который принимает перечисляемый набор DTO-класса, представляющий строки, вставляемые как входные данные. Уровень доступа управляет всеми деталями и тем самым избавляет бизнес-логику от 15–20 строк сложного кода, связанного с операциями над базой данных. Эта поддержка массового копирования — отличный пример концептуально простой операции (эффективной вставки строк в таблицу), которую в итоге обычно становится весьма сложно реализовать только потому, что нижележащий .NET-класс SqlBulkCopy работает с IDataReader, а не напрямую с нашим DTO-классом.
Уровень доступа к базе данных был первым из реализованных нами, и результат с самого начала оказался очень успешным. Благодаря этому мы пишем меньше кода (причем сам код стал проще), а наши решения естественным образом в максимальной мере охватываются модульными тестами. Отталкиваясь от этих положительных результатов, мы быстро осознали преимущества введения AAL между кодом бизнес-логики и несколькими другими внешними ресурсами.
Уровни доступа к сервисам Наша реализация торговой системы в высшей степени ориентирована на сервисы, и поэтому для нас важно отказоустойчивое взаимодействие между сервисами. Наш стандартный протокол — Windows Communication Foundation (WCF), и у нас много кода, связанного с WCF-вызовами.
Большинство реализаций следует одному и тому же шаблону в целом. Сначала разрешаются адреса конечных точек (как правило, наши сервисы работают либо в режиме «активный-активный», либо в режиме «активный-пассивный»). Затем с помощью .NET-класса ChannelFactory создается реализация класса канала, по которому вызывается нужный метод. Если метод выполняется успешно, канал закрывается и удаляется, но, если метод дает сбой, требуется проверка на исключение. В некоторых случаях имеет смысл повторно вызвать метод в той же конечной точке, тогда как в других ситуациях лучше выполнять автоматическое восстановление после сбоев и пытаться обращаться по одной из других доступных конечных точек. Вдобавок нам зачастую нужно помещать в карантин сбойную конечную точку на короткий период, чтобы не перегружать ее попытками соединения и вызовами метода, который дает ошибку.
Корректно реализовать этот шаблон — задача далеко не тривиальная, и она может потребовать 10–15 строк кода. И вновь это усложнение вводится только потому, что бизнес-логика, к которой нам нужно обращаться, находится в другом сервисе или вне основного процесса. Мы реализовали адаптивный уровень доступа к сервисам, чтобы исключить это усложнение и сделать вызов удаленного метода настолько же простым и безопасным, как и вызов внутрипроцессного метода.
Принципы и рабочий процесс идентичны таковым в уровне доступа к базам данных. Разработчик пишет помечаемый атрибутами интерфейсом, который сопоставляет только нужные методы, и наш механизм реализации создает тип периода выполнения, реализующий интерфейс с поведением, указанным в атрибутах.
Следующий код показывает небольшой уровень доступа к сервису, сопоставляющий единственный метод:
[ServiceAccessLayer(typeof(IStatisticsSvc),
"net.tcp", "STATISTICS_SVC"]
public interface ISalesStatistics{
[ServiceOperationContract("GetTopSellingItems")]
Product[] GetTopProducts(int productCategory);
}
Атрибут интерфейса определяет нижележащий интерфейс с атрибутом [ServiceContract] (используется при внутреннем вызове ChannelFactory), применяемый протокол и идентификатор вызываемого сервиса. Последний служит ключом для нашей службы поиска сервисов (service locator), которая разрешает реальные адреса конечных точек во время вызова. Уровень доступа по умолчанию использует исходную WCF-привязку для данного протокола, но это можно изменить, задав дополнительные свойства в атрибуте [ServiceAccessLayer].
Единственный обязательный параметр в ServiceOperationContract — это «глагол действия», который идентифицирует сопоставленный метод в нижележащем контракте WCF-сервиса. Другие, необязательные параметры этого атрибута указывают, будут ли кешироваться результаты вызова сервиса и всегда ли безопасно автоматически восстанавливать операцию после неудачи, даже если вызов первой конечной точки WCF дал ошибку после того, как код был уже выполнен в целевом сервисе.
Другие уровни доступа Мы также создали аналогичные AAL для файлов трассировки, счетчиков производительности и своей шины сообщений. Все они основаны на тех же принципах, которые были проиллюстрированы в предыдущих примерах, т. е. бизнес-логика выражает доступ к ресурсам в максимально простом виде, перемещая все метаданные в атрибуты.
Интеграция встраивания зависимостей
С помощью уровней доступа нашим разработчикам больше не нужно реализовать массу тривиального инфраструктурного кода, но по-прежнему должен быть способ вызова механизма реализации AAL для получения экземпляра типа, реализуемого в период выполнения, при каждом вызове сопоставленного внешнего ресурса. Механизм реализации можно вызывать напрямую, но это идет вразрез с нашими принципами сохранения четкости и тестируемости бизнес-логики.
Мы решили эту проблему, зарегистрировав наш механизм реализации как источник динамической регистрации в Autofac, чтобы он вызывался всякий раз, когда Autofac не может разрешить какую-либо зависимость с помощью любой статической регистрации. В этом случае Autofac будет запрашивать механизм реализации, может ли тот разрешить данную комбинацию типа и идентификатора. Механизм проверит тип и предоставит его экземпляр, если этот тип является интерфейсом уровня доступа с атрибутами.
После этого мы сформировали среду, где реализации бизнес-логики могут просто объявлять свои типы интерфейсов уровня доступа и принимать их в качестве параметров (например, в конструкторах классов), а затем довериться DI-контейнеру в том, что он сумеет разрешить эти параметры, вызывая на внутреннем уровне механизм реализации. Такие реализации будут естественным образом работать с интерфейсами и будут легко тестируемыми, так как для тестирования этих интерфейсов достаточно создать несколько классов-заглушек.
Реализация
Все наши уровни доступа реализуются по одной методике. Общая идея заключается в том, чтобы реализовать всю функциональность в коде на чистом C# в абстрактном базовом классе, а потом лишь генерировать тонкий класс, который наследует от этого базового класса и реализует интерфейс. Сгенерированное тело каждого метода интерфейса просто делегирует выполнение универсальному методу Execute в базовом классе.
Сигнатура этого универсального метода выглядит так:
object Execute(Attribute, MethodInfo, object[], TAttributeData)
Первый параметр — атрибут метода интерфейса уровня доступа, из которого вызывается метод Execute. Как правило, он содержит все метаданные (например, имя хранимой процедуры, спецификацию повторных попыток и др.), необходимые методу Execute для того, чтобы обеспечить правильное поведение исполняющей среды.
Второй параметр — это отражаемый экземпляр MethodInfo для метода интерфейса. Он хранит всю информацию о реализуемом методе, в том числе типы и имена его параметров, и используется методом Execute для интерпретации третьего параметра, который содержит значения всех параметров для текущего вызова метода интерфейса. Метод Execute обычно пересылает эти значения нижележащему API ресурса, например в виде параметров для хранимой процедуры.
Четвертый параметр является пользовательским типом, который хранит фиксированные данные, используемые при каждом вызове метода, чтобы сделать его максимально эффективным. Фиксированные данные инициализируются только раз (методом абстрактного базового класса), когда механизм реализует класс периода выполнения. Наши уровни доступа к базам данных используют этот функционал для разового анализа хранимых процедур и подготовки шаблона SqlCommand к использованию при вызове метода.
Параметры Attribute и MethodInfo, передаваемые методу Execute, также отражаются только раз и повторно используются при каждом вызове метода, чтобы минимизировать издержки вызова.
Значение, возвращаемое Execute, используется как возвращаемое значение для реализованного метода интерфейса.
Эта структура довольно проста и, как оказалось, весьма гибка и эффективна. Мы повторно использовали ее во всех наших уровнях доступа через абстрактный общий базовый класс AccessLayerBase. Он реализует всю необходимую логику для анализа помеченного атрибутом интерфейса и управления процессом генерации нового класса в период выполнения. В каждой категории уровней доступа имеется свой специализированный абстрактный базовый класс, производный от AccessLayerBase. Он содержит реализацию доступа к внешнему ресурсу, например к хранимой процедуре в соответствии со всеми нашими правилами. На рис. 3 показана иерархия классов реализации для примера интерфейса уровня доступа к базе данных. Светло-серой рамкой обведены элементы инфраструктуры AAL, черной — интерфейс с атрибутом, определенный функциональным решением для бизнес-логики, а темно-серым — класс периода выполнения, генерируемый механизмом реализации AAL.
Рис. 3. Схема реализации уровня доступа
AccessLayerBase Abstract Class | Абстрактный класс AccessLayerBase |
DatabaseAccessLayer Class | Класс DatabaseAccessLayer |
IAccessLayer Interface | Интерфейс IAccessLayer |
далее все переводится по аналогии | |
Рис. 3 также иллюстрирует, как мы дали возможность базовым классам реализовать открытые интерфейсы (наследуя от IAccessLayer), которые предоставляют ключевую информацию о поведении. Это предназначено для использования не в реализациях бизнес-логики, а инфраструктурной логикой, например для отслеживания каждого неудачного вызова хранимой процедуры.
Эти интерфейсы уровня доступа также полезны в нескольких особых случаях, где технические или бизнес-требования вынуждают обращаться к нижележащему за уровнем доступа ресурсу таким способом, который не полностью поддерживается AAL. С помощью этих интерфейсов наши разработчики могут использовать AAL, но перехватывать и настраивать нижележащие операции так, чтобы они отвечали специфическим требованиям. Хороший пример — событие IDatabaseAccessLayer.ExecutingCommand. Оно генерируется непосредственно перед выполнением SqlCommand и позволяет нам настраивать этот объект, изменяя такие свойства, как значения интервалов ожидания или какие-то параметры.
Отчеты и проверки
AAL-интерфейсы с атрибутами в бизнес-логике также позволяют нам использовать механизм отражения применительно к компилируемым двоичным файлам на этапе сборки и получать полезные отчеты. Наша группа включила этот функционал в процесс сборки на основе Team Foundation Server (TFS), чтобы в результат каждой сборки теперь входило несколько небольших, но информативных XML-файлов.
Отчеты на этапе сборки Уровень доступа к базе данных сообщает полный список хранимых процедур, представлений и массовых операций вставок. На основе этого мы упрощаем анализ и проверяем, чтобы все необходимые объекты базы данных были должным образом развернуты и сконфигурированы до выпуска бизнес-логики.
Аналогично наш уровень доступа к журналу событий сообщает полный список элементов в этом журнале, которые может генерировать сервис. На стадиях после сборки эта информация принимается и преобразуется в пакет управления (management pack) для производственной среды Microsoft System Center Operations Manager. Это разумно, так как гарантирует, что у Operations Manager всегда будет актуальная информация о том, как лучше всего обрабатывать проблемы, возникающие в производственной среде.
Разумеется, широко используемая и очень гибкая платформа может стать единой точкой сбоя.
Автоматизированные пакеты Microsoft Installer Мы применили ту же методику отражения для сбора ценного ввода в пакеты Microsoft Installer (MSI), которые генерируются для наших Windows-служб на конечном этапе сборок в TFS. Главная цель этих пакетов — установить и сконфигурировать журнал событий и счетчики производительности, чтобы они гарантированно соответствовали развертываемой бизнес-логике. Процесс сборки извлекает имена для журнала событий и определения счетчиков производительности из двоичных файлов и автоматически генерирует MSI-пакет, устанавливающий эти имена и определения.
Проверки в период выполнения Одна из наиболее часто сообщаемых ошибок в нашей производственной среде — попытка сервиса вызвать несуществующую хранимую процедуру (или существующую, но с другой сигнатурой) из производственной базы данных. Такого рода ошибки случаются из-за того, что мы временами пропускаем развертывание всех необходимых объектов базы данных при развертывании сервиса в производственной среде. Здесь критически важно не то, что мы что-то упускаем при развертывании — это можно было бы сравнительно легко исправлять, а тот факт, что ошибки происходят не при развертывании, а позднее, при первом вызове хранимой процедуры. Мы использовали формируемый на основе отражения список всех объектов базы данных, к которым запрашивается доступ, и, чтобы устранить проблему, позволили нашим сервисам проверять наличие и корректность всех объектов при их запуске. Сервис просто проходит по списку объектов, а затем запрашивает каждый из них в базе данных для проверки того, что при необходимости он сможет обратиться к этому объекту. Тем самым мы переместили выявление всех таких ошибок на этап развертывания, когда исправить их гораздо проще и безопаснее.
Я перечислил эти дополнительные области применения, чтобы проиллюстрировать важнейшее преимущество AAL. Располагая почти полной информацией о поведении сервиса, доступной через отражение, мы открыли совершенно новое измерение интеллектуальной отчетности, сборки, автоматизации и мониторинга. Некоторые из открывшихся возможностей наша группа уже использует, но мы присматриваемся к ряду дополнительных интересных сфер применения.
Эффективность труда и качество
AAL, спроектированные и реализованные за последние два года, на практике доказали свою чрезвычайную пользу для повышения эффективности труда разработчиков и качества решений. Мы сократили время на подготовку нового сервиса с недель до часов, а на расширение существующих сервисов — с дней до минут. Это позволило увеличить нашу гибкость и тем самым удешевить стоимость собственных разработок.
Наши уровни доступа подходят при реализации огромного множества бизнес-решений. Однако несколько особых случаев, где эти уровни не годятся, все же есть: обычно это ситуации, в которых нужна очень высокая степень адаптации, например, когда имя вызываемой хранимой процедуры считывается из таблицы конфигурации и на этапе компиляции не известно. Наша группа умышленно предпочла не поддерживать такие случаи в уровнях доступа, чтобы избежать дополнительного усложнения инфраструктуры. Вместо этого мы разрешили нашим разработчикам использовать в таких случаях чистые .NET API.
Само по себе решение AAL не является большим и было разработано за несколько человеко-месяцев в течение двухлетнего периода. Таким образом, наши начальные инвестиции не были слишком высокими и уже достигли уровня безубыточности благодаря экономии времени на разработке и поддержке.
Разумеется, широко используемая и очень гибкая платформа может стать единой точкой сбоя. Мы ослабили остроту этой проблему за счет полного охвата решения AAL модульными тестами и развертывания его новых версий под тщательным контролем. Вы могли бы возразить, что подход с AAL сам по себе вводит дополнительную сложность в нашу систему и вынуждает наших разработчиков изучать новый уровень абстракции. Но мы считаем, что эти издержки с лихвой компенсируются повышением общей эффективности труда и качества.