我有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代码的例子吗?
答案 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就是让代码更易于管理,更清晰,可重用和舒适。