Построение PowerShell cmdlets

Некоторые фреймворки совсем не предоставляют швов, которые позволяют нам управлять жизненным циклом основных элементов фреймворка. Windows PowerShell является одним из таких фреймворков.

Примечание

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

Важным элементов PowerShell является cmdlet (предполагаю, что это слово произносится как commandlet, но я видел только, что он пишется как cmdlet). Вы можете считать cmdlet продвинутой утилитой командной строки.

cmdlet – это класс, унаследованный от Cmdlet, и он должен иметь конструктор по умолчанию. Как и для ASP.NET это требование эффективно исключает любое использование Constructor Injection. Решение проблемы тоже аналогично: мы перемещаем Composition Root в конструктор каждого cmdlet. Единственное отличие – отсутствует встроенный Application Context, поэтому мы должны прибегнуть к самому низшему универсальному деноминатору: статическому классу.

Примечание

Я предпочитаю code smell (гнилой код) любому использованию ключевого слова static, но по сравнению с анти-паттернами code smell'ы указывают только на потенциальные недостатки проектирования. В некоторых особых случаях использование подобных идиом оправдано, и это именно тот случай.

Вас может заинтересовать то, как это все отличается от анти-паттерна Service Locator. Как и для ASP.NET главное отличие заключается не в структуре кода, а в использовании паттерна. Вместо того чтобы пытаться использовать статический Service Locator в качестве виртуального ключевого слова new, для каждого cmdlet мы используем его только один раз. Чтобы защитить себя в дальнейшем от неправильного использования, мы можем сделать Composer внутренним и использовать его только для преобразования типов из различных сборок, как это продемонстрировано на рисунке 7-17.

Рисунок 7-17: Когда нет выхода для статического контейнера, мы можем сделать его внутренним и переместить в корневую сборку приложения. Все методы Resolve возвращают классы, которые определены в других сборках. Таким образом, как только контейнер преобразовал реализатор, ни один из классов преобразованной иерархии зависимостей не имеет доступа к статическому контейнеру, поскольку все они находятся за пределами центральной сборки приложения, а контейнер находится внутри.

Результатом преобразования диаграммы зависимостей является класс, определенный в другой сборке, и этот класс не имеет доступа к статическому контейнеру, поскольку он находится внутри центральной сборки приложения. Реализатору cmdlet необходимо использовать соответствующие DI-паттерны, как например, Constructor Injection, для того, чтобы применять любые зависимости, и мы эффективно защищаем себя от угроз Service Locator'а.

Давайте рассмотрим пример, который иллюстрирует этот принцип.

Пример: построение cmdlet'ов управления корзиной

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

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

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

Вы можете реализовать желаемое API с помощью двух cmdlet'ов: один – для того, чтобы извлечь все корзины, а другой – для удаления корзины данного пользователя. Следующий листинг является примером того, как это могло бы выглядеть в интерактивной сессии.

Листинг 7-14: Удаление корзин, хранящихся более одного месяца
PS C:\> Get-Basket
LastUpdated Owner Total
----------- ----- -----
19.03.2010 20:5... ploeh 89,4000
22.01.2010 19:5... ndøh 199,0000
21.03.2010 09:1... fnaah 171,7500
PS C:\> $now = [System.DateTime]::Now
PS C:\> $month = [System.TimeSpan]::FromDays(30)
PS C:\> $old = $now - $month
PS C:\> Get-Basket | ? { $_.LastUpdated -lt $old } |
Remove-Basket
PS C:\> Get-Basket
LastUpdated Owner Total
----------- ----- -----
19.03.2010 20:5... ploeh 89,4000
21.03.2010 09:1... fnaah 171,7500
PS C:\>

Строка 5: Старая корзина

Строка 7-9: Рассчитывает дату закрытия

Строка 10-11: Удаляет старые корзины

Перед тем как вы начнете удалять корзины, вам хотелось бы просмотреть текущие корзины системы. Вы можете использовать пользовательский cmdlet Get-Basket для того, чтобы перечислить все корзины. Обратите внимание на то, что каждая корзина имеет три свойства, которые сообщают вам о том, когда корзина в последний раз обновлялась, кто является владельцем корзины, а также об общей стоимости (включая скидки) корзины.

Текущей датой выполнения этой конкретной сессии была 22 марта 2010 года. Обратите внимание на то, что второй корзине уже более 30 дней. Теперь вы можете рассчитать дату закрытия на основании текущей даты и использовать ее в выражении фильтра. Вы можете удалить все старые корзины путем передачи результата Get-Basket в фильтр, а затем передавая результат отфильтрованных корзин в cmdlet Remove-Basket. Если бы вы захотели выполнить фильтрацию по свойству Total, то вы также смогли бы это сделать тем же способом.

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

Примечание

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

Для продвижения этого API написания сценариев вам необходимо реализовать два пользовательских cmdlet'а. Поскольку одним из требований является то, что Total должен принимать во внимание все соответствующие бизнес-правила, вам необходимо компоновать cmdlet'ы в рамках уровня Domain Model.

Построение GetBasketCmdlet

Давайте рассмотрим, как реализуется cmdlet Get-Basket. Remove-Basket реализуется похожим способом, поэтому я не буду рассматривать его реализацию.

Для того чтобы избежать соблазна статического контейнера, вы будете реализовывать полноценный мост между cmdlet'ом PowerShell и Domain Model в отдельной библиотеке, которая носит название BasketPowerShellLogic. Рисунок 7-18 демонстрирует, как компонуется приложение в пределах библиотек.

Рисунок 7-18: Библиотека BasketPowerShell содержит только инфраструктуру, необходимую для того, чтобы осчастливить PowerShell – это humble-объект. Как только BasketContainer преобразовал BasketManager, все дальнейшие реализации происходят в других сборках. Класс BasketManager не имеет доступа к внутреннему BasketContainer, но использует IBasketService из Domain Model. Обычно стрелки обозначают указатели. Не все рассматриваемые классы продемонстрированы на рисунке.

Примечание

Если вы думаете, что рисунок 7-18 очень похож на рисунок 7-16, то вы начинаете понимать паттерн.

Примечание

Вы можете вспомнить IBasketService из главы 2, раздела 2.3.2.

Класс GetBasketCmdlet должен иметь конструктор по умолчанию для того, чтобы соответствовать PowerShell, поэтому вы используете его в качестве Composition Root и оставляете его в виде humble-объекта. Следующий листинг демонстрирует только то, насколько он "скромен".

Листинг 7-15: Реализация GetBasketCmdlet
[Cmdlet(VerbsCommon.Get, "Basket")]
public class GetBasketCmdlet : Cmdlet
{
	private readonly BasketManager basketManager;
	public GetBasketCmdlet()
	{
		this.basketManager =
			BasketContainer.ResolveManager();
	}
	protected override void ProcessRecord()
	{
		var baskets =
			this.basketManager.GetAllBaskets();
		this.WriteObject(baskets, true);
	}
}

Строка 7-8: Composition Root

Строка 12-13: Делегирование полномочий реализатору

В требуемом конструкторе по умолчанию вы используете статический контейнер для того, чтобы преобразовать BasketManager, который выступает в роли реализации. BasketManager использует Constructor Injection для запроса экземпляра IBasketService. К настоящему моменту вы должны уже хорошо знать этот паттерн, а также реализацию BasketContainer, продемонстрированную в следующем листинге.

Листинг 7-16: Преобразование BasketManager
internal static BasketManager ResolveManager()
{
	BasketRepository basketRepository =
		new SqlBasketRepository(
			BasketContainer.connectionString);
	DiscountRepository discountRepository =
		new SqlDiscountRepository(
			BasketContainer.connectionString);
	BasketDiscountPolicy discountPolicy =
		new RepositoryBasketDiscountPolicy(
			discountRepository);
	IBasketService basketService =
		new BasketService(basketRepository,
			discountPolicy);
	return new BasketManager(basketService);
}

Строка 1: Внутренний метод

Строка 15: Возвращает basket manager

Метод, так же, как и весь класс, является внутренним, что делает возможным вызов его из GetBasketCmdlet, как это продемонстрировано в листинге 7-15, но невозможно случайно использовать его из BasketManager или из его зависимостей.

Теперь реализация метода должна быть вам понятной. И снова я считаю, что проще всего отойти от результата. Для класса BasketManager необходим экземпляр IBasketService, и поэтому вы используете класс BasketService (других реализаций, которые вы могли бы выбрать, у вас нет).

Для BasketService необходимы BasketRepository и BasketDiscountPolicy. Для BasketDiscountPolicy вы используете RepositoryBasketDiscountPolicy. Для этого класса требуется еще одна абстракция репозитория, а для этих двух репозиториев вы используете реализации на основе SQL Server.

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

Remove-Basket cmdlet руководствуется тем же паттерном: он использует статический, но внутренний BasketContainer для того, чтобы преобразовать экземпляр BasketManager, а потом делегировать реализацию преобразованному экземпляру. Оба cmdlet'а выступают в роли сочетания Composition Root и humble-объекта.

Класс BasketManager реализуется в другой сборке. Как только код уходит от cmdlet'ов, риск, что какая-либо из основополагающих реализаций будет использовать статический контейнер в качестве Service Locator, исчезает, поскольку он является внутренним по отношению к сборке, содержащей cmdlet'ы.

Примечание

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

Фреймворк, подобный PowerShell, является самым DI-недружественным. Использование простой технологии превращения каждого элемента фреймворка в Composition Root и humble-объект дает вам простой способ решения этой проблемы.

или RSS канал: Что новенького на smarly.net