Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ASP_NET_MVC_4_Framework_s_primerami_na_C_dlya_p

.pdf
Скачиваний:
25
Добавлен:
19.03.2016
Размер:
17.66 Mб
Скачать

Это большой шаг вперед. Трехуровневая архитектура является наиболее широко используемым паттерном для бизнес-приложений. Она не имеет ограничений относительно того, как реализован пользовательский интерфейс, и обеспечивает хорошее разделение понятий, не будучи слишком сложной. Также DAL может быть создан таким образом, что юнит тестирование будет относительно легким. Вы можете увидеть очевидное сходство между классическим трехуровневым приложением и MVC паттерном. Разница состоит в том, что поскольку UI слой напрямую соединен с «click-and-event» GUI фреймворком (таким как Windows Forms и ASP.NET Web Forms), становится почти невозможным выполнение автоматизированных юнит тестов. И поскольку UI часть трехуровневого приложения может быть очень сложной, мы имеем много кода, который не может быть тщательно протестирован.

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

Вариации MVC

Мы уже изучили основные принципы создания MVC приложений, и особенно то, как они относятся к реализации ASP.NET MVC. Другие интерпретируют аспекты паттерна по-другому и корректируют или иным образом адаптируют MVC для своих проектов. В следующих разделах мы представим краткий обзор двух наиболее распространенных вариантов на тему MVC. Понимание этих вариантов не является необходимым для работы с ASP.NET MVC. Мы включили эту информацию для полноты.

Model-View-Presenter паттерн

MVP является вариацией MVC, который разработан, чтобы соответствовать GUI платформам,

сохраняющим состояние (stateful), таким как Windows Forms или ASP.NET Web Forms, и это стоящая попытка представить лучшие аспекты Smart UI без тех проблем, которые он обычно приносит.

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

Реализация пассивного представления, в котором представление не содержит логики – это контейнер для элементов управления UI, которые напрямую управляются презентером.

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

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

Model-View-View Model паттерн

MVVM паттерн является самой последней вариации MVC. Он возник в 2005 году в команде

Microsoft, которая разрабатывала технологию, ставшую Windows Presentation Foundation (WPF) и Silverlight.

51

В MVVM паттерне модели и представления играют те же роли, что и в MVC. Разница состоит в MVVM концепции модели представления, которая является абстрактным представлением пользовательского интерфейса. Как правило, это C# класс, который раскрывает оба свойства для данных, которые будут отображаться в интерфейсе, и операции по данным, которые могут быть вызваны из пользовательского интерфейса. В отличие от MVC контроллера, модель представления MVVM не имеет понятия, что существует представление (или любая конкретная UI технология). MVVM представление использует связывающую функцию WPF/Silverlight для двунаправленного связывания свойств, предоставляемых элементами управления в представлении (пункты в выпадающем меню или результат нажатия кнопки), со свойствами, предлагаемыми моделью представления.

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

Совет

MVC также использует термин модель представления, но он относится к простому классу модели, который используются только для передачи данных из контроллера в представление. Мы должны четко различать модель представления и доменную модель, которая является сложным представлением данных, операций и правил.

Проблемно-ориентированное программирование (DDD)

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

ASP.NET MVC не диктует того, какая технология должна использоваться для доменной модели. Вы вольны выбрать любую технологию, которая будет взаимодействовать с .NET Framework, и тут есть много вариантов. Тем не менее, ASP.NET MVC предлагает инфраструктуру и соглашения, чтобы помочь подключить классы доменной модели к контроллерам и представлениям, а также к самому MVC Framework. Есть три ключевые функциональные возможности:

Связывание данных модели является функцией, которая автоматически заполняет объекты модели, используя входные данные, как правило, отправленные из HTML формы.

Метаданные модели позволяют описать фреймворку смысл классов модели. Например, вы можете предоставить читабельное описание их свойств или дать подсказки о том, как они должны отображаться. MVC Framework может автоматически представить изображение или редактор UI для классов модели в представлениях.

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

Мы кратко коснулись связывания данных и валидации, когда мы строили наше первое MVC приложение в главе 2, и мы вернемся к этим темам и дальнейшему их изучению в главах 22 и 23. На данный момент, мы собираемся отложить реализацию ASP.NET MVC в сторону и подумать о доменном моделировании как о самостоятельной деятельности. Мы собираемся создать простую

52

доменную модель с помощью .NET и SQL Server, используя некоторые основные технологические приемы из мира DDD.

Построение доменной модели

У вас, наверное, уже был опыт мозгового штурма по созданию доменной модели. Обычно сюда включены разработчики, бизнес эксперты, большое количество кофе, печенья и ручки с маркерами. Через некоторое время люди в комнате приходят к начальному общему знаменателю, и тогда возникает первый проект доменной модели. (Мы опускаем рассказ о многочасовых разногласиях. Достаточно сказать, что разработчики будут тратить первые часы, требуя от бизнес экспертов реальных задач, а не взятых из научной фантастики, в то время как бизнес эксперты будут выражать удивление и обеспокоенность, что время и смета расходов аналогичны тем, что NASA требует, чтобы достичь Марса. Кофе имеет важное значение в решении таких противоречий и в таком противостоянии, в конечном итоге, мочевой пузырь у всех будет настолько полным, что все пойдут на компромисс, и наметится прогресс в решении задачи).

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

Рисунок 3-5: Первый набросок модели для аукционного приложения

Эта модель содержит набор элементов Members, каждый из которых содержит набор элементов Bids. Каждый Bid предназначен для одного Item, а каждый Item может содержать несколько Bid от разных Members.

Повсеместно используемый язык

Ключевым преимуществом реализации вашей доменной модели в качестве отдельного компонента является то, что вы можете установить по своему выбору язык и терминологию. Вам стоит попытаться найти терминологию для ее объектов, операций и отношений, которые будут понятны не только разработчикам, но и бизнес экспертам. Мы рекомендуем вам адаптировать доменную терминологию, когда модель уже существует. Например, если то, что разработчик будет называть пользователями и ролями (users и roles), известно в домене как агенты и разрешения (agents и clearances), то мы рекомендуем вам принять последний вариант в вашей доменной модели.

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

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

53

Этот подход также помогает избежать в приложении сверхобобщения. Программисты, как правило, зачастую хотят смоделировать все возможные реалии бизнеса, а не конкретно то, что требует бизнес. В модели аукциона такое могло бы произойти, если бы мы изменили members и items общими понятиями resources и relationships. Когда мы создаем доменную модель, которая не совсем соответствует моделируемому домену, мы упускаем возможность получить любую реальную картину бизнес процесса и, в будущем, представление бизнес процесса может оказаться неправильным. Эти ограничения не являются запретом, они являются маяком, который направляет вашу работу в правильное русло.

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

Агрегаты и упрощение

На рисунке 3-5 представлена хорошая отправная точка для нашей доменной модели, но тут нет никаких полезных указаний о реализации модели с использованием C# и SQL Server. Если мы загрузим в память элемент Member, должны ли мы также загрузить Bids и Items, связанные с ним? И если это так, нужно ли нам загрузить все другие Bids для этих Items, а также Members, которые связаны с этими Bids? Когда мы удаляем объект, должны ли мы удалить также связанные с ним объекты, и если да, то какие? Если мы выберем для реализации хранилище документов вместо реляционной базы данных, какая коллекция объектов будет представлять собой единый документ? Мы этого не знаем, а наша доменная модель не дает нам никаких ответов на эти вопросы.

В данном случае DDD предлагает организовать доменные объекты в группы, называемые агрегатами. На рисунке 3-6 показано, как мы можем объединить объекты в нашей доменной модели аукциона.

Рисунок 3-6: Аукционная доменная модель с агрегатами

54

Суть агрегата заключается в группировке нескольких объектов доменной модели: есть ключевая сущность (root entity), которая используется для определения цельного агрегата, и она действует как «босс» для валидации и сохранения операций. Агрегат рассматривается как единое целое с точки зрения изменения данных, поэтому мы должны создать агрегаты, которые представляют собой отношения, имеющие смысл в контексте доменной модели, и создать операции, которые логически соответствуют реальным бизнес процессам: иными словами, мы должны создавать агрегаты, группируя объекты, которые изменяются как группа.

Ключевое правило DDD гласит, что объекты за пределами конкретного экземпляра агрегата могут быть связаны только с root entity, а не с любым другим объектом внутри агрегата (на самом деле, идентичность не корневого объекта должна быть уникальной только в пределах своего агрегата). Это правило подтверждает представление о работе с объектами внутри агрегата как с единым целым.

В нашем примере Members и Items являются ключевыми сущностями агрегата, в то время как Bids могут быть доступны только в контексте элемента Item, который является корневым в своем агрегате. Элементы Bids могут быть связаны с элементами Members (которые являются основными сущностями, root entity), но Members не могут непосредственно ссылаться на Bids (поскольку они не являются корневыми).

Одно из преимуществ агрегатов состоит в том, что они упрощает систему отношений между объектами в доменной модели, и часто они могут дать дополнительное понимание природы домена, который моделируется. В сущности, создание агрегатов ограничивает отношения между объектами доменной модели, так что они становятся больше похожими на отношения, которые существуют в реальном домене. В листинге 3-1 показано, как выглядит наша доменная модель, выраженная в C#.

Листинг 3-1: Аукционная доменная модель, выраженная в C#

public class Member

{

public string LoginName { get; set; } // Уникальный ключ public int ReputationPoints { get; set; }

}

public class Item

{

public int ItemID { get; private set; } // Уникальный ключ public string Title { get; set; }

public string Description { get; set; } public DateTime AuctionEndDate { get; set; } public IList<Bid> Bids { get; set; }

}

public class Bid

{

public Member Member { get; set; } public DateTime DatePlaced { get; set; } public decimal BidAmount { get; set; }

}

Обратите внимание, как мы легко смогли понять однонаправленный характер отношений между Bids и Members. Мы также смогли смоделировать некоторые другие ограничения, например, элементы Bids остаются неизменными (это представляет собой общую стратегию аукциона, когда ставки не могут быть изменены, как только они сделаны). Применение агрегации позволило нам создать более полезную и точную доменную модель, которую мы смогли с легкостью представить в C#.

55

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

С другой стороны, они накладывают ограничения, которые иногда могут показаться искусственными, потому что зачастую они и есть искусственные. Агрегаты обычно появляются в базах данных документа, но они не являются родным понятием в SQL Server и в большинстве инструментов ORM. Поэтому если вы хотите их хорошо реализовать, вашей команде понадобится дисциплина и эффективный обмен информацией.

Определение репозиториев

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

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

Соглашение заключается в определение отдельной модели данных для каждого агрегата, потому что агрегаты являются естественными единицами для сохранения постоянства. В случае нашего аукциона, например, мы можем создать два репозитория: один для Members и один для Items (обратите внимание, что нам не нужен репозиторий для Bids, потому что они будут сохранены как часть агрегата Items). В листинге 3-2 показано, как могут быть определены эти репозитории.

Листинг 3-2: C# классы репозиториев для доменных классов Member и Item

public class MembersRepository

{

public void AddMember(Member member)

{

/* Реализуй меня */

}

public Member FetchByLoginName(string loginName)

{

/* Реализуй меня */ return null;

}

public void SubmitChanges()

{

/* Реализуй меня */

}

}

public class ItemsRepository

{

public void AddItem(Item item)

{

56

/* Реализуй меня */

}

public Item FetchByID(int itemID)

{

/* Реализуй меня */ return null;

}

public IList<Item> ListItems(int pageSize, int pageIndex)

{

/* Реализуй меня */ return null;

}

public void SubmitChanges()

{

/* Реализуй меня */

}

}

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

Строительство слабосвязанных компонентов

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

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

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

IEmailSender.

Любой другой компонент нашего приложения, которому нужно отправить имейл, скажем, например, вспомогательный метод сброса пароля PasswordResetHelper, может отправить электронную почту, только ссылаясь на методы интерфейса. Как показано на рисунке 3-7, между

PasswordResetHelper и MyEmailSender не существует прямой зависимости.

Рисунок 3-7: Использование интерфейсов для разделения компонентов

Вводя IEmailSender, мы гарантируем, что не существует прямой зависимости между

PasswordResetHelp и MyEmailSender. Мы могли бы заменить MyEmailSender другим

57

компонентом для отправки электронной почты или даже использовать в целях тестирования mockобъекты. Мы вернемся к теме mock-объектов в главе 6.

Примечание

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

Использование внедрения зависимости (Dependency Injection)

Интерфейсы помогают нам разделять компоненты, но у нас по-прежнему есть проблема: C# не предоставляет встроенного способа для легкого создания объектов, реализующих интерфейсы, за исключением создания интерфейсов конкретного компонента. В конечном итоге, у нас есть код, показанный в листинге 3-3.

Листинг 3-3: Создание экземпляра конкретного класса для осуществления реализации интерфейса

public class PasswordResetHelper { public void ResetPassword() {

IEmailSender mySender = new MyEmailSender();

//...вызов методов интерфейса для конфигурации информации по имейлу mySender.SendEmail(); }}

Мы всего лишь частично на пути к слабосвязанным компонентам: класс PasswordResetHelper настраивает и отправляет электронную почту через интерфейс IEmailSender, но для создания объекта, который реализует этот интерфейс, он должен был создать экземпляр MyEmailSender.

Мы сделали еще хуже, теперь PasswordResetHelper зависит от IEmailSender и MyEmailSender,

как показано на рисунке 3-8.

Рисунок 3-8: Компоненты, которые все равно тесно связаны

Что нам нужно, так это способ получить объекты, которые реализуют данный интерфейс без необходимости создания объекта напрямую. Решение этой проблемы называется внедрением зависимости (Dependency injection, DI), известное также как инверсия управления (Inversion of Control, IoC).

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

58

Есть два варианта DI паттерна. Первый состоит в том, что мы удаляем все зависимости от конкретных классов из наших компонентов, в данном случае PasswordResetHelper. Это делается путем передачи реализации необходимых интерфейсов в конструктор класса, как показано в листинге 3-4.

Листинг 3-4: Удаление зависимостей из класса PasswordResetHelper

public class PasswordResetHelper

{

private IEmailSender emailSender;

public PasswordResetHelper(IEmailSender emailSenderParam)

{

emailSender = emailSenderParam;

}

public void ResetPassword()

{

// ...вызов методов интерфейса для конфигурации информации по имейлу...

emailSender.SendEmail();

}

}

Мы сломали зависимость между PasswordResetHelper и MyEmailSender: конструктор PasswordResetHelper требует объект, который реализует интерфейс IEmailSender, но он не знает, или его не волнует, что это за объект и он больше не ответственен за его создание.

Зависимости внедряются в PasswordResetHelper во время выполнения, то есть будет создан экземпляр класса, реализующего интерфейс IEmailSender, и передан конструктору PasswordResetHelper при создании экземпляра. Тут не существует зависимости от времени компиляции между PasswordResetHelper и любым классом, который реализует интерфейсы, от которых он зависит.

Примечание

Класс PasswordResetHelper требует того, чтобы его зависимости были

внедрены с помощью его конструктора – это известно как constructor injection (внедрение через конструктор). Мы могли бы также сделать так, чтобы зависимости были внедрены через открытые свойства, что известно как setter injection (внедрение через свойства).

Поскольку с зависимостями работают во время выполнения, мы можем решить, какие реализации интерфейса будут использоваться при запуске приложения: мы можем выбирать между различными компонентами отправки электронной почты или вставлять mock-объекты для тестирования.

DI пример с MVC

Давайте вернемся к аукционной доменной модели, которую мы создали ранее, и применим к ней DI. Целью является создание класса контроллера, мы назовем его AdminController. Он использует репозиторий MembersRepository, но без непосредственной связи AdminController и MembersRepository. Мы начнем с определения интерфейса, который разделит наши два класса – мы назовем его IMembersRepository – и изменим класс MembersRepository для реализации интерфейса, как показано в листинге 3-5.

59

Листинг 3-5: Интерфейс IMembersRepository

public interface IMembersRepository

{

void AddMember(Member member);

Member FetchByLoginName(string loginName); void SubmitChanges();

}

public class MembersRepository : IMembersRepository

{

public void AddMember(Member member)

{

/* Реализуй меня */

}

public Member FetchByLoginName(string loginName)

{

/* Реализуй меня */

}

public void SubmitChanges()

{

/* Реализуй меня */

}

}

Теперь мы можем написать класс контроллера, который зависит от интерфейса

IMembersRepository, как показано в листинге 3-6.

Листинг 3-6: Класс AdminController

public class AdminController : Controller

{

IMembersRepository membersRepository;

public AdminController(IMembersRepository repositoryParam)

{

membersRepository = repositoryParam;

}

public ActionResult ChangeLoginName(string oldLoginParam, string newLoginParam)

{

Member member = membersRepository.FetchByLoginName(oldLoginParam); member.LoginName = newLoginParam; membersRepository.SubmitChanges();

// ... теперь будет показано представление return View();

}

}

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

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

Мы решили вопрос с зависимостью: мы собираемся внедрить наши зависимости в конструкторы наших классов во время выполнения. Но нам все равно нужно решить еще один вопрос – как мы создадим экземпляр конкретной реализации интерфейса без создания зависимостей еще гденибудь в нашем приложении?

60

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]