Я решил написать на эту тему, т. к. постоянно использую модели видов (view models) в веб-приложениях на ASP.NET MVC, и часто приходится объяснять суть такого подхода коллегам, а подходящего материала, чтобы ссылаться на него, мы так и не нашли. Эта статья ориентирована прежде всего на новичков.

Представим, что нам необходимо отобразить календарь некоторых мероприятий на текущий месяц. Это достаточно сложная конструкция. Календарь должен содержать заголовок с названием текущего месяца и годом, строку с названиями дней и, собственно, сами дни (6 рядов по 7 дней), каждый из которых имеет дату и, опционально, некоторый набор мероприятий, названия которых необходимо отобразить, предварительно загрузив их из базы данных. Также предположим, что выходные и праздничные дни должны быть отмечены особым образом. Т. е. в итоге должно получиться нечто такое:




Немного теории


Шаблон проектирования MVC и его концепция разделения приложения на 3 части (модель, представление и контроллер) знакомы, наверное, каждому разработчику.

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

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

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

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

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

На этом, думаю, пора переходить к практике.

Практика


Вернемся теперь к нашему календарю мероприятий как к более простому примеру. Для начала, подготовим пустое веб-приложение на ASP.NET MVC (я буду использовать ASP.NET Core, чтобы заодно продемонстрировать возможности новой платформы, но в контексте нашего примера это не имеет значения). Добавим в него единственный контроллер DefaultController с единственным действием (action) Calendar в нем — оно будет отвечать за отображение календаря. Добавим также соответствующее представление (пока что без всякого содержимого). Если сейчас запустить наше приложение мы должны получить пустую страницу. (В конце статьи вы найдете ссылку на готовый тестовый проект, выложенный на GitHub.)

Как мы уже убедились выше, чтобы передать все необходимые данные нашему представлению, нам потребуется соответствующая модель вида. Назовем ее CalendarViewModel. (Модели вида очень удобно размещать в папке ViewModels проекта, повторяя структуру папки Views; позже я приведу соответствующий скриншот.) Сразу добавим в нее очевидное свойство Date типа DateTime. Оно потребуется нам для отображения текущих месяца и года. Должен получиться вот такой класс:

public class CalendarViewModel
{
  public DateTime Date { get; set; }
}

Теперь добавим строитель для нашей модели вида (сейчас в нем особой необходимости нет, но позже она появится — сделаем это заранее):

public class CalendarViewModelBuilder
{
  public CalendarViewModel Build()
  {
    return new CalendarViewModel()
    {
      Date = DateTime.Now
    };
  }
}

Как видим, метод Build строителя не принимает параметров и возвращает новый объект класса CalendarViewModel — готовую модель вида.

Теперь укажем наш класс CalendarViewModel в качестве модели вида для представления Calendar и добавим отображение месяца и года из этой модели вида:

@model AspNetCoreViewModels.ViewModels.Default.Calendar.CalendarViewModel
<div class="calendar">
  <div class="header">
    @Model.Date.ToString("MMMM yyyy")
  </div>
</div>

Далее воспользуемся строителем CalendarViewModelBuilder для передачи модели вида представлению. Наш контроллер должен принять следующий вид:

public class DefaultController : Controller
{
  public ActionResult Calendar()
  {
    return this.View(new CalendarViewModelBuilder().Build());
  }
}

Теперь мы можем запустить приложение снова, и на этот раз кое-что уже буде отображаться (я немного повозился со стилями, поэтому будущий календарь уже имеет некоторое оформление):



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

@model AspNetCoreViewModels.ViewModels.Default.Calendar.CalendarViewModel
<div class="calendar">
  <div class="header">
    @Model.Date.ToString("MMMM yyyy")
  </div>
  <table cellpadding="0" cellspacing="0">
    <tr>
      <th>Пн</th>
      <th>Вт</th>
      <th>Ср</th>
      <th>Чт</th>
      <th>Пт</th>
      <th>Сб</th>
      <th>Вс</th>
    </tr>
  </table>
</div>

В браузере это выглядит так:



Теперь настал черед самого интересного — отображения дней. Для этого добавим отдельное частичное представление _Day и модель вида DayViewModel для него. (Также вполне возможно, что в дальнейшем в нашем проекте мы могли бы захотеть отображать дни с запланированными мероприятиями независимо от календаря. Например, как отдельный блок мероприятий на сегодня. Будем иметь это в виду.)

Пока что добавим в класс DayViewModel свойство Date типа DateTime и еще 3 свойства типа bool — IsNotCurrentMonth, IsWeekendOrHoliday и IsToday:

public class DayViewModel
{
  public DateTime Date { get; set; }
  public bool IsNotCurrentMonth { get; set; }
  public bool IsWeekendOrHoliday { get; set; }
  public bool IsToday { get; set; }
}

Метод Build строителя на этот раз принимает один параметр — дату:

public class DayViewModelBuilder
{
  public DayViewModel Build(DateTime date)
  {
    return new DayViewModel()
    {
      Date = date,
      IsNotCurrentMonth = date.Month != DateTime.Now.Month,
      IsWeekendOrHoliday = date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday,
      IsToday = date.Date == DateTime.Now.Date
    };
  }
}

Как видим, строитель инициализирует все свойства модели вида. Флаг IsNotCurrentMonth определяет, находится ли день вне текущего месяца (чтобы иметь возможность выделить его серым цветом). Установленный IsWeekendOrHoliday означает, что день является выходным или праздничным, а IsToday — сегодняшним (соответственно, мы будем выделять такие дни красным или зеленым).

Частичное представление _Day может выглядеть следующим образом (обратите внимание, мы специально не используем здесь тег td, чтобы это частичное представление можно было использовать отдельно от календаря):

<div class="day @(this.Model.IsNotCurrentMonth ? "not-current-month" : null) @(this.Model.IsWeekendOrHoliday ? "weekend-or-holiday" : null) @(this.Model.IsToday ? "today" : null)">
  <div class="date">
    @Model.Date.Day.ToString("00")
  </div>
</div>

В зависимости от значения свойств IsNotCurrentMonth, IsWeekendOrHoliday и IsToday устанавливаются соответствующие CSS-классы.

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

Размышляя о том, какую логику лучше реализовать в строителе модели вида, а какую — непосредственно в представлении, следует исходить из результатов простого теста: придется ли заново дублировать эту логику, если потребуется заменить представление? Если да, то логику следует размещать в строителе модели вида. Если нет — непосредственно в представлении (это означает, что код слишком специфичен и относится только к конкретному способу отображения). Хотя если под логикой подразумевается нечто действительно объемное, то возможно лучшим решением будет сделать дополнительную модель вида под конкретное представление и перенести эту логику в метод Build ее строителя. В нашем случае мы могли бы представить дни в виде массива из 42 элементов (6 рядов по 7 дней), но в таком случае в представлении Calendar нам потребуется логика для разбиения этого массива на строки. Поэтому, пожалуй, уместнее будет сразу сделать массив двумерным (если только мы не предполагаем, что в дальнейшем потребуется выводить дни как-то иначе, чем таблицей):

public class CalendarViewModel
{
  public DateTime Date { get; set; }
  public DayViewModel[,] Days { get; set; }
}

Метод Build строителя этой модели вида теперь можно дополнить примерно такой логикой:

DayViewModel[,] days = new DayViewModel[6,7];
DateTime date = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
int offset = (int)date.DayOfWeek;

if (offset == 0)
  offset = 7;

offset--;
date = date.AddDays(offset * -1);

for (int i = 0; i != 6; i++)
{
  for (int j = 0; j != 7; j++)
  {
    days[i, j] = new DayViewModelBuilder().Build(date);
    date = date.AddDays(1);
  }
}

Запустим приложение и посмотрим, что получилось:



Почти готово. Теперь разберемся с мероприятиями. Во-первых, нам потребуется добавить класс Event в нашу модель:

public class Event
{
  public int Id { get; set; }
  public DateTime Date { get; set; }
  public string Name { get; set; }
}

Во-вторых, мы добавим некий фейковый слой доступа к данным, который на самом деле просто будет возвращать предопределенные объекты для заданных дат. Не буду останавливаться на этом подробно, его реализацию (с использованием шаблонов Единица работы и Репозиторий в простейшем виде + использование встроенного в ASP.NET Core DI) можно посмотреть в тестовом проекте.

Сразу же добавим модель вида EventViewModel, строитель для нее и частично представление _Event.

Класс EventViewModel:

public class EventViewModel
{
  public DateTime Date { get; set; }
  public string Name { get; set; }
}

Метод Build класса EventViewModelBuilder:

public EventViewModel Build(Event @event)
{
  return new EventViewModel()
  {
    Date = @event.Date,
    Name = @event.Name
  };
}

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

Частичное представление _Event:

@model AspNetCoreViewModels.ViewModels.Shared.EventViewModel
<div class="event">
  @Model.Date.ToString("HH:mm")<br />
  @Model.Name
</div>

Очевидно, что все это может использоваться повторно, вне зависимости от дней или календаря.

Чтобы закончить наше приложение нам необходимо добавить свойство Events типа IEnumerable в класс DayViewModel и проинициализировать его в строителе модели вида дня. Для этого нам потребуется, чтобы строитель модели вида дня мог обращаться к слою доступа к данным, который у нас представлен реализацией шаблона Единица работы. Я не хотел бы касаться сейчас этого детально, чтобы не увеличивать и без того большую статью. Вкратце, все обращения к слою доступа к данным в рамках одного запроса к контроллеру должны происходить контексте единственного экземпляра класса нашей единицы работы. Т. е. такой экземпляр должен быть создан при создании объекта контроллера (например, с помощью DI) и передан во все строители моделей видов по цепочке. Поэтому я добавил еще один абстрактный класс ViewModelBuilderBase, конструктор которого принимает один аргумент storage типа IStorage и сохраняет его в защищенной переменной, чтобы все наследники имели к нему доступ. Теперь метод Build класса DayViewModelBuilder может быть дополнен инициализацией свойства Events:

Events = this.Storage.EventRepository.FilteredByDate(date).Select(
  e => new EventViewModelBuilder(this.Storage).Build(e)
)

Как видим, мы обращаемся к репозиторию EventRepository и с помощью его метода FilteredByDate отбираем все мероприятия на заданную дату, а затем при помощи метода LINQ Select и строителя EventViewModelBuilder проецируем каждый объект модели Event на объект модели вида EventViewModel.

Если теперь добавить вывод мероприятий в частичное представление _Day, то наше приложение будет готово и запустив его мы получим то, что было изображено на первом скриншоте:

<div class="events">
  @foreach (var @event in this.Model.Events)
  {
    @Html.Partial("_Event", @event)
  }
</div>

Вот и все. Итоговая структура проекта:



Заключение


Я выложил этот проект на GitHub, чтобы можно было посмотреть на него вживую. Повторюсь, он реализован на ASP.NET Core. Вот здесь можно найти все что необходимо, чтобы его запустить.

Надеюсь, у меня получилось объяснить суть этого подхода и продемонстрировать простой способ его реализации. Я совершенно ничего не упомянул об использовании моделей видов для отображения форм, хотя это не менее распространенный сценарий их использования. Если будет интересно, я могу описать это в следующей статье. Возможно из-за использования «базы данных» пример показался слишком запутанным, но хотелось непременно коснуться этого аспекта. В общем, спасибо за внимание и буду рад услышать критику!
Поделиться с друзьями
-->

Комментарии (41)


  1. RouR
    08.09.2016 13:45
    +4

    Меня наверно закидают тапками, но считаю что тут переизбыток ООП. В частности я бы выкинул все билдеры, кроме CalendarViewModelBuilder.
    Причина — я не верю что на практике будет переиспользован DayViewModelBuilder. Если будет какой-то новый CalendarVisitorsViewModel, то вместо DayViewModel будет использоваться новый DayVisitorsViewModel в котором скорее вместо IEnumerable EventViewModel будет IEnumerable VisitorsDayStats А разбираться что делает билдер внутри билдера, который внутри третьего билдера ради гипотетической возможности переиспользования в будущем как-то не хочется.


    1. Hydro
      08.09.2016 15:14
      +2

      Я бы и билдеры выбросил, да и это не билдеры, а фабрики по сути.

      Несколько лет назад, когда изучал WPF, мой мозг был затуманен подобными статьями, где обязательно былa ViewModel, даже там, там где она не нужна (например свойства модели тупо копировались во вью-модель). Боюсь на новичков эта статья произведет такой же эффект, как и на меня когда-то, а именно, слепая вера, что вью модель — наше все, а по сути лишняя прокладка.
      Был бы пример нагляднее, где без ViewModel ну никак.


      1. DmitrySikorsky
        08.09.2016 15:28

        Да, наименование так себе. Вот тут подробнее ответил.

        Я согласен, что местами модели видов могут выглядеть избыточными, но какое есть решение? Использовать в некоторых местах модели видов (не помещать же все необходимые данные во ViewBag), а в других — «сырые» объекты предметной области? Это очень усложнит чтение кода, он перестанет быть однородным и это бОльшее зло, чем несколько доп. классов, на мой взгляд.


    1. DmitrySikorsky
      08.09.2016 15:24

      Согласен, что следует подходить к этому вопросу внимательно и не добавлять билдеры там, где они не нужны. Но не могу согласиться с тем, что следует удалить все билдеры, кроме календаря. Повторное использование это постоянное явление. Если снова посмотреть на магазин: товар отображается в списке товаров, в списке рекомендуемых и просмотренных товаров, в корзине, в предыдущих заказах и еще в куче мест. Можно использовать различные представления, но одну модель вида для них всех. Можно управлять глубиной построения модели вида, когда это необходимо. Но это в целом решает 2 важные задачи: избавление от дублирования кода и четкие и понятные рамки ответственности различных классов. В статье я привел очень упрощенный пример, возможно из-за этого сложилось такое мнение.


      1. sentyaev
        08.09.2016 17:43
        +1

        Я обычно добавляю мапперы/билдеры и т.д. только когда понимаю, что начинаю дублировать код, не раньше. Это позволяет уменьшить кол-во классов, а именно не писать dto/mappres ДО того как они нужны.


  1. osharper
    08.09.2016 14:23

    хоть посмотрел на русскоязычную VS :)

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


    1. DmitrySikorsky
      08.09.2016 15:29

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


  1. Vitalii_Panchenko
    08.09.2016 15:16

    Вообще использование паттерна Builder в данном контексте не правильное, лучше подходит паттерн Factory.

    Пример использования Builder`a:

    var emailBuilder = new EmailBuilder();
    emailBuilder.From(«from@from.from»);
    emailBuilder.To(«to@to.to»);

    emailBuilder.Build();

    причем метод Build() всегда без параметров.


    1. DmitrySikorsky
      08.09.2016 15:19

      Согласен. Но тут дело не в паттерне, а в названии класса. Билдер в данном случае не означает, что используется шаблон проектирования Строитель. Почему-то так повелось, использовать для построения модели вида билдер, а для обратного процесса — маппер. Я иногда задумывался об этом. Возможно, действительно стоит пересмотреть подход к наименованию, чтобы не было путаницы.


      1. sentyaev
        08.09.2016 17:40

        Правильнее было бы назвать mapper, т.к. builder это известный всем шаблон проектирования и такое наименование вводит в заблуждение.


        1. DmitrySikorsky
          08.09.2016 21:06

          Т. к. модели вида не маппятся, а конструируется из разных кусков, лучше все-таки наверное Builder переименовать в Factory, и метод Build переименовать тоже. Тогда это будет вполне отражать суть. А мапперы применяются для обратного процесса — превращения модели вида (например, формы) в модель предметной области.


          1. sentyaev
            08.09.2016 22:02
            +1

            Да. Звучит логично.


  1. yarosroman
    08.09.2016 18:25
    +1

    А не проще для этого использовать компоненты (Components)? К примеру я через компонент вывожу дерево комментариев на сайте.


    1. DmitrySikorsky
      08.09.2016 23:54

      Компоненты как раз и созданы для вывода комментариев или чего-то подобного (т. е. самодостаточных блоков), вы правы. Например, я вывожу блоки меню и форм в своем проекте с использованием компонент. Но модели вида это немного из другой оперы. Например, вы можете в компоненте вывести календарь, и опять упретесь в необходимость передачи данных в его вьюху. Не будете же вы вставлять в компонент еще 42 других компонента? Т. е. это дополняющие друг друга вещи, а не взаимоисключающие. Кстати, есть мнение, что компоненты (а в предыдущих версиях ASP.NET — конструкции вроде Html.Action(), т. е. рендеринг результата некого запроса внутри представления) как бы нарушают концепцию MVC. Нарушают даже не в плохом смысле, а просто как факт.


  1. olen
    09.09.2016 14:16
    +2

    В общих чертах использую подобный подход. В частности, для каждой View создаю свою ViewModel, при необходимости ViewModel может содержать другие ViewModel.
    А вот билдеры, хоть и кажутся на первый взгляд хорошим решением, содержат, на мой взгляд, недостатки. Например, DayViewModelBuilder содержит такую строку:

    Events = this.Storage.EventRepository.FilteredByDate(date).Select(
    e => new EventViewModelBuilder(this.Storage).Build(e) )
    Т.е. для каждого дня месяца будет обращение к Storage для получения списка событий на этот день. Гораздо эффективнее получить события за все необходимые нам дни сразу.


    1. DmitrySikorsky
      09.09.2016 16:19

      Да, согласен. Здесь следует быть внимательным. Хотя если используется нечто вроде EF с Include то все вложенные сущности могут быть загружены одним запросом.


      1. olen
        09.09.2016 16:24

        Тут речь не о вложенных сущностях, а о том, что DayViewModelBuilder знает только об одном дне, для которого и загружает Event.


        1. DmitrySikorsky
          09.09.2016 16:32

          В этом примере — да. Но чаще бывает вывод чего-то вроде списка товаров, для которых все вложенные вещи обычно можно загрузить при помощи JOIN. Но. Даже в таком случае можно что-то придумать, чтобы выгрузить все мероприятия на месяц за 1 раз. Это уже скорее вопрос оптимизации, чем подхода в целом.


          1. olen
            09.09.2016 18:05
            +1

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


  1. Razaz
    10.09.2016 01:39

    Добавлю свои пять копеек:
    В проектах просто сделали SomeViewModel(SomeModel) конструкторы и все. Достали объект, скормили конструктору и забыли. Сам ViewModel разберется что и куда смапить. Это очень упростило код и при изменении всегда есть только одно место где надо менять. Ну и реюзабельность опять же — скармливаете root объект и все строится по иерархии. Комбинируйте как хотите. Если вдруг надо дополнительный данные — еще параметр в конструкторе. Все зависимости сразу ясны и понятны.
    Плюс иногда делается отдельный InputModel, что бы автоматом экранировать входные параметры.


    1. sentyaev
      10.09.2016 03:40

      Вот я кучу способов пробовал, но до сих пор не определился как же лучше.
      1. Сначала просто руками писал маппинг Model -> ViewModel
      2. Использовал Automapper для этого
      3. Пробовал метод который вы описали
      4. В одном проекте во ViewModel отдавали DbContext и она уже вытаскивала что нужно
      Сейчас склоняюсь к тому, что если используется anemic model, то вообще ничего не нужно, можно просто отдавать во вьюхи эти модели (или коллекции моделей), и ничего страшного, что вьюхи будут зависеть от доменной модели (т.к. если модель anemic, то там все равно ничего нет, это просто структура данных по сути).
      Я понимаю, что нужно иногда форматирование сделать, или дропдауны, но это решаемо.
      Я не говорю, что это правильный подход, скорее я думаю попробовать это в следующем проекте.


      1. Razaz
        10.09.2016 12:23

        Примерно такой же путь. Разве что контекст никогда не давали.
        У нас просто ViewModel сейчас нет практически — REST API + SPA. Идет конвертация из Model->Resource и там уже всякие финтифлюшки типа ссылок и другие hypermedia прелести и хитрые json конвертеры.

        Я не люблю выставлять модель напрямую так как ViewModel может отличаться от доменной модели. Как раз вы вспомнили про дропдауны и тд.
        ViewModel так же может ограничивать данные, доступные во view. Ну и обратный баиндинг туда же.


        1. sentyaev
          10.09.2016 12:35

          так как ViewModel может отличаться от доменной модели

          У меня в компании как раз холивар на эту тему идет))

          Это на самом деле довольно резонный аргумент и логичный, но на практике у нас (я имею ввиду именно текущий свой проект) почти 100% вьюмоделей повторяют доменные модели (исключения, это буквально 2 класса из более чем 100).
          И мой основной аргумент за то, чтобы выбросить 98 вьюмоделей и оставить 2, а если вдруг у нас появится вьюха которая отличается от доменной модели, то добавить тогда уже эту вьюмодель, но не раньше.


          1. Razaz
            10.09.2016 23:13

            Ну по сути ViewModel в MVC — это специализированный DTO.
            Для дропдаунов через всякие вьюбэги данные таскать очень некомильфо имхо :)
            Тут есть еще нюанс — как вы данные запроса собираете? Прямо в модель?


            1. sentyaev
              11.09.2016 00:30

              как вы данные запроса собираете?

              Модель это проекция из Монги.

              Да, забыл сказать, что в текущий проект — это не ASP.NET Mvc приложение, а WebApi и данные в mongodb.
              Получается, что достаем по сути готовые данные, а потом мапим на dto, причем один к одному, почти всегда.


              1. Razaz
                11.09.2016 01:25

                Ну у нас то же MVC контроллеров штуки 3 осталось ;)
                Все данные достаются из разных источников — фс, бд, сервисы и тд. Иногда совпадают, а иногда нет. Бывает необходимость изменения терминологии относительно источника и тд. Ну и hypermedia в полный рост — вариация на тему Json API.
                Плюс используется Swagger и генерация клиентов через Autorest. Поэтому только аналог ViewModel И никак иначе.


      1. RouR
        10.09.2016 12:28

        Пару лет использую anemic model и жизнь стала проще :)
        Энтерпрайз решениям важно движение данных и их история, которое лучше всего делается на анемичном домене. ООП в чистом виде — для работы с состояниям объектов, для игр это лучший вариант. Каждой задаче свой инструмент.


        1. sentyaev
          10.09.2016 12:58

          Я поработал в достаточном количестве проектов и все они использовали anemic model.
          В этом подходе нет ничего плохого, он рабочий.
          Но, все проекты выглядят одинаково, если открыть проект Project.Web, там будет такое разделение на каталоги — Controllers, Models, Views, Migrations и т.д.
          Если проект побольше, то скорее всего все разнесут по проектам, и будут такие проекты как Project.Models, Project.Data, Project.Api, Project.Dal и т.д., думаю вы это сто раз видели.

          Основная проблема для меня, это то, что все это не имеет никакого отношения к предметной области. Это же просто какие-то технические штуки, скорее детали имплементации.
          И в каждом из этих проектов повторяются одни и теже вложенные каталоги (например User, Product скорее всего будут во всех проектах).
          И получается, что хоть мы и разделяем все по проектам, и думаем что все это по SOLID, на самом деле это не так, и все эти проекты связаны и друг без друга не работают.

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


          1. RouR
            10.09.2016 17:03

            Унификация уменьшает порог вхождения нового сотрудника, в этом нет ничего плохого.
            То, разделение что вы привели, я тоже не увидел предметной области. Видимо она спрятана в «т.д.» У меня в проекте вся логика предметной области находится в отдельном слое. Бизнес-сущности-модели отделены от «технических» моделей. Про «одни и теже вложенные каталоги» с таким не сталкивался, во всём солюшене у меня один класс User, а никак не несколько в разных подпроектах.
            Подпроекты между собой должны быть связаны, эта связь и определяет конечный продукт. Но связь должна быть слабой, через DI, чтобы изменения одного подпроекта не были фатальными для других частей.
            Если проект становится сложно сопровождать, то видимо вылезают ошибки архитектуры.


            1. sentyaev
              10.09.2016 17:26

              Я имел ввиду что-то вроде этого:

              Project.Web/
                  Controllers/
                      UserController
                  ViewModels/
                      UserViewModel
                  Views/
                      User/
                          Index
                          Details
              
              Project.Business/
                  Interfaces/
                      IUserService
                  Services/
                      UserService
              
              Project.DAL
                  Repositories/
                      IUserRepository
                      UserRepository
                  Entities/
                      User
              


              Тут User можно заменить на что угодно, например Order, ShoppingCart, Car, Animal.
              При такой структуре DI не помогает, а скорее скрывает проблему.


              1. Razaz
                10.09.2016 23:15

                Мы сейчас активно перенимаем структуру проектов отсюда: Orchard 2. Сборка это не слой приложения, а бизнес фича. Тоесть получаем кучку мини-приложений со стандартной структурой папок и нэйминга :)


                1. sentyaev
                  11.09.2016 00:39

                  Посмотрел на Orchard, те же яйца.
                  То, что я привел в пример, это конечно вырожденный случай, у нас как раз проект более похож на Orchard.
                  Посмотрите на структуру каталогов — Web, Validations, Abstractions, Events, FileSystem, разве это хоть что-то говорит о том что приложение делает?

                  Я не говорю, что это не правильно, скорее я дошел до того, что я не вижу за этими каталогами ничего о том что приложение делает, нет даже намека на функциональность.


                  1. Razaz
                    11.09.2016 01:33

                    Вообще говорит.
                    1. Web — способ хостинга. В первой версии еще и Cli был.
                    2. Validation — стандартная вариация на Check/Guard и тд.
                    3. Abstractions — суффикс обозначающий, что сборка содержит только абстракции определенной фичи. Если фичи нет — то общие по проекту.
                    4. Events- событийная модель.
                    Это CMS. И для нее это Business Domain. Готовые модули можно поглядеть в первой версии.

                    ИМХО называть проекты BLL, DAL И тд считаю дурным тоном.


                    1. sentyaev
                      11.09.2016 02:24

                      Ok. Я, к сожалению, специфику CMS не знаю, никогда их не писал, так что тут вам виднее.

                      Но, я считаю, что все что называется Abstractions, Common, Core, Shared, Helpers, Extentions — дефекты. Т.к. они не относятся к приложению никак. (Хотя я так считаю, но у самого в проекте есть Shared, еще не придумал как от этого избавиться, но думаю)

                      Validation, Events — по идее должны быть рядом с бизнес объектом, зачем выносить отдельно. Они же к чему-то относятся? Не бывает же какой-то абстракной валидации.

                      проекты BLL, DAL И тд считаю дурным тоном.

                      Согласен с вами, но часто видел как это просто называли Core и Persistence (были и еще вариации).

                      Вот к примеру сейчас делаем еще одно приложение, и если открыть проект, то там будут такие каталоги: Dashboard, Sports, Encoding, Tickets. И это именно бизнес сущьности, т.е. это нечто значимое на языке заказчика. И если пойти в Sports, там будут Games, Teams, Players.
                      А если пойти например в Teams, там будут TeamService, Team, TeamsCollection, TeamFilter и т.д.


                      1. Razaz
                        11.09.2016 16:37

                        Не согласен по поводу дефектов.
                        Validation — это механизмы валидации — это cross-cutting функциональность, так же как и события.

                        Мы используем нэйминг вида CompanyName.ProductName.Feature.SubFeature.
                        Пример:

                        SomeCompany.SuperProduct.Identity — это основная сборка с реализацией фичи.
                        SomeCompany.SuperProduct.Identity.Abstractions — контракты и абстракции, которые используются данной фичей и которые можно шарить между сборками.
                        SomeCompany.SuperProduct.Identity.SqlServer — сабфича для поддержки SqlServer.
                        SomeCompany.SuperProduct.Identity.Ldap — сабфича для поддержки Ldap директорий.

                        Структура каждого из проектов однородна:
                        -Services
                        -Models
                        -Events
                        -Helpers
                        -Extensions
                        -ComponentModel
                        -Configuration
                        -Controllers
                        и так далее.

                        При разработке плагинов разработчик имеет доступ к Abstractions и SubFeatures, Но не имеет доступа к основной имплементации. Так же все эти сборки лежат в Nuget.
                        Каждая фича — мини приложение, которое полностью изолировано от другого кода и опирается только на базовые абстракции.
                        Core кстати то же пока есть, но в нем реализации некоторых контрактов из базовых абстракций.
                        Этот дизайн мы начали использовать еще до Asp.Net Core и он показал себя очень хорошо, особенно если проект развивается не один год и над ним работают как внутренние разработчики, так и разработчики плагинов и расширений.
                        Фичей так же является кеширование, месаджинг, шина событий и так далее.
                        Если например надо какой-то внутренний стор реализовать над бд, то это фича приложения — SomeCompany.SuperProduct.Storage.SqlServer, SomeCompany.SuperProduct.Storage.Mongo и так далее.Но фича может сделать opt-out и использовать интеграцию с Oracle для предоставления каких то данных и не использовать внутренний сторадж приложения.

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


    1. DmitrySikorsky
      10.09.2016 18:01

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


      1. Razaz
        10.09.2016 23:18
        +1

        ViewModel в принципе никуда не обращается.
        Все данные должны быть переданы в конструкторе. Его задача представить данные в нужном виде для View и не более.
        Билдеры размывают ответственность и вносят неопределенность в части того, что вы внезапно можете получить пачку запросов в БД.
        В принципе билдер не должен ходить в бд никогда, а все данные для построения принимать из вне ;)


        1. DmitrySikorsky
          11.09.2016 00:10

          Хотелось бы лучше понять ваш подход, т. к. меня действительно беспокоит возможное количество обращений к базе в предложенном мной решении. Если взять для примера страницу, на которой отображается список объектов А, каждый из которых еще отображает объект Б. И если представить, что объекты А отображаются повсеместно на других страницах, т. е. не зависит именно от страницы списка. И объекты Б также используются отдельно. Каким образом строить иерархические структуры и избегать дублирования кода И множества запросов к БД, если все данные должны передаваться в конструктор? А если вложенность моделей видов еще глубже?


          1. Razaz
            11.09.2016 01:45

            Все просто:

            public class VideModel1
            {
                public string Prop1 {get;}
            
                public ViewModel(Model1 model1)
                {
                    Prop1 = model1.Prop1;
                }
            }
            
            public class VideModel2
            {
                public ViewModel1 Model1{get;}
            
                public string Prop2 {get;}
            
                public ViewModel2(Model2 model2, Model1 model1)
                {
                    Prop2 = model2.Prop2;
                    Model1 = new ViewModel1(model1);
                }
            }
            
            public class VideModel3
            {
                public List<ViewModel2> Models2{get;}
            
                public string Prop3 {get;}
            
                public ViewModel3(Model3 model3, params KeyValuePair<Model2, Model1>[] pairs)
                {
                    Prop3 = model3.Prop3;
                    Models2 = pairs.Select(pair=> new ViewModel2(pair.Key, pair.Value)).ToList();
                }
            }
            


            Зачем усложнять? В каждом конкретном методе вы знаете что вам надо достать — пусть метод этим и занимается(ну или сервис какой).
            Комбинировать можно как угодно.


            1. DmitrySikorsky
              11.09.2016 01:53

              Ясно. При таком подходе вы будете снова и снова дублировать логику извлечения объектов откуда-то (например, из базы). Не считая уже того, что для сложных представлений эти конструкторы будут нечитабельными. И для генерации модели вида сложной страницы мы получим здоровенный кусок кода, где все будет в одной куче. Лучше на мой взгляд использовать возможности, вроде Include, для загрузки всего графа объектов доменной модели одним запросом, чтобы не обращаться к БД более 1го раза. Или же делать некий специальный объект-источник всех необходимых доменных объектов, откуда их могут брать все модели вида, подлежащие рендерингу при текущем запросе.


              1. Razaz
                11.09.2016 16:43

                1. Что значит дублировать? Никогда не сталкивался с такими проблемами. Это вопрос к организации получения данных(контроллер/сервисный слой).
                2. С чего вы взяли? Если у вас есть Aggregate Root — достаньте его в контроллере и скормите в конструктор с одним параметром.
                Если вам надо по каким то условиям еще что-то доставать во время создания ViewModel — то это ошибка дизайна.
                То, какие данные отдать клиенту — это знание контроллера/сервиса. И это знание не должно утекать в билдеры или ViewModel.

                Про инклюд вы правильно сказали. Если у вас EF — собирайте все в одном объекте и отдайте ее в конструктор. А он пусть просто рассует то, что ему дали.