- PVSM.RU - https://www.pvsm.ru -
Случалось ли вам делать рефакторинг "толстых" контроллеров? Приходилось ли создавать многоэтажные модели представлений? Добавлять в представление данные и переписывать при этом код контроллера? Казалось ли вам, что что-то идёт неправильно?
Причина в том, что многие MVC-фреймворки не вполне следуют шаблону MVC, а люди, использующие их, сами того не замечая, ещё больше отклоняются от него. Казалось бы, он довольно прост, и описан в Википедии [1], но раз за разом возникают проблемы его понимания.
Взгляните на это классическое действие "толстого" контроллера:
public ActionResult AddComment(int articleId, string content)
{
// бизнес-логика
var article = ArticleRepository.Instance.GetById(articleId);
if (article == null)
return View("ArticleNotFound");
var comment = new Comment
{
ArticleId = articleId,
Content = content
};
CommentRepository.Instance.Add(comment);
//формирование модели представления
var viewModel = new ArticleViewModel
{
Article = article,
Comments = CommentRepository.Instance.GetByArticleId(articleId)
};
return View("Article", viewModel);
}
И на этот шаблон представления:
@Model ArticleViewModel
<header><h1>@Model.Article.Title</h1></header>
<article>@Model.Article.Content</article>
<ul>
@foreach (var comment in Model.Comments)
{
<li>@comment.Content</li>
}
</ul>
Какие здесь есть проблемы?
И такие ситуации возникают не только в проектах на ASP.NET MVC, а почти на любых MVC-фреймворках. Чтобы бороться с ними, нужно их понять.
Очень часто моделью называют классы, отображающиеся на сущности схемы БД. Однако в рамках MVC этот термин имеет другой смысл. Model — это модель предметной области, содержащая всю бизнес-логику приложения. В том числе сущности, различные сервисы, репозитории, фабрики и многое другое. Модель хранит состояние и может его изменять в зависимости от действий над ней.
Часть функционала модели (или весь) может быть даже реализована с помощью внешних сервисов, но с точки зрения контроллера и представления это не важно: модель предоставляет им API: классы данных и сервисы, имеющие методы для каждого действия, которое мы можем совершить с моделью. Модель может существовать в отрыве от нашего веб-приложения, и поэтому её обычно стоит реализовывать в виде одного или нескольких отдельных проектов.
В контексте нашего примера стоит создать класс, предоставляющий возможность управления статьями (в частности, добавление комментария) и, желательно, соответствующий интерфейс:
public interface IArticleService
{
// Другие методы
// Об успешности операции мы можем узнавать либо из исключений,
// которые может выбросить этот метод, либо из возвращаемого им значения.
void AddComment(string commentContent);
}
В веб-разработке часто под View понимается шаблон со специальной разметкой, куда должны быть подставлены данные, предоставленные ему контроллером. При этом он сам не обращается к модели предметной области и не получает оттуда данные. Это приводит к тому, что контроллер должен подготовить для него данные в специальном виде (это вынуждает его нарушать принцип единственной ответственности [2]). А если нужно добавить какую-то информацию в шаблон, приходится менять свойства модели представления и код контроллера или провайдера модели представления (если таковой используется).
Модель представления при этом является обособленным классом, и наполняется не связанными по смыслу свойствами, совокупности которых нельзя даже дать связного названия, кроме как %PageName%ViewModel
(ещё один плохой признак).
Но в описании MVC говорится, что представление имеет связь с моделью, и может запрашивать у неё данные, а некоторых случаях даже менять состояние модели! (Но от последнего, по моему мнению, по возможности лучше отказаться.)
Если следовать этому правилу, для передачи данных в представление не понадобится модель представления с большим количеством свойств. Скорее всего её вообще не будет, или это будет идентификатор необходимой сущности или сама сущность в крайнем случае. Остальные данные, связанные с ней, представление должно получить само через предоставленные через внедрение зависимости сервисы, принадлежащие Model.
Если многим представлениям приходится схожим образом обрабатывать данные перед показом, могут быть созданы другие классы, помогающие им в этом. Это могут быть не только "Helper"-классы. И, хоть они и не будут шаблонами разметки, они все равно будут относиться к слою View.
Контроллер, а в частности его действие (в веб-разработке) является конечной точкой маршрутизации запроса. Всё, что должно сделать действие, это:
(В некоторых вариантах MVC с активной моделью, не относящихся к веб-разработке, может отсутствовать последний пункт, так как представление подписывается на изменения в модели, и изменяется автоматически.)
Если придерживаться этого, контроллеры будут оставаться действительно тонкими, и вам не придется для этого делать уловки вроде %PageName%ViewModelProvider
, чтобы собрать нужные данные для представления.
Хорошо. Но для того, чтобы следовать всему этому, нужно внедрить зависимости в контроллеры и представления. Как это сделать в ASP.NET MVC? Широко распространена практика внедрения в контроллер через свойства. Эту возможность предоставляют многие библиотеки вроде Autofac или Ninject, но есть и другие варианты, которые следует знать.
Внедрить зависимость в контроллер через конструктор можно двумя способами:
В основной линейке фреймворков ASP.NET MVC нет возможности внедрить зависимости в конечный класс представления, так как этот класс не существует до тех пор, пока оно в первый раз не потребуется. В этот момент на лету будет разобран код шаблона, и создастся наследник класса WebViewPage
c перегруженным методом Execute
из одного из предков — WebPageExecutingBase
, содержащим сложный код генерации ответа по шаблону.
Но есть возможность отнаследовать этот класс не от WebViewPage
, а от собственного класса с помощью директивы @inherits
в начале кода шаблона:
(новый класс-предок представления, файл можно разместить рядом с шаблоном)
using System.Web.Mvc;
namespace ASP
{
// здесь я упростил параметр типа ArticleViewModel
// до int-идентификатора статьи
public abstract class ArticlePageBase: WebViewPage<int>
{
// Autofac не требует дополнительных атрибутов для внедрения,
// но другие инструменты могут требовать.
// Источник, из которого представление получит статью
public IArticleRepository ArticleRepository { get; set; }
// Источник, из которого представление получит комментарии к ней
public ICommentRepository CommentRepository { get; set; }
// Рекомендательный сервис, выдающий список статей,
// имеющих сходство с текущей
public IArticleRecomendationService RecomendationService { get; set; }
}
}
(Razor-шаблон представления)
@inherits ArticlePageBase
@{
// получение нужных данных из предоставленных сервисов
var article = ArticleRepository.GetById(Model);
var comments = CommentRepository.GetByArticleId(Model);
var recommendedArticles = RecomendationService.GetRecomendations(Model);
}
@*Вывод полученных данных*@
К сожалению, здесь невозможно сделать внедрение через конструктор, так как генерируемый наследник не будет его использовать, что вызовет исключение во время выполнения. Но есть возможность внедрить зависимости через свойства. Это может сделать Autofac [6] и, возможно, другие инструменты.
Логику получения и подготовки данных для отображения, если она слишком сложна, можно написать, перегрузив метод InitializePage
предка представления, и сохранив полученные данные в защищённые поля, которые можно использовать в шаблоне:
public abstract class ArticlePageBase: WebViewPage<int>
{
public IArticleRepository ArticleRepository { get; set; }
// Свойство, используемое в шаблоне для отображения данных
protected Article Article;
protected override void InitializePage()
{
Article = ArticleRepository.GetById(Model);
}
}
@inherits ArticlePageBase
<h1>@Article.Title</h1>
или прямо в шаблоне в блоке C#-кода (что на мой взгляд даже лучше, если нет необходимости писать дополнительные методы для преобразования данных модели в подходящий формат):
@inherits ArticlePageBase
@{
// получение нужных данных из предоставленных сервисов
var article = ArticleRepository.Get(Model);
var comments = CommentRepository.GetByArticleId(Model);
var recommendedArticles = RecomendationService.GetRecomendations(Model);
}
<h1>@ArticleService.Get(Model).Name</h1>
<header><h1>@article.Title</h1></header>
<article>@article.Content</article>
<ul>
@foreach (var comment in comments)
{
<li>@comment.Content</li>
}
</ul>
<ul>
@foreach (var recommendedArticle in recommendedArticles)
{
// Вывод ссылки на рекомендуемую статью
}
</ul>
Так же в базовом классе этого представления можно описать несколько защищенных методов для обработки данных, которые можно потом использовать в шаблоне.
Движок Razor в ASP.NET MVC Core позволяет [7] внедрить зависимость с помощью директивы @inject
в начале шаблона. На самом деле происходит всё то же создание свойства, но уже не в предке представления, а в самом его классе, генерируемом движком:
@model int
@inject IArticleRepository ArticleRepository
@{
var article = ArticleRepository.GetById(Model)
}
<h1>@article.Title</h1>
Но для этого необходимо, чтобы этот интерфейс был зарегистрирован в методе ConfigureServices
класса Startup
:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Некоторые действия
services.AddTransient<IArticleRepository, ArticleRepository>();
// Некоторые действия
}
Используя эти техники, можно упростить код действия контроллера до следующего:
public ActionResult AddComment(int articleId, string content)
{
// Вместо исключений можно анализировать
// возвращаемое сервисом значение. Это не принципиально.
try
{
ArticleService.AddComment(articleId, content);
}
catch (ArticleNotFoundException e)
{
return View("ArticleNotFound");
}
return View("Article", articleId);
}
Теперь он действительно тонкий. На него можно даже написать лаконичные модульные тесты. Вся бизнес-логика находится в модели, а все, что относится к показу — в представлении.
Сам шаблон MVC в контексте веб-разработки тоже имеет некоторые недостатки. Обработчики запросов объединяются в виде методов-действий внутри одного класса-контроллера. Это ведёт к тому, что для того, чтобы совершить одно действие, нужно проинициализировать весь контроллер, внедрив в него зависимости, необходимые для всех его действий. Это тоже нарушение SRP. Так же часто возникает проблема правильной группировки действий по контроллерам.
Пол М. Джонс [8] предложил альтернативный шаблон — ADR [9] (реализованный на PHP), некое подобие которого можно встретить в других библиотеках. У этого шаблона есть преимущества перед MVC:
Остается ждать или попробовать самим реализовать ADR-фреймворк на .NET.
Всем хорошего кода и тонких контроллеров!
MVC на Википедии [1]
Принцип единственной ответственности (SRP) на Википедии [2]
Внедрение зависимости в контроллер через конструктор с помощью Autofac [3]
Внедрение зависимости в контроллер через конструктор с помощью Castle Windsor [4]
Создание собственной фабрики контроллеров [5]
Внедрение зависимости в представление через свойства с помощью Autofac [6]
Внедрение зависимости в представление в ASP.NET MVC Core [7]
Статья про ADR на Хабрахабре [10]
ADR на Википедии [11]
Автор: Николай Лебедев
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/asp-net-mvc/268677
Ссылки в тексте:
[1] Википедии: https://ru.wikipedia.org/wiki/Model-View-Controller
[2] принцип единственной ответственности: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%BD%D1%86%D0%B8%D0%BF_%D0%B5%D0%B4%D0%B8%D0%BD%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D0%B9_%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8
[3] Используя Autofac: https://autofac.readthedocs.io/en/latest/integration/mvc.html#register-controllers
[4] другой инструмент: https://www.codeproject.com/Tips/1052382/ASP-NET-MVC-Dependency-Injection-using-Windsor
[5] Написав свою собственную фабрику контроллеров: https://www.codeproject.com/Tips/732449/Understanding-and-Extending-Controller-Factory-i
[6] может сделать Autofac: https://autofac.readthedocs.io/en/latest/integration/mvc.html#enable-property-injection-for-view-pages
[7] позволяет: https://docs.microsoft.com/en-us/aspnet/core/mvc/views/dependency-injection
[8] Пол М. Джонс: https://github.com/pmjones
[9] ADR: http://pmjones.io/adr/
[10] Статья про ADR на Хабрахабре: https://habrahabr.ru/post/260769/
[11] ADR на Википедии: https://en.wikipedia.org/wiki/Action%E2%80%93domain%E2%80%93responder
[12] Источник: https://habrahabr.ru/post/342748/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.