Главная страница   /   6.5. Мониторинг связывания (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

6.5. Мониторинг связывания

Слабая связанность является ценной, но это удивительно легко – ввести сильную (тесную) связанность. Все, что нужно, это начинающий разработчик и малейшая неосторожность, и будут введены жесткие ссылки. В Visual Studio можно легко добавить новые ссылки на существующий проект, но часто мы хотим того избежать. Необходимо соблюдать дисциплину, чтобы гарантировать, что каждый модуль фокусируется на своей зоне ответственности.

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

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

Связывание при юнит тестировании

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

В примере коммерческого приложения уже есть юнит тесты, так что вы легко можете добавить еще один. Следующий листинг показывает модульный тест, который защищает модуль логики представления (Presentation Logic module) от прямого обращения к модулю доступа к данным на основе SQL Server (SQL Server–based Data Access module).

Листинг 6-4: Обеспечение слабой связанности при помощи юнит тестирования
[Fact]
public void SutShouldNotReferenceSqlDataAccess()
{
	// Fixture setup
	Type sutRepresentative = typeof(HomeController);
	var unwanted = "Ploeh.Samples.Commerce.Data.Sql";
	// Exercise system
	var references =
		sutRepresentative.Assembly
		.GetReferencedAssemblies();
	// Verify outcome
	Assert.False(
		references.Any(a => a.Name == unwanted),
		string.Format(
			"{0} should not be referenced by SUT",
			unwanted));
	// Teardown
}

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

Кроме того, необходимо определить сборку, на которую не должны ссылаться. Вы можете использовать ту же технику и выбрать репрезентативный тип из этой сборки, но это будет означать, что вы должны сослаться на эту сборку из модульного теста. Это не так плохо, как ссылки на нежелательную сборку из рабочего кода, но это все равно создаст искусственную связь между этими двумя библиотеками – можно сказать, они становятся «уличенными в связи». Хотя безопасность типа желательна, слабая связанность имеет козыри в данном случае, поэтому вы определяете нежелательную сборку при помощи строки (но см. следующие обсуждения относительно других возможностей).

Получить сборку, на которую ссылаются, от репрезентативного типа так же просто, как один раз вызвать метод. Теперь вы можете использовать простой LINQ запрос, чтобы убедиться, что ни одна из этих сборок, на которые ссылаются, не имеет нежелательного имени. В утверждении (прим. переводчика: в принципе ААА при юнит тестировании последнее А – это Assert, в данном случае «утверждение») вы также выводите сообщение, которое отображается, если утверждение не выполняется.

Совет

Это утверждение использует простой LINQ запрос, но вы можете заменить его циклом foreach, если вы разрабатываете на .NET 3.0 или более ранних версиях.

Совет

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

Тестирование связанности при помощи Red/Green/Refactor

Если вы используете TDD для реализации вашего кода, вы привыкли к так называемому циклу разработки Red/Green/Refactor, где вы сначала пишете неудачный тест, затем он проходит успешно, и, наконец, вы меняете код, чтобы сделать его более легким в поддержке.

Оказывается, сделать так, чтобы тест, предотвращающий тесную связанность, не сработал, сложнее, чем вы думаете. Даже если целевой проект Visual Studio имеет ссылку на нежелательную зависимость, компилятор включит только ссылку, если она используется.

Таким образом, чтобы сделать тест, который не сработает, мы должны сначала добавить нежелательную ссылку, а затем написать фиктивную строку кода (dummy code), которая использует тип из нежелательной зависимости. Как только мы увидели, что тест не прошел, мы можем затем изменить процесс на противоположный, чтобы тест прошел. Очевидно, что это не проблема, если тестируемая библиотека уже нарушает ограничение связанности.

В предыдущем примере юнит тест добавлен к существующему набору юнит тестов, предназначенному для модуля логики представления. Рисунок 6-16 иллюстрирует ссылки в действии.

Рисунок 6-16: Библиотека PresentationLogicUnitTest – это набор тестов, нацеленный на библиотеку PresentationLogic. Чтобы сделать это, данная библиотека должна содержать ссылку к своей цели, а также общие абстракции, которые определены в доменной модели. Поскольку PresentationLogicUnitTest не нацелен на доменную модель, модуль DomainModel показан серым цветом.

Листинг 6-4 определяет нежелательную сборку при помощи простой строки, но было бы более безопасно для типа определить его при помощи репрезентативного типа. Однако вам может потребоваться добавить в юнит тест ссылку к модулю доступа к данным на основе SQL Server (SQL Server–based Data Access module), как показано на рисунке 6-17.

Рисунок 6-17: Если мы хотим безопасности типов, добавив репрезентативный тип из библиотеки SqlDataAccess в PresentationLogicUnitTest, мы вводим новую зависимость в набор юнит тестов по той единственной причине, что мы хотим убедиться, что это никогда не будет случайно добавлено в библиотеку PresentationLogic. Иронично, не так ли?

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

Косвенные зависимости

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

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

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

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

Это не проблема, если у нас есть основания полагать, что имена сборок стабильны. Если же это не так, нам нужна другая стратегия.

Связывание при интеграционном тестировании

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

Вы можете добавить новый тестовый проект в решение Commerce (Commerce solution) и добавить все нужные ссылки. Рисунок 6-18 показывает это решение, и хотя он очень похож на рисунок 6-17, разница состоит в том, что для интеграционного теста все ссылки легальны и одинаково действительны.

Рисунок 6-18: Проект CommerceIntegrationTest содержит автоматизированные тесты, которые проверяют, что отношения между модулями являются правильными. В отличие от модульных тестов, интеграционные тесты могут содержать столько ссылок, сколько необходимо, для проведения данного теста.

Интеграционные тесты

Интеграционное тестирование – это еще один тип автоматизированного тестирования на уровне API. Разница между модульным тестом и интеграционным тестом заключается в том, что модульное тестирование имеет дело с модулями (юнитами) в изоляции, в то время как интеграционные тесты сосредоточены на проверке того, что несколько юнитов (часто в разных библиотеках) интегрируются друг с другом, как нужно.

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

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

Листинг 6-5: Обеспечение слабой связанности при помощи интеграционного теста
[Fact]
public void PresentationModuleShouldNotReferenceSqlDataAccess()
{
	// Fixture setup
	Type presentationRepresentative =
		typeof(HomeController);
	Type sqlRepresentative =
		typeof(SqlProductRepository);
	// Exercise system
	var references =
		presentationRepresentative.Assembly
		.GetReferencedAssemblies();
	// Verify outcome
	AssemblyName sqlAssemblyName =
		sqlRepresentative.Assembly.GetName();
	AssemblyName presentationAssemblyName =
		presentationRepresentative.Assembly.GetName();
	Assert.False(references.Any(a =>
		AssemblyName.ReferenceMatchesDefinition(
		sqlAssemblyName, a)),
		string.Format(
		"{0} should not be referenced by {1}",
		sqlAssemblyName,
		presentationAssemblyName));
	// Teardown
}

Строки 10-12: Получить сборки, на которые ссылаются

Строки 14-17: Получить имена сборок

Строки 18-20: Искать нежелательную зависимость

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

Так же как и раньше, вы получите список всех сборок, на которые ссылается библиотека PresentationLogic. Используя AssemblyName каждой сборки, вы проверяете, чтобы ссылки не содержали сборку на основе SQL Server. Встроенные статический метод ReferenceMatchesDefinition сравнивает имена сборок.

Возможно, вы заметили, что тесты в листингах 6-4 и 6-5 аналогичны. Вы могли бы написать новые тесты, как тот, что в листинге 6-5, изменив два репрезентативных типа и оставив все остальное, как есть.

Следующим логическим шагом было бы выделение общей части теста в параметризованный тест (Parameterized Test). Это позволит вам написать простой список почти декларативных тестов, которые определяют, что разрешено или не разрешено в этой конкретной совокупности модулей.

Модульные тесты и интеграционные тесты являются отличными вариантами, если вы уже используете автоматизированные тесты на API уровня. Если же нет, вы должны начать делать это как можно скорее, но есть и другие альтернативы.

Использование NDepend для мониторинга связанности

Если по каким-то непостижимым причинам вы не хотите использовать юнит тесты, вы можете использовать инструмент под названием NDepend (http://ndepend.com), который предупредит вас, если вы или члены вашей команды ввели нежелательную связанность.

NDepend является коммерческим инструментом программного обеспечения, который анализирует проекты или решения и дает много статистики о коде. В качестве примера, он может генерировать графы зависимостей, которые мало чем отличаются от тех, что вы видели в этой книге. Если проанализировать коммерческое решение Мэри из главы 2, мы получим граф, показанный на рисунке 6-19.

Рисунок 6-19: Граф зависимостей, сгенерированный NDepend, для коммерческого решения Мэри. По умолчанию NDepend включает все зависимости, в том числе модули из BCL. Размер блоков отображает число строк кода в каждом модуле, а толщина стрелки отражает число элементов, используемых по всем ссылкам.

Это кажется сложным, но мы можем скрыть BCL модули, и мы получим рисунок 6-20.

Рисунок 6-20: Измененный график NDepend для коммерческого решения Мэри. На этом графе я вручную удалил все BCL модули и сделал блоки и стрелки одного размера.

Выглядит ли рисунок 6-20 знакомыми? Если вы обладаете эйдетической памятью, вы можете вспомнить рисунок 2-10, в ином же случае просто вернитесь к нему. Обратите внимание, что они представляют одни и те же структуры и иллюстрируют одни и те же отношения.

NDepend может сделать гораздо больше, чем нарисовать симпатичные графики. Одна из его наиболее мощных возможностей – это Code Query Language (CQL), который позволяет нам запрашивать наш код по широкому спектру информации и имеет синтаксис, напоминающий синтаксис SQL.

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

WARN IF Count > 0 IN SELECT ASSEMBLIES WHERE
IsDirectlyUsing "ASSEMBLY:Ploeh.Samples.Mary.ECommerce.Data.Sql" AND
NameIs "Ploeh.Samples.Mary.ECommerce.Domain"

После выполнения этот CQL запрос выдает предупреждение, если доменный модуль напрямую обращается к модулю SQL Server DataAccess. В решении Мэри этот запрос действительно выдаст предупреждение.

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

Примечание

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

NDepend и автоматизированные тесты – это два способа автоматического контроля кода, чтобы незаконные зависимости случайно не прокрались. Мы можем использовать один или оба из этих вариантов в рамках тестирования сборки (Build Verification Test, BVT) или непрерывной интеграции (Continuous Integration, CI).

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

Внимание

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

Обращайте внимание на каждый случай, и если есть реальная проблема, решайте ее.

Рассмотрите возможность использования автоматизированных инструментов для мониторинга связанности в больших базах кода. Это может предотвратить случайную тесную связанность и «порчу» вашего кода, а сами сосредоточьтесь на другие проблемах, описанных в этой главе.