这是否违反DRY原则?

时间:2014-01-24 17:59:01

标签: asp.net-mvc asp.net-mvc-viewmodel data-transfer-objects

我有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(),则会返回错误:

  

无法在LINQ to Entities查询中构造实体或复杂类型“proj.DAL.Item”。

我见过的解决方案是创建一个数据传输对象(DTO),将数据从我的域模型传输到我的视图模型。

然而,通过这样做,我将有3份数据副本。如果我需要添加另一个数据库字段并显示它,我需要更新3个文件。我相信我违反了DRY原则。在使用DTO和查看模型时,违反DRY原则是不可避免的吗?如果没有,你能提供一个如何重构这个以获得DRY代码的例子吗?

3 个答案:

答案 0 :(得分:2)

拥有多个模型is not a DRY violation但是您的代码违反了“关注点分离”原则,因为域模型与(或构建,读取:耦合到)持久性模型相同。您应该为每个图层分隔模型,并使用像automapper这样的工具来映射它们。这可以防止模型服务于多个目的。

它看起来像重复自己,但实际上你保持你的图层分离并确保代码可维护性。

答案 1 :(得分:1)

与ramiramulu不同,我不会引入过多的抽象。

如果您使用EF,您的DAL实际上是实体框架,无需抽象。很多人都试图这样做,但这只会使你的代码复杂化,没有任何好处。如果您正在进行SQL请求并直接调用存储过程,那么DAL会有所帮助,但是在EF(这是另一个抽象,或NHibernate)之上构建抽象是一个坏主意。

此外,作为抽象的纯DTO越来越多,但如果你有一个中间件并且不直接访问数据库就可以使用它们 - 例如,像NServiceBus这样的消息总线:消息会在这种情况下被视为DTO。

除非你做一个非常简单和纯粹的CRUD(在这种情况下,继续把逻辑放在控制器中 - 没有理由为非常简单的业务增加复杂性),你应该确保将业务逻辑移到控制器之外。为此,您有很多选择,但最受欢迎的选项有两种:带有domain driven design的丰富域模型或带有service oriented design的丰富商业服务。他们有很多方法可以做到这一点,但这两种方法说明了截然不同的方法。

Rich Domain(每个聚合控制器)

在第一种情况下,您的控制器将负责获取域对象,调用逻辑并返回视图模型。他们在View世界和Model世界之间架起了桥梁。如何获取域对象需要有点抽象,通常简单的虚拟方法效果很好 - 保持简单。

聚合根:

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
    }
}

丰富的服务(每个服务的控制器)

使用丰富的服务,您可以构建更加面向服务的抽象。当业务逻辑产生多个边界和模型时,这很好。服务扮演着视图和模型之间桥梁的角色。它们永远不应暴露底层模型,只暴露特定的ViewModel(在这种情况下扮演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等),这是首选的方法。 / p>

此外,在这种情况下,服务只能与一个模型相关。如果共享业务逻辑,则相同的服务可以操纵多个模型类型。因此,OrderService可以访问库存并在那里进行调整等。它们比基于模型的更基于流程。

答案 2 :(得分:0)

我会这样做 -

我的域名模型 -

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

public class ItemProductLine : ProductLine
{
    // more fields
}

public class ProductLine
{
    // more fields
}

DAL会 -

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

BAL将是 -

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 将在单独的类库中维护。

我选择这种方式的主要原因是,对于特定的企业,可以有任意数量的应用程序/前端,但他们的业务领域模型不应该更改。所以我的BAL不会改变。它返回业务域本身。这并不意味着每次我需要返回Item模型,而是我将拥有MainItemModel,MiniItemModel等,所有这些模型都将服务器业务需求。

现在,前端(可能是控制器)负责决定调用哪个BAL方法以及在前端使用多少数据。

现在一些开发人员可能会争辩说,UI不应该具有判断能力来决定使用多少数据以及要查看哪些数据,而BAL应该具有决策能力。我同意,如果我们的域模型强大而灵活,那么BAL本身就会发生这种情况。如果安全性是主要约束并且域模型非常坚固,那么我们可以在BAL本身进行自动转换。或者只是在UI方面使用它。结束一天,MVC就是让代码更易于管理,更清晰,可重用和舒适。