我已经生成了一些流畅的 NHibernate 代码。它的实体代码如下:
private ISet<CardPlace> _cardPlace;
public MagazineType()
{
_cardPlace = new HashedSet<CardPlace>();
}
public virtual ISet<CardPlace> CardPlace
{
get { return _cardPlace; }
set { _cardPlace = value; }
}
该属性的映射如下:
HasMany(x => x.CardPlace)
.Access.CamelCaseField(Prefix.Underscore)
.Cascade.AllDeleteOrphan()
.Fetch.Select()
.AsSet()
.Inverse()
.LazyLoad()
.KeyColumns.Add("MAGAZINE_ID");
我不明白的是
.Access.CamelCaseField(Prefix.Underscore)
线。为什么它不直接映射到属性而是映射到私有支持字段?这样做有什么理由吗?
如果删除
.Access.CamelCaseField(Prefix.Underscore)
映射将到属性 getter 和 setter。
该行指示流利的 nhiberate 使用该字段。
这里的答案将/应该/可能是:改进领域模型设计。这里的主要辅助词是“封装”。因为:
为什么我们要尝试创建实体模型(包含所有业务/验证规则)...
...同时保持后门打开,即拥有公共设置器。
有一篇非常好的文章,深入探讨了这一点:
让我引用它的摘要(但请仔细阅读该文本)
聚合根边界在理论上很有趣,但您可能看到的许多领域模型仅具有名义上的边界。
如果域模型公开操作和命令只是为了通过直接转到属性设置器来公开绕过这些操作,那么实际上根本没有边界。
...
保留公共设置器的理由通常是“更容易测试”。根据经验,无效的域对象更令人困惑并且更难测试,因为您无法知道在使用应用程序时您已经设置了一个实际上有效的上下文。
文章中的所有论点(请阅读)都浓缩在最后一段:
我们可以通过以下方式避免这种混乱和可能的额外防御性编码:
删除只能通过在我们的域模型上执行的操作和命令来更改的数据的公共设置器。 再次强调,这就是封装的意义所在。我们的模型只公开支持的内容,不允许不支持的内容。
注意:嗯,我确实使用公共设置器,并且我确实使用“借口”,例如“这更适合测试......
封装,正如 Radim Köhler 在上面提到的“位于核心”。但是,让我帮忙举一些实际例子。
首先拥有一个私有支持字段可以允许在获取值或设置值时完成一些工作。
将 DB 数据映射到支持字段允许 getter 执行读取数据时正在完成的工作,并允许设置数据时完成的工作。但是,支持字段将是存储时的原始数据库数据。
您可能会问什么样的工作?假设您将数据作为标志存储在数据库中......您可以将设置器中的标志转换为某种二进制格式,然后在读取时将其从二进制格式转换回来——允许属性将转换封装为存储在数据库中的原始数据。属性可以使用相同的数据库列,对二进制或整数数据库类型进行逻辑运算。有时,您的数据库可能包含多年的数据并与其他系统集成,这不允许您轻松更改数据库格式。您的 getter/setter 可以将其转换为代码中更容易理解的格式。您甚至可以执行一些编码/解码。有很多可能性。
封装也可能涉及复杂性。
假设数据库中存储的内容涉及许多因素——类中许多事物的一些复杂结果,这些结果是由方法以及其他属性、字段、引用对象等的状态产生的。在这种情况下,您可能无法掌握所有内容设置器中的逻辑。您可能有一个私人二传手或根本没有二传手。读取该值可能仍然通过公共或受保护的 getter 进行,需要进行一些少量的工作。在这种情况下,支持字段必须保存映射。当需要工作时,您不能允许公开设置或获取属性,因为有人很容易认为他们可以直接获取或设置值并绕过转换值所需的工作。如果 setter 或 getter 直接映射到数据库中的原始数据,他们甚至可能会尝试再次重复封装在类中的工作。
在处理集合或复杂类型时,有时您需要在 getter 或 setter 中进行转换。您通过属性公开复杂类型,但数据在数据库中被归结为简单类型。组件映射也可以做到这一点。