【问题标题】:Does this violate the DRY principle?这是否违反了 DRY 原则?
【发布时间】:2014-02-15 20:07:59
【问题描述】:

我有 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 代码?

【问题讨论】:

  • 你为什么不直接做select item。您是否有任何理由要将正在检索的项目转换为不同的项目?
  • 你的意思是我将使用select new Item()而不是select Item()
  • 不,您将使用select item。您的查询将返回IQueryable<Item>,然后您可以在其上调用ToList() 以从您的存储库方法中返回一个列表。届时,您的域将使用List<Item>,从而为您省去了使用 DTO 的麻烦。为视图保留单独的ItemViewModel,并按照迈克的建议,使用 AutoMapper 在两者之间进行映射。

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


【解决方案1】:

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

这看起来像是在重复你自己,但实际上你是在保持你的层解耦并确保代码的可维护性。

【讨论】:

  • 您能否提供一个示例来说明如何分离域和持久性模型?现在,我正在使用 automapper 来映射域模型 (DM) 和视图模型 (VM)。如果我要实现 DTO,我打算使用 automapper 来映射 DM 和 DTO 以及 DTO 和 VM。
  • 简单。您的领域模型(好吧,在这种情况下可能是数据结构)应该根据业务期望忽略持久性(ORM、EF 实体等)构建。持久性模型是为数据库设计的。在许多应用程序中,业务数据结构可以 100% 与持久性模型(orm 实体)相同,但这只是巧合,这些模型仍然不同,因为它们服务于具有不同关注点的 2 层。您不需要 DTO,只需将 DM(或查询的持久性模型)直接映射到 VM
【解决方案2】:

与 ramiramulu 不同,我会避免引入太多抽象。

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

此外,纯 DTO 作为抽象越来越不受欢迎,但如果您有中间件并且不直接访问数据库,则可以使用它们 - 例如,像 NServiceBus 这样的消息总线:消息将被考虑这种情况下的 DTO。

除非您执行非常简单和纯粹的 CRUD(在这种情况下,继续,将逻辑放入控制器中 - 没有理由为非常简单的业务增加复杂性),您肯定应该将业务逻辑移到控制器之外。为此,您有很多选择,但其中两个最流行的是:domain driven design 的富域模型或service oriented design 的丰富业务服务。它们有很多方法可以做到这一点,但这两种方法说明了非常不同的方法。

富域(每个聚合的控制器)

在第一种情况下,您的控制器将负责获取域对象、调用逻辑并返回视图模型。他们在 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
    }
}

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

借助丰富的服务,您可以构建更加面向服务的抽象。当业务逻辑产生多个边界和模型时,这非常有用。服务在 View 和 Model 之间扮演着桥梁的角色。他们不应该公开底层模型,只公开特定的 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 等),这是首选方法。

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

【讨论】:

【解决方案3】:

我会这样做 -

我的领域模型 -

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 就是为了让代码更易于管理、更干净、可重用和舒适。

【讨论】:

    猜你喜欢
    • 2010-11-29
    • 1970-01-01
    • 2015-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多