0

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

4

3 回答 3

2

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

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

于 2014-01-24T18:09:09.063 回答
1

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

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

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

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

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

在第一种情况下,您的控制器将负责获取域对象、调用逻辑并返回视图模型。他们在 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 可以访问库存并在那里进行调整等。它们更多地基于流程而不是基于模型。

于 2014-01-24T20:36:25.693 回答
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 不会改变。它返回业务域本身。这并不意味着每次我需要返回项目模型,而是我将拥有 MainItemModel、MiniItemModel 等,所有这些模型都将服务于业务需求。

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

现在一些开发人员可能会争辩说,UI 不应该有判断能力来决定使用多少数据和查看什么数据,而是 BAL 应该有这种决定权。我同意,如果我们的域模型强大且灵活,那么 BAL 本身就会发生这种情况。如果安全是主要约束并且域模型非常坚固,那么我们可以在 BAL 本身进行自动映射器转换。或者只是将它放在 UI 端。归根结底,MVC 就是为了让代码更易于管理、更干净、可重用和舒适。

于 2014-01-24T18:35:10.773 回答