Нарушает ли это принцип DRY?

У меня есть 3 модели предметной области - Item, ItemProductLine и ProductLine. Каждый из них соответствует уже существующим таблицам базы данных. У меня также есть модель представления, которую я использую в своем представлении.

Модели предметной области:

public class Item
{
    public string itemId { get; set; }
    public string itemDescription { get; set; }
    public float unitPrice { get; set; }
    // more fields
    public virtual ItemProductLine itemProductLine { get; set; }
}

public class ItemProductLine
{
    public string itemId { get; set; }
    public String productLineId { get; set; }
    // more fields
    public virtual ProductLine productLine { get; set; }
}

public class ProductLine
{
    public string productLineId { get; set; }
    public string productLine { get; set; }
    // more fields
}

Посмотреть модель:

public class ItemViewModel
{
    public string itemNumber { get; set; }
    public String itemDescription { get; set; }
    public Double unitPrice { get; set; }
    public string productLine { get; set; }
}

Мой текущий запрос:

from item in dbContext.Items
where unitPrice > 10
select new ItemViewModel()
{
    itemNumber = item.itemNumber
    itemDescription = item.itemDescription
    unitPrice = item.unitPrice
    productLine = item.itemProductLine.productLine.productLine
}

В настоящее время у меня есть этот запрос в контроллере, но я занимаюсь рефакторингом кода. Я хочу поместить код запроса в класс репозитория на уровне доступа к данным. Из того, что я прочитал, мне не следует ссылаться на какие-либо модели представления в этом слое. Если я изменю select new ItemViewModel() на select new Item(), он вернет ошибку:

Сущность или сложный тип proj.DAL.Item нельзя создать в запросе LINQ to Entities.

Решение, которое я видел, - это создать объект передачи данных (DTO) для передачи данных из моей модели предметной области в мою модель представления.

Однако, сделав это, у меня будет 3 копии данных. Если мне нужно добавить еще одно поле базы данных и отобразить его, мне нужно обновить 3 файла. Я считаю, что нарушаю принцип DRY. Неизбежно ли нарушение принципа DRY при использовании DTO и моделей просмотра? Если нет, не могли бы вы привести пример того, как это сделать, чтобы получить СУХИЙ код?


person janinaj    schedule 24.01.2014    source источник
comment
Почему бы тебе просто не сделать select item. Есть ли причина, по которой вы хотите преобразовать получаемые вами предметы в другие?   -  person Kenneth    schedule 24.01.2014
comment
Вы имеете в виду, что вместо select new Item() я буду использовать select Item()?   -  person janinaj    schedule 24.01.2014
comment
Нет, вы будете использовать select item. Ваш запрос вернет IQueryable<Item>, и вы сможете затем вызвать ToList() для возврата списка из вашего метода репозитория. В этот момент ваш домен будет работать с List<Item>, избавляя вас от необходимости работать с DTO. Сохраните свой отдельный ItemViewModel для вида и, как предложил Майк, используйте AutoMapper для сопоставления между ними.   -  person Brian Cauthon    schedule 24.01.2014


Ответы (3)


Наличие нескольких моделей не является нарушением DRY однако ваш код нарушает принцип разделения проблем, потому что модель предметной области совпадает с (или построена на ней, читай: связана с) моделью персистентности. Вы должны держать свои модели разделенными для каждого слоя и использовать такой инструмент, как automapper, для их сопоставления. Это не позволяет модели служить более чем одной цели.

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

person MikeSW    schedule 24.01.2014
comment
Можете ли вы привести пример того, как я могу разделить модель домена и модель сохранения? Прямо сейчас я использую automapper для сопоставления модели предметной области (DM) и модели представления (VM). Если мне нужно реализовать DTO, я намерен использовать automapper для сопоставления DM и DTO, а также DTO и VM. - person janinaj; 24.01.2014
comment
Простой. Ваша модель предметной области (хорошо, в данном случае, возможно, структуры данных) должна быть построена в соответствии с ожиданиями бизнеса, игнорируя постоянство (ORM, EF-сущности и т. Д.). Модель сохраняемости разработана для базы данных. Во многих приложениях структура бизнес-данных может быть на 100% идентична модели персистентности (orm entity), но это просто совпадение, эти модели все еще разные, потому что они обслуживают 2 уровня с разными проблемами. Вам не нужен DTO, вы просто сопоставляете DM (или модель сохранения для запросов) с VM. - person MikeSW; 24.01.2014

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

Если вы используете EF, ваш DAL на самом деле является Entity Framework, не нужно абстрагироваться от этого. Многие люди пытаются это сделать, но это только сильно усложняет ваш код без всякой выгоды. Если вы выполняли SQL-запросы и вызывали хранимые процедуры напрямую, тогда был бы полезен DAL, но создание абстракции поверх EF (которая является другой абстракцией или над NHibernate) - плохая идея.

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

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

Богатый домен (контроллер на агрегат)

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

Совокупный корень:

public class Item
{
    public string itemId { get; set; }
    public string itemDescription { get; set; }
    public float unitPrice { get; set; }
    // more fields
    public virtual ItemProductLine itemProductLine { get; set; }

    // Example of logic, should always be in your aggregate and not in ItemProductLine for example
    public void UpdatePrice(float newPrice)
    {
       // ... Implement logic
    }
}

Просмотреть модель:

public class ItemViewModel
{
    public int id { get; set; }
    public string itemNumber { get; set; }
    public String itemDescription { get; set; }
    public Double unitPrice { get; set; }
    public string productLine { get; set; }
}

Контроллер:

public class ItemController : Controller
{
    [HttpGet]
    public ActionResult Edit(int id)
    {
       var item = GetById(id);
       // Some logic to map to the VM, maybe automapper, valueinjector, etc.
       var model = item.MapTo<ItemViewModel>();
       return View(model); 
    }

    [HttpPost]
    public ActionResult Update(int id, ItemViewModel model)
    {
       // Do some validation
       if (!model.IsValid)
       {
           View("Edit", model); // return edit view
       }

       var item = GetById(model.id);

       // Execute logic
       item.UpdatePrice(model.unitPrice);
       // ... maybe more logic calls

       Save(item);

       return RedirectToAction("Edit");
    }

    public virtual Item GetById(int id)
    {
        return dbContext.Items.Find(id);
    }

    public virtual bool Save(Item item)
    {
        // probably could/should be abstracted in a Unit of Work
        dbContext.Items.Update(item);
        dbContext.Save();
    }
}

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

В такой архитектуре нужно быть осторожным с границами. Например, у вас может быть контроллер / агрегат продукта и вы хотите перечислить все элементы, связанные с этим продуктом, но он должен быть доступен только для чтения - вы не можете вызывать какие-либо компании по элементам из продуктов - вам нужно перейти к контроллеру элементов для этого. Лучший способ сделать это - автоматически сопоставить ViewModel:

public class ProductController : Controller
{
    // ...

    public virtual IEnumerable<ItemViewModel> GetItemsByProductId(int id)
    {
        return dbContext.Items
            .Where(x => ...)
            .Select(x => x.MapTo<ItemViewModel>())
            .ToList();
        // No risks of editing Items
    }
}

Богатые службы (Контроллер на службу)

Используя богатый набор сервисов, вы создаете абстракцию, более ориентированную на сервисы. Это замечательно, когда бизнес-логика порождает несколько границ и моделей. Сервисы играют роль моста между представлением и моделью. Они НИКОГДА не должны раскрывать базовые модели, только определенные модели представления (которые в этом случае играют роль DTO). Это очень хорошо, когда у вас есть сайт MVC и некоторые REST WebApi, работающие, например, с одним и тем же набором данных, они могут повторно использовать одни и те же службы.

Модель:

public class Item
{
    public string itemId { get; set; }
    public string itemDescription { get; set; }
    public float unitPrice { get; set; }
    // more fields
    public virtual ItemProductLine itemProductLine { get; set; }
}

Просмотреть модель:

public class ItemViewModel
{
    public int id { get; set; }
    public string itemNumber { get; set; }
    public String itemDescription { get; set; }
    public Double unitPrice { get; set; }
    public string productLine { get; set; }
}

Услуга:

public class ItemService
{
    public ItemViewModel Load(int id)
    {
        return dbContext.Items.Find(id).MapTo<ItemViewModel>();
    }

    public bool Update(ItemViewModel model)
    {
        var item = dbContext.Items.Find(model.id);

        // update item with model and check rules/validate
        // ...

        if (valid)
        {            
            dbContext.Items.Update(item);
            dbContext.Save();
            return true;
        }

        return false;
    }
}

Контроллер:

public class ItemController : Controller
{
    public ItemService Service { get; private set; }

    public ItemController(ItemService service)
    {
        this.Service = service;
    }

    [HttpGet]
    public ActionResult Edit(int id)
    {
       return View(Service.Load(id)); 
    }

    [HttpPost]
    public ActionResult Update(int id, ItemViewModel model)
    {
       // Do some validation and update
       if (!model.IsValid || !Service.Update(model))
       {
           View("Edit", model); // return edit view
       }

       return RedirectToAction("Edit");
    }
}

Контроллеры существуют только для вызова Сервисов и составления результатов для Представлений. Они «глупы» по сравнению с контроллерами, ориентированными на домен, но если у вас много сложностей с представлениями (тонны составных представлений, ajax, сложная проверка, обработка json / xml вместе с HTML и т. Д.), Это предпочтительный подход.

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

person Karhgath    schedule 24.01.2014
comment
В вашей службе не должно быть моделей просмотра ... stackoverflow.com/questions/3002168/ - person Martin Dawson; 19.06.2016

Я бы так сделал -

Моя модель домена -

public class Item
{
    // more fields
    public virtual ItemProductLine itemProductLine { get; set; }
}

public class ItemProductLine : ProductLine
{
    // more fields
}

public class ProductLine
{
    // more fields
}

ДАЛ был бы -

    public class ItemRepository
    {
        public Item Fetch(int id)
        {
           // Get Data from Database into Item Model
        }
    }

БАЛ будет -

public class ItemBusinessLayer
{
    public Item GetItem(int id)
    {
       // Do business logic here
       DAL.Fetch(10);
    }
}

Контроллер будет -

public class ItemController : Controller
{
    public ActionResult Index(int id)
    {
       Item _item = BAL.GetItem(10);
       ItemViewModel _itemViewModel = AutomapperExt.Convert(_item); // something where automapper will be invoked for conversion process
       return View(_itemViewModel);
    }
}

Automapper будет храниться в отдельной библиотеке классов.

Основная причина, по которой я выбираю этот способ, заключается в том, что для конкретного бизнеса может быть любое количество приложений / интерфейсов, но их бизнес-модели не должны меняться. Так что мой БАЛ меняться не собираюсь. Он сам возвращает бизнес-домены. Это не означает, что каждый раз, когда мне нужно возвращать модель Item, вместо этого у меня будет MainItemModel, MiniItemModel и т. Д., Все эти модели будут соответствовать бизнес-требованиям сервера.

Теперь интерфейсная часть (возможно, контроллеры) несет ответственность за решение, какой метод BAL вызывать и сколько данных следует использовать во внешнем интерфейсе.

Некоторые разработчики могут возразить, что пользовательский интерфейс не должен обладать этой способностью к суждению, чтобы решать, сколько данных использовать и какие данные просматривать, вместо этого BAL должен иметь такую ​​силу для принятия решения. Я согласен, и это произойдет в самом BAL, если наша модель предметной области будет сильной и гибкой. Если безопасность является основным ограничением, а модели предметной области очень надежны, тогда мы можем выполнить преобразование автомата в самом BAL. Или просто сделайте это на стороне пользовательского интерфейса. В конце концов, MVC делает код более управляемым, чистым, многоразовым и удобным.

person ramiramilu    schedule 24.01.2014