Каков наилучший способ рефакторингового кода презентации из моих объектов домена в решении 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, поэтому мы должны тратить время и деньги на его изменение, хотя мы не можем показать конкретного значения для этот расход ".

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