jri*_*zzo 7 asp.net-mvc separation-of-concerns mongodb
据我所知,MVC中关注点分离的"正确"结构是为您的视图构建视图模型,并为您选择的存储库中的数据模型提供独立的数据模型.我开始尝试使用MongoDB,并且我开始认为在使用无架构的NO-SQL样式数据库时这可能不适用.我想将此场景呈现给stackoverflow社区,看看每个人的想法是什么.我是MVC的新手,所以这对我来说很有意义,但也许我忽略了一些......
以下是我讨论的示例:当用户想要编辑他们的个人资料时,他们会转到UserEdit视图,该视图使用下面的UserEdit模型.
public class UserEditModel
{
public string Username
{
get { return Info.Username; }
set { Info.Username = value; }
}
[Required]
[MembershipPassword]
[DataType(DataType.Password)]
public string Password { get; set; }
[DataType(DataType.Password)]
[DisplayName("Confirm Password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
[Required]
[Email]
public string Email { get; set; }
public UserInfo Info { get; set; }
public Dictionary<string, bool> Roles { get; set; }
}
public class UserInfo : IRepoData
{
[ScaffoldColumn(false)]
public Guid _id { get; set; }
[ScaffoldColumn(false)]
public DateTime Timestamp { get; set; }
[Required]
[DisplayName("Username")]
[ScaffoldColumn(false)]
public string Username { get; set; }
[Required]
[DisplayName("First Name")]
public string FirstName { get; set; }
[Required]
[DisplayName("Last Name")]
public string LastName { get; set; }
[ScaffoldColumn(false)]
public string Theme { get; set; }
[ScaffoldColumn(false)]
public bool IsADUser { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
请注意,UserEditModel类包含一个继承自IRepoData的UserInfo实例?UserInfo是保存到数据库的内容.我有一个通用的存储库类,它接受任何继承IRepoData形式的对象并保存它; 所以我只是打电话Repository.Save(myUserInfo)
来完成它.IRepoData定义了_id(MongoDB命名约定)和时间戳,因此存储库可以基于_id进行upsert,并根据Timestamp检查冲突,以及对象刚刚保存到MongoDB的其他任何属性.在大多数情况下,视图只需要使用@Html.EditorFor
,我们很高兴!基本上,只有视图需要的任何内容都会进入基础模型,只有存储库需要的任何内容才能获得[ScaffoldColumn(false)]
注释,而其他所有内容在两者之间都是相同的.(顺便说一句 - 用户名,密码,角色和电子邮件都保存到.NET提供程序,这就是为什么它们不在UserInfo对象中.)
这种情况的巨大优势是双重的......
我可以使用更少的代码,因此更易于理解,开发更快,更易于维护(在我看来).
我可以在几秒钟内重新计算 ...如果我需要添加第二个电子邮件地址,我只需将其添加到UserInfo对象 - 它只是通过向对象添加一个属性而添加到视图并保存到存储库.因为我使用的是MongoDB,所以我不需要改变我的数据库模式或弄乱任何现有数据.
鉴于此设置,是否需要制作用于存储数据的单独模型?你们都认为这种方法的缺点是什么?我意识到显而易见的答案是标准和关注点分离,但是你能想到的任何现实世界的例子会证明这会引起一些令人头疼的问题吗?
值得注意的是,我正在开发一个由两个开发人员组成的团队,所以很容易看到好处并忽略了一些标准.您是否认为在较小的团队中工作会对这方面产生影响?
无论使用何种数据库系统,MVC中的视图模型的优势都存在(即使您不使用数据库系统也是如此).在简单的CRUD情况下,您的业务模型实体将非常接近地模仿您在视图中显示的内容,但除了基本的CRUD之外,情况并非如此.
其中一个重要问题是业务逻辑/数据完整性问题,使用与视图中使用的相同的数据建模/持久性类.DateTime DateAdded
在您的用户类中拥有属性的情况下,表示添加用户的时间.如果您提供一个直接挂钩到您的UserInfo
类的表单,您最终会得到一个动作处理程序,如下所示:
[HttpPost]
public ActionResult Edit(UserInfo model) { }
Run Code Online (Sandbox Code Playgroud)
您很可能不希望用户在添加到系统时能够进行更改,因此您首先想到的是不在表单中提供字段.
但是,出于两个原因,你不能依赖它.首先,DateAdded
如果你做了一个new DateTime()
或它将会得到的结果将是相同的null
(对于这个用户,这两种方式都是不正确的).
第二个问题是用户可以在表单请求中欺骗它并添加&DateAdded=<whatever date>
到POST数据,现在您的应用程序将DB中的DateAdded字段更改为用户输入的内容.
这是设计,因为MVC的模型绑定机制查看通过POST发送的数据,并尝试自动将它们与模型中的任何可用属性连接.它无法知道发送的属性不是原始形式,因此它仍然会将其绑定到该属性.
ViewModels没有这个问题,因为您的视图模型应该知道如何将自身转换为数据实体或从数据实体转换,并且它没有DateAdded
要欺骗的字段,它只有显示(或接收)它的数据所需的最小字段.
在您的确切场景中,我可以轻松地使用POST字符串操作重现这一点,因为您的视图模型可以直接访问您的数据实体.
在视图中直接使用数据类的另一个问题是,当您尝试以不太适合数据建模方式的方式呈现视图时.例如,假设您为用户提供以下字段:
public DateTime? BannedDate { get; set; }
public DateTime? ActivationDate { get; set; } // Date the account was activated via email link
Run Code Online (Sandbox Code Playgroud)
现在让我们说作为管理员,您对所有用户的状态感兴趣,并且您希望在每个用户旁边显示状态消息,并根据该用户的状态给出管理员可以执行的不同操作.如果您使用数据模型,您的视图代码将如下所示:
// In status column of the web page's data grid
@if (user.BannedDate != null)
{
<span class="banned">Banned</span>
}
else if (user.ActivationDate != null)
{
<span class="Activated">Activated</span>
}
//.... Do some html to finish other columns in the table
// In the Actions column of the web page's data grid
@if (user.BannedDate != null)
{
// .. Add buttons for banned users
}
else if (user.ActivationDate != null)
{
// .. Add buttons for activated users
}
Run Code Online (Sandbox Code Playgroud)
这很糟糕,因为您的视图中有很多业务逻辑(禁用的用户状态始终优先于激活的用户,禁止的用户由具有禁止日期的用户定义等等).它也复杂得多.
相反,更好的(至少是imho)解决方案是将用户包装在ViewModel中,该ViewModel具有状态的枚举,当您将模型转换为视图模型时(视图模型的构造函数是一个很好的地方)可以插入您的业务逻辑一次以查看所有日期并找出用户应该处于什么状态.
然后,上面的代码简化为:
// In status column of the web page's data grid
@if (user.Status == UserStatuses.Banned)
{
<span class="banned">Banned</span>
}
else if (user.Status == UserStatuses.Activated)
{
<span class="Activated">Activated</span>
}
//.... Do some html to finish other columns in the table
// In the Actions column of the web page's data grid
@if (user.Status == UserStatuses.Banned)
{
// .. Add buttons for banned users
}
else if (user.Status == UserStatuses.Activated)
{
// .. Add buttons for activated users
}
Run Code Online (Sandbox Code Playgroud)
在这个简单的场景中,这可能看起来不像代码更少,但是当确定用户状态的逻辑变得更复杂时,它会使事情更易于维护.您现在可以更改用户状态的确定逻辑,而无需更改数据模型(您不必因为查看数据的方式而更改数据模型),并且可以将状态确定保存在一个位置.
应用程序中至少有3层模型,有时可以安全地组合,有时则不能.在问题的上下文中,可以将持久性和域模型组合在一起,但不能组合视图模型.
您描述的场景可以直接使用任何实体模型.它可以使用Linq2Sql模型作为您的ViewModel,实体框架模型,休眠模型等.重点是您希望将持久模型直接用作视图模型.正如您所提到的那样,关注点的分离并没有明确强迫您避免这样做.实际上,关注点分离甚至不是构建模型层的最重要因素.
在典型的Web应用程序中,至少有3个不同的模型层,尽管将这些层组合成单个对象是可能的,有时也是正确的.模型层从最高级别到最低级别是您的视图模型,域模型和持久性模型.您的视图模型应该准确描述视图中的内容,不多也不少.您的域模型应该准确描述您的完整系统模型.您的持久性模型应该准确描述您的域模型的存储方法.
ORM有许多形状和大小,具有不同的概念目的,而MongoDB 就像你描述的那样只是其中之一.大多数人承诺的错觉是你的持久性模型应该与你的域模型相同,而ORM只是从数据存储到域对象的映射工具.对于简单的场景来说,这当然是正确的,在这些场景中,您的所有数据都来自一个地方,但最终会受到限制,并且您的存储会降级为更适合您情况的内容.当这种情况发生时,模型往往变得截然不同.
在决定是否可以将域模型与持久性模型分开时,要遵循的一条经验法则是,您是否可以在不更改域模型的情况下轻松更换数据存储.如果答案是肯定的,它们可以合并,否则它们应该是单独的模型.存储库界面自然适合于从任何可用的数据存储中提供域模型.一些较新的重量轻的ORM,如短小精悍和大规模的,使得它非常容易使用你的域模型作为自己持久的模型,因为他们为了不要求特定的数据模型来执行持续性,你只是直接写入查询,并让ORM只处理映射.
在阅读方面,视图模型也是一个独特的模型层,因为它们代表了您的域模型的一个子集,但是您需要为了向页面显示信息.如果你想显示用户的信息,链接到他的所有朋友,当你将鼠标悬停在他们的名字上时,你会得到一些关于该用户的信息,你的持久性模型直接处理,即使使用MongoDB,也可能会非常疯狂.当然,并非每个应用程序都在每个视图上显示这样的互连数据集合,有时域模型正是您想要显示的内容.在这种情况下,没有理由将具有您想要显示的对象的映射的额外权重加到具有相同属性的特定视图模型中.在简单的应用程序中,如果我想要做的就是扩充域模型,我的视图模型将直接从域模型继承并添加我想要显示的额外属性.话虽如此,在您的MVC应用程序变大之前,我强烈建议您使用视图模型进行布局,并使所有基于页面的视图模型都继承自该布局模型.
在写入方面,视图模型应该只允许您希望对访问视图的用户类型进行编辑.不要将管理员视图模型发送到非管理员用户的视图.如果您自己编写此模型的映射层以考虑访问用户的权限,您可以逃避这一点,但这可能比创建继承自常规视图模型并增加它的第二个管理模型更有用.使用管理员属性.
最后关于你的观点:
当实际上更容易理解时,更少的代码只是一个优势.它的可读性和理解能力是写作者的技能的结果.有一些着名的短代码示例,甚至可以让开发人员花费很长时间来剖析和理解.大多数这些例子来自巧妙编写的代码,这些代码不易理解.更重要的是,您的代码100%符合您的规范.如果您的代码很短,易于理解和可读,但不符合规范,那么它就毫无价值.如果它完全符合规范,但很容易被利用,那么规范和代码就毫无价值.
安全地在几秒钟内重构是代码编写良好的结果,而不是它的简洁性.只要您的规范正确地符合您的目标,遵循DRY原则将使您的代码易于重构.对于模型层,您的域模型是编写良好,可维护且易于重构的代码的关键.您的域模型将根据业务需求的变化而变化.您的业务需求变化是一个很大的变化,必须注意确保新的规范经过充分考虑,设计,实施,测试等.例如,您今天要说的是要添加第二个电子邮件地址.您仍然需要更改视图(除非您使用某种脚手架).此外,如果明天您需要更改需求以添加对最多100个电子邮件地址的支持,该怎么办?您最初提出的更改对于任何系统来说都相当简单,更大的更改需要更多的工作.
归档时间: |
|
查看次数: |
1666 次 |
最近记录: |