我有一个中央域程序集,其中包含各种丰富的域模型。许多业务逻辑,等等。为了使本示例更简单,这里可能是最简单的示例:

public class Location
{
    private int _id;
    public int ID
    {
        get { return _id; }
        private set
        {
            if (value == default(int))
                throw new ArgumentNullException("ID");
            _id = value;
        }
    }

    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentNullException("Name");
            _name = value;
        }
    }

    public string Description { get; set; }

    private string _address;
    public string Address
    {
        get { return _address; }
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentNullException("Address");
            _address = value;

            GeoCoordinates = IoCContainerFactory.Current.GetInstance<Geocoder>().ConvertAddressToCoordinates(Address);
        }
    }

    public Coordinates GeoCoordinates { get; private set; }

    private Location() { }

    public Location(string name, string address)
    {
        Name = name;
        Description = string.Empty;
        Address = address;
    }

    public Location(int id, string name, string description, string address, Coordinates coordinates)
    {
        if (coordinates == null)
            throw new ArgumentNullException("GeoCoordinates");

        ID = id;
        Name = name;
        Description = description;
        Address = address;
    }

    public class Coordinates
    {
        public decimal Latitude { get; private set; }
        public decimal Longitude { get; private set; }

        private Coordinates() { }

        public Coordinates(decimal latitude, decimal longitude)
            : this()
        {
            Latitude = latitude;
            Longitude = longitude;
        }

        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;
            if (!(obj is Coordinates))
                return false;
            var coord = obj as Coordinates;
            return ((coord.Latitude == this.Latitude) &&
                    (coord.Longitude == this.Longitude));
        }

        public override string ToString()
        {
            return string.Format("Latitude: {0}, Longitude: {1}", Latitude.ToString(), Longitude.ToString());
        }
    }
}


出于多种原因,我不想使用这些示例域模型作为我的MVC应用程序中的演示模型。刚开始,我只是为模型创建非常相似的DTO,以用作演示模型。像这样的东西:

public class LocationViewModel
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Address { get; set; }
    public decimal Latitude { get; set; }
    public decimal Longitude { get; set; }
}


但是,这并非对每种视图情况都有意义。例如,一个Create操作不应具有ID属性。 Delete操作不需要所有这些信息。依此类推。

所以现在我要介绍的演示文稿模型与演示文稿本身是一对一的。诸如此类:

public class LocationCreateViewModel
{
    public string Name { get; set; }
    public string Description { get; set; }
    public string Address { get; set; }
}

public class LocationDetailsVieWModel
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Address { get; set; }
    public decimal Latitude { get; set; }
    public decimal Longitude { get; set; }
}


依此类推,为绑定到它们的视图进行了自定义。这变得更加有用,因为我可以使用数据批注更干净地使用ASP.NET MVC工具。这样的事情:

public class LocationCreateViewModel
{
    [Required]
    public string Name { get; set; }
    public string Description { get; set; }
    [Required]
    public string Address { get; set; }
}


这些事情可能会变得更加复杂,但关键是我将它们保留在演示模型中,因为我不认为它们具有放置在业务领域中。我认为,如果实际上没有要求,则在类属性上具有[Required]注释会产生误导,除非使用一组非常特定的工具进行解释。而且由于很多其他事情都在使用这些域模型,而不仅仅是这个MVC网站,所以我想确保逻辑确实融入了模型中,并且没有松散地应用于一组假定的工具。

此设置中的一项经常性功能是在表示模型和域模型之间进行转换。因此,需要转换为领域模型的表示模型上具有实例方法:

public Location ToDomainModel();


,需要从领域模型构建的表示模型上具有静态方法:

public static ConstructFromDomainModel(int id);
public static ConstructFromDomainModel(Location location);



所有这些的最初目标是分离关注点。很多。但是我想知道我在这方面是否采取了错误的措施。这不一定是大量的代码,但是我不希望它变得难以管理。随着越来越多的添加,正在发生某种“类爆炸”。而且单元测试的增长速度甚至更快(一旦我解决了这个问题,它将成为一个问题)。

是否有“更好”的方法?是否有已知的模式可以更好地遵循并且仍可在与工具无关的方法中保持关注点分离?

评论

我想知道类似AutoMapper的东西是否可以为您工作。但是它可能不会完全解决问题,因为它只会消除对转换方法的需要。顺便说一句,这是一个很好的问题-我也想听听别人怎么说。

@AlexSchimp:我曾一度想到过AutoMapper,可能会再次访问它。此实现要注意的另一件事是,它使我可以通过另一种方式使LocationViewModels与Location分离。视图模型与它们填充的视图耦合,并且可以针对这些视图进行高度自定义。因此,单个视图模型可能是用于更复杂视图的少数模型的组合。 (例如,说一个基于位置的视图还需要一些事件数据或用户数据等。我不是发送所有模型,而是创建一个更轻巧的复合视图模型。)

@David您的域模型与您的数据模型(DB)有1:1映射吗?

#1 楼

我今天只是在玩这个概念。我在另一个程序集中定义了一个User类。然后,我创建了三个“基于”该用户类的类(但不派生自该类):CreateUser,EditUser和DetailsUser。每个都包含特定于视图的DataAnnotations(必填,DataType等)。

qq1202078q

CreateUser没有ID,并且具有额外的属性VerifyPassword。我的验证逻辑可确保VerifyPassword == Password。没有ID属性,因为它是一个新用户。在我的“创建”操作中进行验证之后,我可以将其映射到“用户”并将其添加到我的数据存储中。

public class CreateUser
{
    [Required]
    public String FirstName { get; set; }
    [Required]
    public String LastName { get; set; }
    [Required]
    [DataType(DataType.EmailAddress)]
    public String Email { get; set; }
    [Required]
    [DataType(DataType.Password)]
    public String Password { get; set; }
    [Required]
    [DataType(DataType.Password)]
    public String VerifyPassword { get; set; }
}


对于“编辑用户”,我从数据库,并将其映射到EditUser对象。 EditUser具有只读和隐藏的ID,并且没有密码属性。 MVC的模型绑定程序可防止任何人在User对象上注入EditUser对象上不存在的属性。

public class EditUser 
{
    [HiddenInput(DisplayValue = false)]
    public int Id { get; set; }
    [Required]
    public String FirstName { get; set; }
    [Required]
    public String LastName { get; set; }
    [Required]
    [DataType(DataType.EmailAddress)]
    public String Email { get; set; }
}


对于DetailsUser,我做类似的事情,再次隐藏了Password属性。

你对类爆炸是正确的。但是,每个班级都非常小且自成体系。将所有这些保留在ViewModels中的好处是,我可以在视图中自由使用Html.EditorForModel()。对我来说,选择是在ViewModels中增加代码,还是在Views中增加代码。由您决定放置位置。

它确实违反了DRY,具有多个具有重复属性的基于用户的类。我认为它们可能来自一个普通的类,甚至可能来自用户本身。我仍在考虑这个问题,并愿意考虑和提出建议。

关于映射,我一直在研究Moo项目(https://github.com/dclucas/MOO )。它有一个简单的映射器,我发现它比AutoMapper更易于使用。

public class DetailsUser
{
    [HiddenInput(DisplayValue = true)]
    public int Id { get; set; }
    public String FirstName { get; set; }
    public String LastName { get; set; }
    public String Email { get; set; }
    public String Password { get { return "Not Shown"; } }
}


如果属性名称匹配,这将从现有的User对象创建一个EditUser对象。

评论


\ $ \ begingroup \ $
听起来我们就违反DRY的行为得出了相同的结论。现在,我的推理是它在做相同的事情,但是出于非常不同的原因,这是一个临界情况。某些更改将在许多对象之间传播,但是许多更改将被隔离在具有严格职责的微小类中。至于从普通的类派生,我倾向于在总体上转向继承。继承常常让我感到紧密联系。
\ $ \ endgroup \ $
–大卫
2012年9月19日19:20在

\ $ \ begingroup \ $
我忽略提及的一件事是,我通常在数据层中使用实体框架。我还没有弄清楚如何获取部分完成的EditUser对象,并告诉EF仅更新列出的列/属性。当映射回User对象时,所有缺少的属性最终都为null。
\ $ \ endgroup \ $
– MikeC
2012年9月19日19:26

\ $ \ begingroup \ $
请您提供一个示例,说明控制器中的代码,例如edit动作。您是否必须使用用户模型查询数据库,然后在显示之前立即将用户模型转换为EditUser模型?
\ $ \ endgroup \ $
–丹尼·兰彻(Danny Rancher)
2014年11月26日在17:18

#2 楼

我不相信有这样的“正确方法”或“错误方法”(或者也许有错误的方法:))。我认为这完全取决于情况和所需的条件。

但是,我一直是使用ViewModels和DTO的粉丝,因此建议您使用的方法是“接受”方式。当我第一次使用这种方法时,我遇到了与您提到的相同的问题,即某些视图共享数据集,并且我不想到处重复这些属性。在这种情况下,我的方法是使用继承。但是,我最终常常要深入3个层次,一段时间后对象的任何更改都变得困难。

回想起来,如今我对ViewModel的创建略有不同。我读过几篇文章,建议您应该只有一个大的扁平ViewModel并根据需要复制属性。这意味着ViewModel特定于您的需求,尽管您可能会有轻微的类爆炸和重复,但是您可以确信在更改一个viewModel时不会影响项目中的其他任何内容。另外,诸如AutoMapper(由Kevin建议)之类的工具也可以帮助您不必担心模型和ViewModel之间的映射。

但是我仍然喜欢共享公共信息的想法,因此一种替代方法是将属性分解为子ViewModel。因此,在上面的示例中,您可以采用这种方法:

创建一堆包含显式分离数据关注点的Viewmodel:

public class UserInformationViewModel
{
    [Required]
    public String FirstName { get; set; }
    [Required]
    public String LastName { get; set; }
}

public class UserContactDetailsViewModel
{
    [Required]
    [DataType(DataType.EmailAddress)]
    public String Email { get; set; }       
}

public class UserPasswordViewModel
{
    [DataType(DataType.Password)]
    public String Password { get; set; }
    [Required]
    [DataType(DataType.Password)]
    public String VerifyPassword { get; set; }
}


现在使用合成为系统的不同视图需求创建任何顶级视图模型:

public class CreateUserViewModel
{
    private UserInformationViewModel _information;

    public UserInformationViewModel Information
    {
        get { return _information ?? (_information = new UserInformationViewModel()); }
        set { _information = information; }
    }

    private UserContactDetailsViewModel _contactDetails;

    public UserContactDetailsViewModel ContactDetails
    {
        get { return _contactDetails ?? (_contactDetails = new UserContactDetailsViewModel()); }
        set { _contactDetails = information; }
    }
    private UserPasswordViewModel _password;

    public UserPasswordViewModel Verification
    {
        get { return _password ?? (_password = new UserPasswordViewModel()); }
        set { _password = information; }
    }   
}

public class EditUserViewModel
{
    [HiddenInput(DisplayValue = false)]
    [ReadOnly(true)]
    public int Id { get; set; }

    private UserInformationViewModel _information;  
    public UserInformationViewModel Information
    {
        get { return _information ?? (_information = new UserInformationViewModel()); }
        set { _information = information; }
    }

    private UserContactDetailsViewModel _contactDetails;    
    public UserContactDetailsViewModel ContactDetails
    {
        get { return _contactDetails ?? (_contactDetails = new UserContactDetailsViewModel()); }
        set { _contactDetails = information; }
    }   
}

// For details view I would use either inheritence or simply add the Verification attribute onto a new
// class.  Lets go with inheritence for now
public class UserDetailsViewModel 
{
    private UserPasswordViewModel _password;

    public UserPasswordViewModel Verification
    {
        get { return _password ?? (_password = new UserPasswordViewModel()); }
        set { _password = information; }
    }   
}


将模型映射到ViewModel

如Kevin所建议已经有许多出色的工具可以为您完成此任务。我还没有亲自使用过它们,但是我听说过有关AutoMapper的好东西。

将Partials用于子视图模型

因为我们现在已经将不同的元素分为组件,所以我将考虑为每个视图模型创建一个不同的局部视图。这样,即使您的视图也变得可重用,并且您可以共享共同的视图表示。

ie

UserContactDetailsViewModel => _UserContactDetails.cshtml
UserInformationViewModel    => _UserInformation.cshtml
UserPasswordViewModel       => _UserNewPassword.cshtml
UserPasswordViewModel       => _UserEditPassword.cshtml


做到每个视图要求都是

视图模型组成和部分上的陷阱:
关于这种方法的问题是,当我在视图中使用这些viewModels时,创建了结果元素意味着绑定不会重新出现在帖子上。

发生的是,我将这样渲染部分内容。

@Html.Partial("_UserInformation", Model.Information)


直到我查看创建的结果html元素之前,它还是很棒的。

<input id = "Firstname" name="Firstname" type="text" /> // etc


这里的问题是,在绑定回时,封装的viewModel上将没有Firstname元素。输入实际上应该是什么样子?

<input id = "Information_Firstname" name="Information_Firstname" type="text" /> // etc


我最终要做的是创建一个扩展方法,该方法可以为我处理此问题而无需担心任何问题。因此,我最终像这样对代码进行了纠正,以生成正确的元素命名。

@Html.Partial("_UserInformation", model => model.Information)


关于其工作原理,我将让您找出答案,或者如果您沿着这条路线走,我可能会发布它?但是,该部分所做的只是最终打印出所需的内容:

<input id = "Information_Firstname" name="Information_Firstname" type="text" /> // etc


摘要
我喜欢您的方法。您是正确的,在类爆炸中可能是一个问题,但是我认为,系统中明确的关注点分离可能会超过这个问题。至于TDD,我认为应用程序中的每个类都不需要测试。例如,如果您的班级是DTO,则无需进行任何测试,因此,班级爆炸不一定是引起关注的原因(或不遵循此路线的原因)。

好吧,只有我的两分钱。希望您能得到一些答案和评论,使您能够编写出自己感到高兴和自豪的代码。毕竟,这不是我们所追求的:)

评论


\ $ \ begingroup \ $
+2,如果可以的话。这显示了关于视图模型与模型的不同思考方式。我正在学习的东西仍在学习中!
\ $ \ endgroup \ $
–詹姆斯·科里(James Khoury)
2012-09-20 3:06



\ $ \ begingroup \ $
@JamesKhoury为James欢呼。我也在不断学习。我加入该网站的原因之一是获得良好的反馈,不同的想法/意见和更好的做事方式。
\ $ \ endgroup \ $
– dreza
2012年9月20日下午3:59

\ $ \ begingroup \ $
您明确指出赞成组合而非继承,我发现这通常是一个好方法。 (我相信这也是《四人帮》中的一本书。)我也很喜欢将相同的哲学带入观点的想法比通常规定的要多得多,我也必须尝试一下。对于TDD,我想我已经迷上了100%的覆盖范围,但是您说对了,因为DTO不需要直接进行测试。相反,我应该测试使用它们的功能,如果这些测试未发现DTO的某些部分,那么我还是不需要这些部分。
\ $ \ endgroup \ $
–大卫
2012年9月20日在12:18

\ $ \ begingroup \ $
这是一个非常老的线程的复活,但是我发现处理视图组件的更好方法是创建编辑器模板,然后使用@@ Html.EditorFor或显示模板并使用@@ Html.DisplayFor 。与使用partials非常相似,但是在您发布时,MVC会处理逻辑。当存在多个关系/集合时,它也处理逻辑
\ $ \ endgroup \ $
–卡尔
18年1月21日,0:41

#3 楼

简而言之,可以考虑三种类型的模型类:


数据模型类-这些类用于在存储中读取/写入数据。这些类几乎总是与您的表方案完全匹配,再加上一些导航属性。如果使用实体框架(应使用),则将在DbContext上定义这些类的DBSet。这些类几乎从不包含任何代码。


表示类-这些基本上是用于显示或报告数据的数据模型类的非规范化版本。例如,您可能具有包含客户ID和OrderDate的Order数据模型类。但是,客户名称是在客户数据模型类上定义的。如果要显示包含“客户名称”和“订单日期”的网格,则可能需要使用由“订单”数据模型类和“客户”数据模型类的选定属性组成的表示类。这些类通常包含的唯一代码是一个构造函数,该构造函数允许从一个或一个数据模型类实例化该类,并且可能使用一两个方法从表示形式类构造数据模型类。避免在这些类上定义业务逻辑代码。


ViewModel类-这些类是视图的后备类。将它们视为背后的代码。名称中的“模型”一词有点误导,因为这些类与数据或表示模型的关系较小,而与提供属性以使视图正确工作的关系较大。与数据模型类或Presentation模型类不同,ViewModel类通常包含对用户事件做出反应的代码。
ViewModel类包含用于帮助用户输入数据的属性。例如,在订单输入屏幕上,ViewModel可能包含一个属性,该属性是OrderTypes的集合。该集合可用于填充下拉列表。 ViewModel还可以包含OrderType属性,该属性用于存储当前选择的OrderType。相比之下,Order表示类可以包含一个字符串类型的属性,该属性仅显示OrderType的名称。在数据模型类上,OrderType可以由一个整数表示,该整数是OrderType表的外键。您应该避免在ViewModel类上定义业务逻辑。而是调用您的业务逻辑层。


有关此主题的更多信息以及有关如何定义存储库和业务逻辑层的完整文章,请参阅面向服务的文章我的网站www.samwheat.com上使用Entity Framework,MVC和MVVM实现存储库模式的方法。

评论


\ $ \ begingroup \ $
在某些情况下,还有“域模型”或“业务模型”类。它们在您的数据和表示类之间。
\ $ \ endgroup \ $
–汤姆(TomPažourek)
16-09-21在7:37

\ $ \ begingroup \ $
@TomPažourek我认为“领域模型”一词是正确的……这就是我说数据模型时的意思。
\ $ \ endgroup \ $
–山姆
16-09-22在22:21

\ $ \ begingroup \ $
我只是指出,在某些情况下,您具有用于读取/写入存储的单独模型以及用于执行业务逻辑的单独模型。在那些情况下,可以将其视为第四种模型。但是同样,不同的项目将需要不同的抽象,这并不总是有益的。
\ $ \ endgroup \ $
–汤姆(TomPažourek)
2016年9月23日下午5:00

#4 楼

正如@Alex Schimp所建议的那样,AutoMapper是一种出色的工具,适用于这种情况及其确切的用途。我一直都在使用AutoMapper在领域模型和视图模型之间进行翻译,这极大地简化了过程,并且省去了很多用于两者之间翻译的编码。它在处理映射方面做得很好,特别是如果您在域和视图之间使字段名称保持相同。因此,在其中视图可能不需要ID的示例中,AutoMapper将找出它不需要将其映射到视图的原因仅仅是因为它不存在。这样做的另一个好处是,如果您的模型发生了变化,则不必记住也要更新翻译代码。只要有一个清晰的映射,AutoMapper就能弄清楚,则翻译层将被自动处理。如果默认方法不够用,还有一些方法可以在AutoMapper中进行更高级的翻译。您使用DTO的方法非常出色,从长远来看,它将获得易于维护,可扩展性和可扩展性的回报。 Martin Fowler在他的《企业应用程序体系结构模式》一书中讨论了DTO设计模式的好处。