Каков наилучший способ рефакторингового кода презентации из моих объектов домена в решении ASP.NET MVC?
Я только что взял проект ASP.NET MVC, и требуется некоторое рефакторинг, но я хотел получить некоторые советы/рекомендации для лучших практик.
У сайта есть сервер SQL Server, и здесь представлен обзор проектов внутри решения:
- DomainObjects (один класс для каждой таблицы базы данных)
- DomainORM (код преобразования из объектов в БД)
- Модели (бизнес-логика)
- MVC (обычная веб-настройка ASP.NET MVC)
---- Контроллеры
---- ViewModels
---- Просмотры
---- Сценарии
Первая "проблема", которую я вижу, заключается в том, что в то время как классы объектов Domain в значительной степени POCO с некоторыми дополнительными свойствами "получить" при вычислении поля, в объектах домена есть код представления. Например, внутри проекта DomainObjects существует объект Person, и я вижу это свойство в этом классе:
public class Person
{
public virtual string NameIdHTML
{
get
{
return "<a href='/People/Detail/" + Id + "'>" + Name + "</a> (" + Id + ")";
}
}
}
поэтому очевидно, что содержимое HTML-содержимого внутри объекта домена кажется неправильным.
Подходы к рефактору:
-
Мой первый инстинкт состоял в том, чтобы переместить это в класс ViewModel внутри проекта MVC, но я вижу, что есть много просмотров, которые попадают в этот код, поэтому я не хочу дублировать код в каждой модели представления.
-
Вторая идея заключалась в создании класса PersonHTML, который был либо:
2а. Обертка, которая взяла в Person в конструкторе или
2b. Класс, унаследованный от Person, а затем имеет все эти методы рендеринга HTML.
Модель представления преобразует любой объект Person в объект PersonHTML и использует его для всего кода рендеринга.
Я просто хотел увидеть:
-
Если здесь есть лучшая практика, так как кажется, что это общая проблема/шаблон, который появляется
-
Насколько плохо это текущее состояние считается, потому что, помимо чувства неправильности, это не вызывает серьезных проблем с пониманием кода или созданием плохих зависимостей. Любые аргументы, которые помогут описать, почему оставить код в этом состоянии плох с реального практического смысла (в сравнении с теоретическим разделением аргументов в отношении проблем), будут полезны, а также есть дискуссия в команде, стоит ли ее менять.
Ответы
Ответ 1
Мне нравится комментарий TBD. Это неправильно, потому что вы смешиваете проблемы домена с проблемами пользовательского интерфейса. Это вызывает сцепление, которое можно было бы избежать.
Что касается ваших предлагаемых решений, мне действительно не нравятся их.
-
Представление модели представления. Да, мы должны использовать модели просмотра, но мы
не хотят загрязнять их HTML-кодом. Итак, пример использования
view будет, если у вас есть родительский объект, тип человека, и вы
хотите показать тип человека на экране. Вы должны заполнить вид
модель с именем типа лица, а не объектом типа полного лица
потому что вам нужно только имя типа человека на экране. Или если
ваша модель домена имела имя и фамилию отдельно, но ваш взгляд
вызывает FullName, вы должны заполнить модель View FullName и
верните это в представление.
-
Класс PersonHtml. Я даже не знаю, что это будет делать. Представления представляют собой представление HTML в приложении ASP.NET MVC. У вас есть два варианта:
а. Вы можете создать шаблон отображения для своей модели. Здесь ссылка на вопрос Qaru для отображения шаблонов Как создать шаблон отображения в проекте MVC 4
б. Вы также можете написать метод HtmlHelper, который будет генерировать правильный HTML для вас. Что-то вроде @Html.DisplayNameLink(...) Это были бы ваши лучшие варианты. Здесь ссылка для понимания HtmlHelpers https://download.microsoft.com/download/1/1/f/11f721aa-d749-4ed7-bb89-a681b68894e6/ASPNET_MVC_Tutorial_9_CS.pdf
Ответ 2
Я сам боролся с этим. Когда у меня был код, который был более логичным, чем HTML, я создал расширенную версию HtmlBuilder. Я расширил некоторые объекты домена, чтобы автоматически распечатать этот помощник, с его содержимым, основанным на функциях домена, которое затем можно было просто распечатать на виде. Однако код становится очень загроможденным и нечитаемым (особенно, когда вы пытаетесь выяснить, откуда оно взялось); по этим причинам я предлагаю удалить как можно больше логики представления/представления из домена.
Однако после этого я решил еще раз взглянуть на шаблоны отображения и редактора. И я стал ценить их больше, особенно в сочетании с T4MVC, FluentValidation и обычными поставщиками метаданных, среди прочего. Я нашел использование HtmlHelpers и расширение метаданных или таблицы маршрутизации на гораздо более чистый способ делать что-то, но вы также начинаете играть с системами, которые менее документированы. Однако этот случай относительно прост.
Итак, во-первых, я бы удостоверился, что у вас есть маршрут, определенный для этого объекта, который похож на ваш маршрут по умолчанию MVC, поэтому вы можете просто сделать это в представлении:
//somewhere in the view, set the values to the desired value for the person you have
@{
var id = 10; //random id
var name = "random name";
}
//later:
<a href="@Url.Action("People", "Detail", new { id = id })"> @name ( @id )</a>
Или, T4MVC:
<a href="@Url.Action(MVC.People.Detail(id))"> @name ( @id )</a>
Это означает, что в отношении видов/режимов просмотра единственная зависимость, которую они имеют, это id
и name
Person
, которые я бы предположил, что ваши существующие модели представлений должны иметь (удаление этого уродливого var id = x
сверху):
<a href="@Url.Action("People", "Detail", new { id = Model.PersonId } )">
@Model.Name ( @Model.PersonId )
</a>
Или, с T4MVC:
<a href="@Url.Action( MVC.People.Detail( Model.PersonId ) )">
@Model.Name ( @Model.PersonId )
</a>
Теперь, как вы сказали, несколько просмотров потребляют этот код, поэтому вам нужно будет изменить представления, чтобы они соответствовали вышеизложенному. Есть и другие способы сделать это, но каждое предложение, которое у меня есть, потребует изменения взглядов, и я считаю, что это самый чистый способ. Это также имеет функцию использования таблицы маршрутов, а это означает, что если система маршрутизации будет обновлена, обновленный URL-адрес будет распечатан здесь без забот, в отличие от жесткого кодирования его в объекте домена в качестве URL-адреса (который зависит от маршрутной системы, которая была настроена определенным образом для работы этого URL-адреса).
Одним из моих других предложений было бы создать Html Helper, называемый Html.LinkFor( c => model )
или что-то в этом роде, но если вы не хотите, чтобы он динамически определял контроллер/действие, основанное на типе, это является ненужным.
Ответ 3
Насколько плохо это текущее состояние считается, потому что, помимо чувства неправильности, это не вызывает серьезных проблем с пониманием кода или созданием каких-либо плохих зависимостей.
Текущее состояние очень плохое, не только потому, что код UI включен в код домена. Это было бы уже неплохо, но это хуже. Свойство NameIdHTML
возвращает жесткосвязную ссылку на страницу пользовательского интерфейса пользователя. Даже в коде пользовательского интерфейса вы не должны жестко кодировать эти типы ссылок. Для этого нужны LinkExtensions.ActionLink
и UrlHelper.Action
.
Если вы измените свой контроллер или ваш маршрут, ссылка изменится. LinkExtensions
и UrlHelper
знают об этом, и вам не нужны дальнейшие изменения. Когда вы используете жесткую ссылку, вам нужно найти все места в коде, где такая ссылка жестко запрограммирована (и вам нужно знать, что эти места существуют). Чтобы еще больше ухудшить ситуацию, код, который вам нужно изменить, находится в бизнес-логике, которая находится в противоположном направлении от цепочки зависимостей. Это кошмар для обслуживания и главный источник ошибок. Вам нужно изменить это.
Если здесь есть лучшая практика, так как кажется, что это общая проблема/шаблон, который появляется.
Да, существует наилучшая практика и использует упомянутые методы LinkExtensions.ActionLink
и UrlHelper.Action
всякий раз, когда вам нужна ссылка на страницу, возвращаемую действием контроллера. Плохая новость заключается в том, что это означает изменения в нескольких точках вашего решения. Хорошей новостью является то, что легко найти эти места: просто удалите свойство NameIdHTML
и появятся ошибки. Если вы не получаете доступ к собственности путем отражения. В этом случае вам потребуется выполнить более тщательный поиск кода.
Вам нужно будет заменить NameIdHTML
кодом, который использует LinkExtensions.ActionLink
или UrlHelper.Action
для создания ссылки. Я предполагаю, что NameIdHTML
возвращает HTML-код, который должен использоваться всякий раз, когда этот человек должен отображаться на HTML-странице. Я также предполагаю, что это общий шаблон в вашем коде. Если мое предположение верно, вы можете создать вспомогательный класс, который преобразует бизнес-объекты в их представления HTML. Вы можете добавить методы расширения к этому классу, которые предоставят HTML-представление ваших объектов. Чтобы я понял, я предполагаю (гипотетически), что у вас есть класс Department
, который также имеет Name
и Id
и имеет аналогичное представление HTML. Затем вы можете перегрузить ваш метод конверсии:
public static class BusinessToHtmlHelper {
public static MvcHtmlString FromBusinessObject( this HtmlHelper html, Person person) {
string personLink = html.ActionLink(person.Name, "Detail", "People",
new { id = person.Id }, null).ToHtmlString();
return new MvcHtmlString(personLink + " (" + person.Id + ")");
}
public static MvcHtmlString FromBusinessObject( this HtmlHelper html,
Department department) {
string departmentLink = html.ActionLink(department.Name, "Detail", "Departments",
new { id = department.Id }, null).ToHtmlString();
return new MvcHtmlString(departmentLink + " (" + department.Id + ")");
}
}
В ваших представлениях вам нужно заменить NameIdHTML
на вызов этого вспомогательного метода. Например, этот код...
@person.NameIdHTML
... необходимо заменить следующим:
@Html.FromBusinessObject(person)
Это также сохранит ваши взгляды в чистоте, и если вы решите изменить визуальное представление Person
, вы можете легко изменить BusinessToHtmlHelper.FromBusinessObject
без изменения каких-либо представлений. Кроме того, изменения в вашем маршруте или контроллерах будут автоматически отражаться сгенерированными ссылками. И логика пользовательского интерфейса остается с кодом пользовательского интерфейса, а бизнес-код остается чистым.
Если вы хотите, чтобы ваш код был полностью свободен от HTML, вы можете создать шаблон отображения для своего лица. Преимущество состоит в том, что весь ваш HTML-код имеет вид, с недостатком необходимости отображения шаблона для каждого типа HTML-ссылки, которую вы хотите создать. Для Person
шаблон отображения будет выглядеть примерно так:
@model Person
@Html.ActionLink(Model.Name, "Detail", "People", new { id = Model.Id }, null) ( @Html.DisplayFor(p => p.Id) )
Вам придется заменить ваши ссылки на person.NameIdHTML
на этот код (если ваша модель содержит свойство Person
типа Person
):
@Html.DisplayFor(m => m.Person)
Вы также можете добавить шаблоны дисплеев позже. Вы можете сначала создать BusinessToHtmlHelper
и как второй этап рефакторинга в будущем, вы измените вспомогательный класс после введения шаблонов отображения (например, выше):
public static class BusinessToHtmlHelper {
public static MvcHtmlString FromBusinessObject<T>( this HtmlHelper<T> html, Person person) {
return html.DisplayFor( m => person );
}
//...
}
Если вы были осторожны только для ссылок, созданных BusinessToHtmlHelper
, дальнейших изменений, необходимых для ваших просмотров, не будет.
Ответ 4
Нелегко дать идеальный ответ на эту проблему. Хотя полное разделение слоев желательно, оно часто вызывает много бесполезных инженерных проблем.
Хотя все в порядке с тем, что бизнес-уровень не должен много знать о слое представления/пользовательского интерфейса, я думаю, что для него приемлемо знать, что эти слои существуют, конечно, без особых деталей.
Как только вы это объявили, вы можете использовать очень недоработанный интерфейс: IFormattable. Это интерфейс, который использует string.Format.
Итак, например, вы можете сначала определить свой класс Person следующим образом:
public class Person : IFormattable
{
public string Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
// reroute standard method to IFormattable one
return ToString(null, null);
}
public virtual string ToString(string format, IFormatProvider formatProvider)
{
if (format == null)
return Name;
if (format == "I")
return Id;
// note WebUtility is now defined in System.Net so you don't need a reference on "web" oriented assemblies
if (format == "A")
return string.Format(formatProvider, "<a href='/People/Detail/{0}'>{1}</a>", WebUtility.UrlEncode(Id), WebUtility.HtmlDecode(Name));
// implement other smart formats
return Name;
}
}
Это не идеально, но, по крайней мере, вы сможете избежать определения сотен заданных свойств и сохранить детали презентации в методе ToString, который специально предназначался для деталей презентации.
Из вызывающего кода вы будете использовать его следующим образом:
string.Format("{0:A}", myPerson);
или используйте MVC HtmlHelper.FormatValue. В .NET есть много классов, поддерживающих IFormattable (например, StringBuilder).
Вы можете усовершенствовать систему и сделать это вместо этого:
public virtual string ToString(string format, IFormatProvider formatProvider)
{
...
if (format.StartsWith("A"))
{
string url = format.Substring(1);
return string.Format(formatProvider, "<a href='{0}{1}'>{2}</a>", url, WebUtility.UrlEncode(Id), WebUtility.HtmlDecode(Name));
}
...
return Name;
}
Вы бы использовали его следующим образом:
string.Format("{0:A/People/Detail/}", person)
Таким образом, вы не кодируете URL-адрес в бизнес-слое. С веб-интерфейсом в качестве уровня презентации вам обычно нужно передать имя класса CSS в формате, чтобы избежать стиля жесткого кодирования на бизнес-уровне. На самом деле вы можете найти довольно сложные форматы. В конце концов, это то, что сделано с объектами, такими как DateTime, если вы думаете об этом.
Вы даже можете пойти дальше и использовать некоторое свойство ambiant/static, которое сообщает вам, работаете ли вы в веб-контексте, поэтому оно работает автоматически, например:
public class Address : IFormattable
{
public string Recipient { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public string ZipCode { get; set; }
public string City { get; set; }
public string Country { get; set; }
....
public virtual string ToString(string format, IFormatProvider formatProvider)
{
// http://stackoverflow.com/questions/3179716/how-determine-if-application-is-web-application
if ((format == null && InWebContext) || format == "H")
return string.Join("<br/>", Recipient, Line1, Line2, ZipCode + " " + City, Country);
return string.Join(Environment.NewLine, Recipient, Line1, Line2, ZipCode + " " + City, Country);
}
}
Ответ 5
В идеале вам захочется реорганизовать ваш код, чтобы использовать модели просмотра. Модели представления могут иметь методы утилиты для простого форматирования строк, например.
public string FullName => $"{FirstName} {LastName}"
Но строго НЕТ HTML! (Быть хорошим гражданином: D)
Затем вы можете создавать различные шаблоны редактора/отображения в следующих каталогах:
Views/Shared/EditorTemplates
Views/Shared/DisplayTemplates
Назовите шаблоны после типа объекта модели, например
AddressViewModel.cshtml
Затем вы можете использовать следующее для отображения шаблонов отображения/редактора:
@Html.DisplayFor(m => m.Address)
@Html.EditorFor(m => m.Address)
Если тип свойства - AddressViewModel, тогда будет использоваться адрес AddressViewModel.cshtml из каталога EditorTemplates или DisplayTemplates.
Вы также можете управлять рендерингом, передавая параметры шаблону следующим образом:
@Html.DisplayFor(m => m.Address, new { show_property_name = false, ... })
Вы можете получить доступ к этим значениям в файле cshtml шаблона следующим образом:
@ {
var showPropertyName = ViewData.ContainsKey("show-property-name") ? (bool)ViewData["show-property-name] : true;
...
}
@if(showPropertyName)
{
@Html.TextBoxFor(m => m.PropertyName)
}
Это позволяет использовать большую гибкость, но также и возможность переопределять шаблон, который используется при применении атрибута UIHint к свойству:
[UIHint("PostalAddress")]
public AddressViewModel Address { get; set; }
Теперь методы DisplayFor/EditorFor будут искать файл шаблона PostalAddress.cshtml, который является еще одним файлом шаблона, например AddressViewModel.cshtml.
Я всегда разбиваю UI на шаблоны, подобные этому для проектов, над которыми я работаю, поскольку вы можете их упаковать через nuget и использовать их в других проектах.
Кроме того, вы также можете добавить их в новый проект библиотеки классов и скомпилировать их в dll, которые вы можете просто ссылаться на ваши проекты MVC. Я использовал RazorFileGenerator для этого ранее (http://blog.davidebbo.com/2011/06/precompile-your-mvc-views-using.html), но теперь предпочитаем использовать пакеты nuget, поскольку он позволяет управлять версиями просмотров.
Ответ 6
Я думаю, вам нужно иметь план, прежде чем вы его измените. Да, проекты, которые вы упомянули, не соответствуют действительности, но это не значит, что новый план лучше.
Во-первых, существующие проекты (это поможет вам понять, чего следует избегать):
Объекты DomainObjects, содержащие таблицы базы данных? это звучит как DAL. Я предполагаю, что эти объекты фактически хранятся в БД (например, если они являются объектно-ориентированными классами) и не отображены из них (например, с использованием структуры сущностей и последующего сопоставления результатов этим объектам), в противном случае у вас слишком много отображений (1 от EF до объектов данных и 2 от объектов данных до моделей). Я видел это, очень типичную ошибку в расслоении. Поэтому, если у вас есть это, не повторяйте это. Кроме того, не называйте проекты, содержащие объекты строк данных, как DomainObjects. Domain означает Model.
DomainORM - Хорошо, но я бы объединил его с объектами строк данных. Не имеет смысла держать проект сопоставления отдельным, если он тесно связан с объектами данных в любом случае. Это похоже на то, что вы можете заменить один без другого.
Модели - доброе имя, это может означать и Domain, так что никто не назвал бы другие проекты этим очень важным словом.
Свойство NameIdHTML - плохая идея для бизнес-объектов. Но это небольшой рефакторинг - переместите это в метод, который выходит куда-то еще, а не внутри вашей бизнес-логики.
Бизнес-объекты, похожие на DTO - тоже плохая идея. Тогда какой смысл в бизнес-логике? моя собственная статья об этом: Как создавать бизнес-объекты
Теперь вам нужно настроить таргетинг (если вы готовы к рефакторингу):
Проект хостинга бизнес-логики должен быть независимым от платформы - без упоминания HTML или HTTP или чего-либо, связанного с конкретной платформой.
DAL - должна ссылаться на бизнес-логику (а не наоборот) и должна отвечать за сопоставление, а также за хранение объектов данных.
MVC - держите тонкие контроллеры, переместив логику в бизнес-логику (где логика - это бизнес-логика) или на так называемый уровень сервиса (aka Application layer layer - необязательный и существует, если необходимо, чтобы вывести код приложения из контроллеры).
Моя собственная статья о расслоении: Архитектура слоистого программного обеспечения
Настоящие причины для этого:
Многоразовая бизнес-логика на потенциально нескольких платформах (сегодня вы только в Интернете, завтра вы можете быть веб-сайтом и сервисами, а также на рабочем столе). Все разные платформы должны использовать идеальную бизнес-логику, если они принадлежат к одному и тому же ограниченному контексту.
Долгосрочная управляемая сложность, которая является известным фактором для выбора чего-то вроде DDD (управляемого доменом) и управляемого данными. Он поставляется с кривой обучения, поэтому вы сначала инвестируете в нее. В долгосрочной перспективе вы сохраняете свою ремонтопригодность на низком уровне, например, постоянно получая премии. Опасайтесь своих противников, они будут утверждать, что это полностью отличается от того, что они делали, и это будет казаться сложным для них (из-за кривой обучения и мышления итеративно для поддержания хорошего дизайна в долгосрочной перспективе).
Ответ 7
Сначала рассмотрим вашу цель, а Кент Бек указывает на "Экономику развития программного обеспечения". Вероятно, целью вашего программного обеспечения является достижение ценности, и вы должны тратить свое время на то, чтобы сделать что-то ценное.
Во-вторых, наденьте шляпу Software Architect и сделайте какой-то расчет. Вот как вы поддерживаете выбор, чтобы тратить ресурсы на это или тратить на что-то еще.
Конец кода в этом состоянии был бы плохим, если бы в течение следующих 2 лет он собирался:
- увеличить количество несчастных клиентов
- сократить доходы вашей компании.
- увеличить количество сбоев программного обеспечения/ошибок/сбоев
- увеличить стоимость обслуживания или изменить код.
- удивляют разработчиков, заставляя их тратить часы на недоразумение.
- увеличить стоимость встроенных разработчиков
Если эти вещи вряд ли произойдут в результате кода, тогда не тратьте свою жизнь в команде на выпрямление карандашей. Если вы не можете определить реальную отрицательную стоимость-следствие кода, тогда код, вероятно, будет хорошо, и ваша теория должна измениться.
Мое предположение было бы "Стоимость изменения этого кода, вероятно, выше стоимости проблем, которые он вызывает". Но вы лучше можете угадать фактическую стоимость проблем. В вашем примере стоимость изменения может быть довольно низкой. Добавьте это в свой вариант 2 списка рефакторинга:
------------------------------------
2в. Используйте методы расширения в приложении MVC для добавления ноу-хау презентации к объектам домена с минимальным кодом.
public static class PersonViewExtensions
{
const string NameAndOnePhoneFormat="{0} ({1})";
public static string NameAndOnePhone(this Person person)
{
var phone = person.MobilePhone ?? person.HomePhone ?? person.WorkPhone;
return string.Format(NameAndOnePhoneFormat, person.Name, phone);
}
}
Когда вы встроили HTML, код в ответе @Sefe - используя методы расширения класса HtmlHelper
- это в значительной степени то, что я сделал бы. Это отличная функция Asp.NetMVC
---------------------------------------
Но этот подход должен быть усвоенной привычкой всей команды. Не спрашивайте у своего начальника бюджет для рефакторинга. Спросите своего босса о бюджете на обучение: книги, время, чтобы сделать кодирование ката, бюджет для привлечения команды к разработчикам.
Не делайте, что бы вы ни делали, любительское программное обеспечение - архитектура мышления, - этот код не соответствует X, поэтому мы должны тратить время и деньги на его изменение, хотя мы не можем показать конкретного значения для этот расход ".
В конечном счете, ваша цель - повысить ценность. Расходы на обучение повысят ценность; тратить деньги на предоставление новых функций или удаление ошибок могут повысить ценность; тратить деньги на переработку рабочего кода только добавляет ценность, если вы действительно устраняете дефекты.