ViewModels в MVC/MVVM/Разделение лучших практик layers-?

Я новичок в использовании ViewModels, и мне интересно, приемлемо ли для ViewModel включать экземпляры моделей домена в качестве свойств, или должны ли свойства этих моделей домена быть свойствами самого ViewModel? Например, если у меня есть класс Album.cs

public class Album
{
    public int AlbumId { get; set; }
    public string Title { get; set; }
    public string Price { get; set; }
    public virtual Genre Genre { get; set; }
    public virtual Artist Artist { get; set; }
}

Как правило, у ViewModel есть экземпляр класса Album.cs, или у вас есть свойства ViewModel для каждого из свойств класса Album.cs.

public class AlbumViewModel
{
    public Album Album { get; set; }
    public IEnumerable<SelectListItem> Genres { get; set; }
    public IEnumerable<SelectListItem> Artists { get; set; }
    public int Rating { get; set; }
    // other properties specific to the View
}


public class AlbumViewModel
{
    public int AlbumId { get; set; }
    public string Title { get; set; }
    public string Price { get; set; }
    public IEnumerable<SelectListItem> Genres { get; set; }
    public IEnumerable<SelectListItem> Artists { get; set; }
    public int Rating { get; set; }
    // other properties specific to the View
}

Ответы

Ответ 1

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

ТЛ; др

Допустимо ли для ViewModel содержать экземпляры моделей доменов?

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


Я начну с того, что укажу на некоторые общие применимые концепции, а позже расскажу о реальных сценариях и примерах.


Давайте рассмотрим некоторые плюсы и минусы не смешивания слоев.

Что это будет стоить вам

Всегда есть подвох, я их подведу, объясню позже и покажу, почему они обычно не применимы

  • дубликат кода
  • добавляет дополнительную сложность
  • дополнительный удар по производительности

Что вы получите

Всегда есть победа, я ее подведу, объясню позже и покажу, почему это имеет смысл

  • независимый контроль слоев

Расходы


дубликат кода

Это не СУХОЙ !

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

Это неверный аргумент. Различные слои имеют четко определенное различное назначение. Поэтому свойства, которые живут в одном слое, имеют другое назначение, чем свойство в другом, даже если свойства имеют одно и то же имя!

Например:

Это не повторяется

public class FooViewModel
{
    public string Name {get;set;}
}

public class DomainModel
{
    public string Name {get;set;}
}

С другой стороны, дважды определяя отображение, повторяется:

public void Method1(FooViewModel input)
{
    //duplicate code: same mapping twice, see Method2
    var domainModel = new DomainModel { Name = input.Name };
    //logic
}

public void Method2(FooViewModel input)
{
    //duplicate code: same mapping twice, see Method1
    var domainModel = new DomainModel { Name = input.Name };
    //logic
}

Это больше работы!

Действительно ли это? Если вы начнете кодировать, более 99% моделей будут перекрываться. Взятие чашки кофе займет больше времени ;-)

"Нужно больше обслуживания"

Да, именно поэтому вам нужно провести модульное тестирование вашего отображения (и помните, не повторяйте отображение).

добавляет дополнительную сложность

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

Мой умный друг однажды сказал это так:

"Летящий самолет - очень сложная вещь. Падающий самолет - очень сложный".

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

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

Добавление слоя дает вам четко определенное поведение, которое из-за очевидного дополнительного отображения будет (немного) более сложным. Смешивание слоев для различных целей приведет к непредсказуемым побочным эффектам при применении изменений. Переименование столбца базы данных приведет к несоответствию в поиске ключа/значения в вашем пользовательском интерфейсе, что заставит вас сделать несуществующий вызов API. Теперь подумайте об этом и о том, как это будет связано с вашими усилиями по отладке и затратами на обслуживание.

дополнительный удар по производительности

Да, дополнительное отображение приведет к увеличению потребляемой мощности процессора. Это, однако (если у вас есть Raspberry Pi подключен к удаленной базе данных) незначительно по сравнению с извлечением данных из базы данных. Итог: если это проблема: используйте кэширование.

Победа


независимый контроль слоев

Что это значит?

Любая комбинация этого (и более):

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

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

остерегайтесь: деловые контрмеры!

"Это должно отражать изменения, это не изменится!"

Придут перемены: ежегодно тратить триллионы долларов США просто невозможно.

Ну, это мило. Но смирись с этим, как разработчик; день, когда вы не совершаете ошибок, - это день, когда вы перестаете работать. То же относится и к бизнес-требованиям.

забавный факт; энтропия программного обеспечения

"Мой (микро) сервис или инструмент достаточно мал, чтобы справиться с этим!"

Это может быть самым сложным, поскольку здесь есть хороший момент. Если вы разрабатываете что-то для однократного использования, оно, вероятно, вообще не сможет справиться с этим изменением, и вам все равно придется его перестраивать, если вы действительно собираетесь его использовать повторно. Тем не менее, для всего остального: "изменения придут", так зачем делать изменения более сложными? И, пожалуйста, обратите внимание, что, возможно, если вы пропустите слои в вашем минималистичном инструменте или сервисе, как правило, слой данных будет ближе к интерфейсу (пользователя). Если вы имеете дело с API, ваша реализация потребует обновления версии, которое должно быть распространено среди всех ваших клиентов. Вы можете сделать это во время одного перерыва на кофе?

"давайте сделаем это быстро и просто, просто на время...."

Ваша работа "на данный момент"? Шучу ;-) но; когда ты собираешься это исправить? Вероятно, когда ваш технический долг заставляет вас. В то время это стоило вам больше, чем этот короткий перерыв на кофе.

"А как насчет" закрыт для модификации и открыт для расширения "? Это также принцип SOLID!"

Да, это! Но это не значит, что вы не должны исправлять опечатки. Или что каждое применяемое бизнес-правило может быть выражено в виде суммы расширений или что вам не разрешено исправлять неисправные вещи. Или, как гласит Википедия:

Модуль считается закрытым, если он доступен для использования другими модулями. Это предполагает, что модуль получил четкое, стабильное описание (интерфейс в смысле скрытия информации)

что на самом деле способствует разделению слоев.


Теперь несколько типичных сценариев:

# ASP.NET MVC

Это то, что вы используете в своем актуальном вопросе:

Позвольте мне привести пример. Представьте себе следующую модель представления и модель предметной области:

примечание: это также применимо к другим типам слоев, чтобы назвать несколько: DTO, DAO, Entity, ViewModel, Domain и т.д.

public class FooViewModel
{
    public string Name {get; set;} 

    //hey, a domain model class!
    public DomainClass Genre {get;set;} 
}

public class DomainClass
{
    public int Id {get; set;}      
    public string Name {get;set;} 
}

Итак, где-то в вашем контроллере вы заполняете FooViewModel и передаете его на ваш взгляд.

Теперь рассмотрим следующие сценарии:

1) Модель предметной области меняется.

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

Если вы отделите ViewModel от DomainModel, незначительная корректировка в сопоставлениях (ViewModel => DomainModel (и обратно)) будет достаточной.

2) DomainClass имеет вложенные свойства, и ваше представление просто отображает "GenreName"

Я видел, как это пошло не так в реальных сценариях.

В этом случае общая проблема заключается в том, что использование @Html.EditorFor приведет к @Html.EditorFor данных для вложенного объекта. Это может включать Id и другую конфиденциальную информацию. Это означает утечку деталей реализации! Ваша фактическая страница связана с моделью вашего домена (которая, вероятно, где-то связана с вашей базой данных). После этого курса вы создадите hidden входы. Если вы комбинируете это с привязкой к модели на стороне сервера или автоматом, становится сложнее блокировать манипуляции со скрытым Id с помощью таких инструментов, как firebug, или если вы забыли установить атрибут для вашего свойства, он станет доступен в вашем представлении.

Хотя некоторые из этих полей можно, возможно, легко заблокировать, но чем больше у вас вложенных объектов "Домен/Данные", тем сложнее будет сделать эту часть правильной. А также; Что делать, если вы "используете" эту модель домена в нескольких представлениях? Будут ли они вести себя так же? Также имейте в виду, что вы можете изменить свою DomainModel по причине, которая не обязательно нацелена на представление. Поэтому при каждом изменении в вашей DomainModel вы должны знать, что это может повлиять на представление (я) и аспекты безопасности контроллера.

3) В ASP.NET MVC обычно используются атрибуты проверки.

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

Я думаю, ясно, что это не тот путь, по которому нужно идти.

4) Больше

Я могу дать вам больше сценария, но это просто вопрос вкуса к тому, что более привлекательно. Я просто надеюсь, что в этот момент вы получите точку :) Тем не менее, я обещал иллюстрацию:

Scematic

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

Требуется чуть больше усилий для построения модели представления, которая обычно на 80+% похожа на модель предметной области. Это может показаться ненужным отображением, но когда появится первое концептуальное отличие, вы обнаружите, что оно того стоило :)

В качестве альтернативы я предлагаю следующую настройку для общего случая:

  • создать модель представления
  • создать доменную модель
  • создать модель данных
  • используйте такую библиотеку, как automapper чтобы создать отображение из одного в другое (это поможет сопоставить Foo.FooProp с OtherFoo.FooProp)

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

Web API/уровень данных /DTO

Другой конкретный пример того, как это будет работать в сценарии Web-API/ORM (EF):

Здесь он более интуитивно понятен, особенно когда потребитель является третьей стороной, вряд ли ваша модель домена соответствует реализации вашего потребителя, поэтому модель представления с большей вероятностью будет полностью автономной.

Web Api Datalayer EF

примечание: название "модель домена", также может называться DTO или "модель"

Обратите внимание, что в веб (или HTTP или REST) API; связь часто осуществляется объектом передачи данных (DTO), который является фактической "вещью", которая выставляется на конечных точках HTTP.

Итак, куда мы должны поместить эти DTO, вы можете спросить. Они между моделью предметной области и моделями представления? Ну да; мы уже видели, что рассматривать их как viewmodel будет сложно, так как потребитель, скорее всего, реализует настроенное представление.

domainmodels ли DTO заменить domainmodels или у них есть причина существовать самостоятельно? В общем, концепция разделения будет применима и к DTO's и к domainmodels. Но опять же: вы можете спросить себя (и здесь я склонен быть немного прагматичным); Достаточно ли логики в домене, чтобы явно определить domainlayer? Я думаю, вы обнаружите, что если ваш сервис становится все меньше и меньше, фактическая logic, которая является частью domainmodels, также уменьшается, и может быть исключена все вместе, и вы получите:

EF/(ORM) EntitiesDTOConsumers


отказ от ответственности/примечание

Как сказал @mrjoltcola: необходимо учитывать и чрезмерную разработку компонентов. Если ничего из вышеперечисленного не применимо, и пользователям/программистам можно доверять, все готово. Но имейте в виду, что ремонтопригодность и возможность повторного использования уменьшатся из-за смешивания DomainModel/ViewModel.

Ответ 2

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

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

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

Для защиты от инъекций используйте безопасные методы, которые имеют отношение к вашему общему подходу. Если вы планируете использовать объекты домена, убедитесь, что вы используете белые списки или черные списки (включение/исключение) в контроллере или через аннотации привязки модели. Черные списки удобнее, но ленивые разработчики, пишущие будущие версии, могут забыть о них или не знать о них. Белые списки ([Bind (Include =...)] обязательны, требуя внимания при добавлении новых полей, поэтому они действуют как модель встроенного представления.

Пример:

[Bind(Exclude="CompanyId,TenantId")]
public class CustomerModel
{
    public int Id { get; set; }
    public int CompanyId { get; set; } // user cannot inject
    public int TenantId { get; set; }  // ..
    public string Name { get; set; }
    public string Phone { get; set; }
    // ...
}

или

public ActionResult Edit([Bind(Include = "Id,Name,Phone")] CustomerModel customer)
{
    // ...
}

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

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

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