域实体应该作为接口还是作为普通对象公开?
Posted
技术标签:
【中文标题】域实体应该作为接口还是作为普通对象公开?【英文标题】:Should Domain Entities be exposed as Interfaces or as Plain Objects? 【发布时间】:2011-01-22 02:22:23 【问题描述】:应该将域实体公开为接口还是普通对象?
用户界面:
public interface IUser
string FirstName get; set;
string LastName get; set;
string Email get; set;
Role Role get; set;
用户实现(实现到 LinqToSql 数据访问层):
public class User : IUser
public string FirstName get; set;
public string LastName get; set;
public string Email get; set;
public Role Role get; set;
用户实现(在 NHibernate 数据访问层中实现):
[NHibernate.Mapping.Attributes.Class]
public class User : IUser
[NHibernate.Mapping.Attributes.Property]
public string FirstName get; set;
[NHibernate.Mapping.Attributes.Property]
public string LastName get; set;
[NHibernate.Mapping.Attributes.Property]
public string Email get; set;
[NHibernate.Mapping.Attributes.Property]
public Role Role get; set;
这只是说明了一些 DAL 特定的实现,目前没有更好的示例。
【问题讨论】:
【参考方案1】:我对此的感觉是域对象(不是域实体,因为该标题暗示与数据库有关)不应该是接口,除非你有一个非常令人信服的理由相信你会需要在未来某个时候支持多种实现。
假设领域模型是人体模型。从字面上看,业务/服务/文档就是域。我们大多数人都在为单一业务或目的开发软件。如果域模型发生了变化,那是因为业务规则发生了变化,因此旧的域模型不再有效 - 没有理由保留旧的模型,与新的模型一起运行。
这场辩论显然不是非黑即白的。您可能正在开发在多个客户端站点高度定制的软件。您可能确实需要同时实现不同的业务规则集,同时确实需要将它们放入统一的架构中。但是,至少在我的经验中,这些情况是例外而不是规则,虽然我一般不喜欢这个词,但这可能是你应该自己思考的情况,YAGNI。
数据访问是您需要更好抽象的常见领域 (persistence ignorance)。在您的示例中,您的模型类具有 NHibernate 属性。但是添加持久性属性使它不再是一个真正的领域类,因为它引入了对 NHibernate 的依赖。 NHibernate 和 Fluent NHibernate 支持使用外部映射声明而不是数据类上的属性来映射 POCO,这往往是使用诸如 NHibernate 或 EF4 等 ORM 时的首选方法,因为它打破了持久性模型和域模型之间的依赖关系。
如果不支持这些映射方法,而您必须使用属性,那么我可能确实建议使用接口代替,但今天的 ORM 比这更复杂,使用反射和动态代理以及方法拦截来完成大部分繁重的工作,因此您无需在此处创建自己的抽象。
您想要作为接口公开的一些对象类型是:
存储库,负责加载/保存域对象; 程序的插件/扩展; View/presenter 模型,以便插入不同的 UI; 具有多种实现的抽象数据类型(数组、列表、字典、记录集和数据表都是序列 AKAIEnumerable
);
具有多种可能算法的抽象操作(排序、搜索、比较);
通信模型(基于 TCP/IP、命名管道、RS-232 的相同操作);
任何特定于平台的东西,如果您计划部署到多个 (Mac/Windows/*nix)。
这绝不是一个完整的列表,但它应该阐明这里的基本原则,即最适合接口抽象的东西是:
-
取决于您可能无法控制的因素;
未来可能会发生变化;和
是水平特征(用于应用/架构的许多部分)。
域类将被广泛使用,但不属于前两个类别中的任何一个;它不太可能改变,您几乎可以完全控制设计。因此,除非类本身将承担间接依赖关系(这是您应该尽可能避免的情况),否则我不会为领域模型中的每个类创建一个接口而付出额外的努力。
【讨论】:
@Aaronaught :对于特定于 ORM 的好的,现在假设我使用的是全文功能,我将使用也使用属性的 Lucene.NET。据我所知,没有办法像 FluentNhibernate 那样使用外部属性映射。那么,为了让我的域实体成为真正的 POCO,最好的方法是什么?也许接口以这种方式工作? @Yoann。 B:我根本不知道 Lucene.NET 使用了装饰器属性。它绝对不需要它们。如果你想使用它们,那么你有两个选择:要么(a)让你的域模型依赖于 Lucene.NET,这似乎是个坏主意(如果你想使用 SQL FTS 怎么办?如果未来版本的Lucene.NET 是否支持 POCO?),或者 (b) 将您的搜索对象和存储库移动到不同的命名空间/程序集,并使用 AutoMapper 之类的工具将它们转换为域对象。无论哪种方式,我都不认为为对象创建接口会有很大帮助。 @Aaronaught:你说 Lucene.NET 不需要属性?它是关于我发布的另一个问题 (***.com/questions/2356593/…),但是如果不使用 Lucene.NET 的属性,你怎么办? @Aaronaught:我希望我的业务对象是真正的 POCO,正如你所说,如果我想在未来使用 SQL FTS 而不是 Lucene.NET。我不太同意您使用 AutoMapper(或类似的东西)的第二个解决方案,因为如果我使用 NHibernate 的实体业务对象,然后使用 AutoMapper,然后使用 AutoMapper 再一次将 BO 映射到 ViewModel,这是很多映射,并且性能会受到影响。 @Yoann。 B:你可能是对的,但要提防过早的优化;很可能超过 90% 的程序总执行时间将花费在等待数据库查询或其他 I/O 操作上。映射非常非常便宜。话虽如此,如果您使用 NHibernate 的 POCO 映射将实体直接映射到域模型(如果可能的话),我认为您的设计会更简洁。【参考方案2】:接口通常被认为是“契约”,因此会定义行为。另一个主要用途是模拟,以便您可以提供模拟的域实体,而不是来自特定数据源(并且依赖于该源)。
对于一个简单的数据传输对象,我看不到定义接口有很多用处,但我愿意被证明是错误的。我会为此使用简单的对象。
【讨论】:
【参考方案3】:在大多数情况下不应键入实体。
一个实体显式地定义了一个对同一类型的其他实体唯一的身份。 Order 实体应该已经具有与系统中所有其他 Order 实体不同的身份。如果您通过继承对其进行建模,它也可能会损害实体的身份。
例如:假设您有一个Customer
,并且您将Customer
实现为AmericanCustomer
。赋予它成为美国客户所需的所有行为和状态(提示:喜欢购物!)。后来,同样的Customer
,同名,同样的购物习惯——去日本旅行。他们还是AmericanCustomer
吗?您是否创建一个新的JapaneseCustomer
并将包括该客户 ID 在内的所有数据复制到这个新类型中?这可能会产生后果..
这位顾客还喜欢购物吗? JapaneseCustomer
可能是这样,也可能不是。然而,现在突然在一个预订的航班中,这个Customer
变得不同了。系统中的其他对象将使用其唯一 ID 请求相同的 Customer
,并且最终可能会得到与预期不同的对象图片(AmericanCustomer
)。也许客户是围绕国籍而不是当前位置建模的。这可以解决一些问题,但也会引入新问题。
围绕他们的身份为您的实体建模。身份不仅仅是关于 ID 字段。模型不止于此。实体应该以业务逻辑的形式具有有意义的行为,是的,但它们不是服务。您不应该担心多态地分派到不同的行为。更重要的是您的系统如何通过其唯一身份查看此实体。避免考虑实体类型。而是围绕身份进行组织。
【讨论】:
【参考方案4】:普通对象,除非有一个可以更改的实现的通用接口。这就是接口的用途。 Money 或 Address 等值类别不属于该类别。
您的示例界面除了 getter/setter 之外没有任何行为。这不是接口的用途。
【讨论】:
我认为这个特定的例子是一个域实体,而不是一个值对象,因为它标识了一个特定的用户。但这取决于使用情况。 对我来说同样的结论 - 域实体或值对象,接口允许在不影响客户端的情况下更改实现。以上是关于域实体应该作为接口还是作为普通对象公开?的主要内容,如果未能解决你的问题,请参考以下文章