属性驱动的领域驱动设计





4.00/5 (2投票s)
一个集中式的业务领域,具有共同的基础

引言
本文重点介绍领域驱动设计,集中讨论如何将该领域的一些原则与一种“特性风格”的方法相结合。本文内容部分基于我发表在 2007 年 12 月荷兰版《Microsoft .NET Magazine》上的一篇文章。
背景
领域驱动设计(通常缩写为 DDD)是近来经常遇到的一个话题。有些人无疑会称其为又一个炒作,而且就像任何新生事物一样,伴随着对其优点的争论以及大批纯粹主义的信徒。我无意走上这两条路中的任何一条,也因为我对领域驱动设计的理解可能与 Eric Evans 和 Jimmy Nilsson 等人最初写下的意图大相径庭。我将在本文末尾列出一些供进一步阅读的参考资料。
一些原则
在 DDD 中,领域被置于中心位置,就像一颗行星,而服务则像卫星或月亮一样围绕它旋转。换句话说,服务本身不属于业务领域,但它们使用领域。
领域被明确定义为业务领域,或者说是知识领域,包含领域类和业务规则。此外,领域通过一个或多个仓储(Repository)来暴露以供检索,通过工厂(Factory)来暴露以供创建(生命周期)之用。
一个领域模型应该可以被另一个领域模型替换,理想情况下,应用程序的其余部分保持不变。这个原则并非 DDD 的核心,但它是我希望遵循的一个原则。
以上列出的原则实际上都是更通用的“关注点分离”原则的例子。这并非什么新鲜事,但在领域驱动设计中,你可以称之为所有原则之母。
统一语言(Ubiquitous Language)
鉴于以上原则,你可能会认为 DDD 更像是一种“废话……这是常识”的方法。事实上,确实如此。DDD 实际上并未添加新技术,也未添加新的语言构造,并且作为一种方法论,它也并非真正的新事物。它结合了许多在很多人心中和很多地方都是最佳实践的东西(但说实话,并不总是我们首先想到的,也不总是在正确的位置)。它强调以领域为中心的口号:“业务知识,我的业务,你的业务”。这可能不是你在任何地方都能找到的口号,因为是我刚编的,但其精髓在于将业务对象、规则、词汇和知识集中在一个地方,并向需要它们的服务和应用程序公开。这样做时,要确保每个参与者都说这种语言。它必须是明确的、尽可能简洁的,并且能被每个人流利使用。这就是 Eric Evans 所说的统一语言。
共同基类,共同基础
为了能够遵循上述总结的原则,一个可行的选择是让每个领域模型类都继承自一个共同的基类。在本文附带的解决方案中,这不仅仅是基类和接口之间的选择,而是一个基本的架构规定,它也使得用一个模型替换另一个模型成为可能(上述第三个原则)。因此,我们拥有的不仅仅是一个基类,而是一个完整的基程序集,领域模型从中派生。这个“基础程序集”(basement assembly)为属性和属性集合、关系提供了支持,并且——稍后会变得很明显——也为自定义特性和验证器提供了支持,这些是此处提出的领域模型的第二个基础。

代码库
该解决方案包含三个项目:
Codebasement.DomainDrivenDesign.Basement
:基础程序集Codebasement.DomainDrivenDesign.Model
:一个示例领域模型Codebasement.DomainDrivenDesign.App
:一个简单的测试窗体
基础程序集是一个用于领域模型的迷你框架。它不能引用模型(因为模型引用了基础程序集),因此在必要时,它依赖于反射来从模型中获取信息,并且大部分包含可以在领域模型类中使用的通用构造。既然代码通常比文字更有说服力,让我们来看一个从 BasementObject
派生的领域类的例子。
using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
using Codebasement.DomainDrivenDesign.Basement.Attributes;
using Codebasement.DomainDrivenDesign.Basement.Basement;
using Codebasement.DomainDrivenDesign.Basement.Converters;
using Codebasement.DomainDrivenDesign.Basement.Enums;
using Codebasement.DomainDrivenDesign.Basement.Utilities;
using Codebasement.DomainDrivenDesign.Model.Editors;
using Codebasement.DomainDrivenDesign.Model.Enums;
using Codebasement.DomainDrivenDesign.Model.ModelClasses;
using Codebasement.DomainDrivenDesign.Model.Validators;
namespace Codebasement.DomainDrivenDesign.Model.ModelClasses
{
/// <summary>
/// This class contains the definition of a server.
/// </summary>
[Serializable, DisplayName("Server"), DefaultProperty("HostName")]
[XmlInclude(typeof (Reservation))]
[Relatable(BasementObjectType = typeof (Reservation), Cardinality = 0,
CardinalityType = CardinalityType.CardinalityOrMore)]
[Linkable]
public class Server : BasementObject
{
private const ServerType _defaultServerType = ServerType.WEBSVR;
/// <summary>
/// Initializes a new instance of the server class.
/// </summary>
public Server()
{
ParameterCollection.Add(new Parameter(this, "ServerType"));
ParameterCollection.Add(new Parameter(this, "HostName"));
ParameterCollection.Add(new Parameter(this, "IPAddress"));
}
/// <summary>
/// Initializes a new instance of the server class.
/// </summary>
protected Server(SerializationInfo info, StreamingContext context)
: this()
{
ConstructParameters(info);
}
/// <summary>
/// Gets or sets the name of the host.
/// </summary>
[Category("Identification")]
[Mandatory]
[Description("The hostname of the server.")]
[RegularExpression(RegexExpressions.HostNameExclusionFormat,
RegexMatchType.MatchExclusions, RegexOptions.Compiled,
RegexExpressions.HostNameExclusionFormatHelp)]
[RefreshProperties(RefreshProperties.All)]
[StringLength(Min = 4, Max = 40)]
public string HostName
{
get { return ParameterCollection["HostName"].Value; }
set
{
if (PropertyValidators.ValidateStringLength(value, 4, 40))
ParameterCollection["HostName"].Value = value;
}
}
/// <summary>
/// Gets or sets the IP address.
/// </summary>
[Category("Identification")]
[Description("Internet Protocol Address")]
[RegularExpression(RegexExpressions.IPAddressFormat,
RegexMatchType.MatchFormat, RegexOptions.Compiled,
RegexExpressions.IPAddressFormatHelp)]
public string IPAddress
{
get { return ParameterCollection["IPAddress"].Value; }
set
{
if (PropertyValidators.ValidateIPAddress(value))
ParameterCollection["IPAddress"].Value = value;
}
}
/// <summary>
/// Gets or sets the type of the server.
/// </summary>
/// <value>The type of the server.</value>
[Category("Server Type"), DisplayName("Main Server Usage")]
[DefaultValue(_defaultServerType)]
[TypeConverter(typeof (EnumDescriptionConverter))]
[Editor(typeof (ServerTypeEditor), typeof (UITypeEditor))]
public ServerType ServerType
{
get
{
string val = ParameterCollection["ServerType"].Value;
return (ServerType)PropertyValidators.ValidateEnum(typeof(ServerType),
val, _defaultServerType);
}
set { ParameterCollection["ServerType"].Value = value.ToString(); }
}
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
public override string ToString()
{
return (!String.IsNullOrEmpty(HostName)
? HostName
:
base.ToString());
}
}
}
参数和属性
为了有一种通用的方式来持久化属性并实现统一的验证方式,领域模型实例的属性被表示为参数,捆绑在基类的 ParameterCollection
上。参数的内部值类型是 string
,以允许任何类型,但字面量被隐藏在属性的 getter 和 setter 中,并且除了作为有意义的返回类型外,从不公开。
属性
如你所见,这里的领域类上使用了大量特性;标准特性如 Serializable
、DefaultProperty
、DisplayName
、Description
、DefaultValue
和 Category
。此外,类型转换器和类型编辑器也是扩展领域模型表达能力的非常有用的工具。我不会深入探讨这些标准特性(即 .NET Framework 中提供的特性),有很多文章解释了它们的用法和实现。
除了标准特性,上面的代码中还可见几个自定义特性:
Mandatory
:属性是否必须有值StringLength
:字符串值属性的最大允许长度RegularExpression
:属性值是否必须符合正则表达式(或不符合,即排除的字符)Relatable
:一个类实例是否可以与另一个类实例相关联,以及如何关联Linkable
:一个类实例是否可以链接到另一个类实例(通过引用重用)
其中一些用于验证目的(例如,在 Microsoft Patterns and Practices Enterprise Library 的验证应用程序块中也可以找到类似的变体)。一些旨在表达关系和行为的特征,如复制、链接等。
验证模式
如上所示的特性的强大之处在于它们可以被非常通用地使用,例如,对这里提出的领域模型应用验证。类、属性、关系和任何其他类型都可以在一个统一的地方进行验证,这个地方就是 BasementObject
基类。下面是一个 Validate
方法的代码,它为任何领域类实例(或一次性为所有实例,即在一个包含其他可关联实例的容器实例上)构建一个运行中的 ValidationResults
集合。模板模式被应用于一个受保护的 ValidateCore
方法,该方法可用于领域模型类中特定的业务规则。违反规则的情况可以在那里进行验证并添加到 ValidationResults
集合中。
/// <summary>
/// Validates this instance.
/// </summary>
public void Validate()
{
if (_validationResults != null)
_validationResults.Clear();
else
_validationResults = new ValidationResultCollection();
AddValidationResults(new ObjectRelationValidator().Validate(this));
AddValidationResults(new ObjectTypeValidator().Validate(this));
AddValidationResults(new ObjectPropertyValidator().Validate(this));
ValidateCore();
}
/// <summary>
/// ValidateCore : overridable in derived classes
/// </summary>
protected virtual void ValidateCore()
{
}
序列化和仓储
附带解决方案中的持久化实现很简单,基于直接的序列化。这里使用的 XmlInclude
特性是为标准的 XmlSerializer
准备的。在我们的例子中,需要额外的用于序列化的构造函数,因为基类包含了一个用于序列化(非标准实现的)参数和关系的自定义实现。
通常情况下,一种“一次性全部搞定”的序列化模式当然是不够的,这需要引入到关系模型或其他存储模型的映射。一个重要的方面是让持久化由一个或多个仓储来管理,可能与提供者模式相结合。这里我们有一个简单的仓储实现,用于基于“容器”的直接序列化。这通常需要被一个更广泛的仓储所取代,该仓储可以从领域模型加载和保存信息,其中的“容器”将被替换为精心选择的聚合边界(例如,客户及其订单和订单项作为一个连贯且可事务处理的“聚合”)。
using Codebasement.DomainDrivenDesign.Basement.Enums;
using Codebasement.DomainDrivenDesign.Basement.Serialization;
using Codebasement.DomainDrivenDesign.Model.ModelClasses;
namespace Codebasement.DomainDrivenDesign.Model.Repository
{
/// <summary>
/// Repository for access and persistence of the model
/// </summary>
public class ModelRepository
{
private readonly ISerializationProvider _provider;
/// <summary>
/// Initializes a new instance of the ModelRepository class.
/// </summary>
public ModelRepository(SerializationType serializationType)
{
switch (serializationType)
{
case SerializationType.Binary:
_provider = new BinarySerializationProvider();
break;
case SerializationType.Soap:
_provider = new SoapSerializationProvider();
break;
case SerializationType.Xml:
_provider = new XmlSerializationProvider();
break;
}
}
/// <summary>
/// Saves the specified basement object.
/// </summary>
public void Save(InformationContainer containerObject,
string fileName)
{
_provider.Save(containerObject, fileName);
}
/// <summary>
/// Loads the specified file name.
/// </summary>
public InformationContainer Load(string fileName)
{
return _provider.Load(typeof(InformationContainer), fileName)
as InformationContainer;
}
}
}
工厂
仓储用于检索和持久化,工厂用于创建。下面是一个用于创建和删除领域对象的基本工厂示例。请注意,这两个简单的方法可以创建或移除模型中的任何领域对象,它们是通用的。你也可以让 CreateObject
递归调用自身,仅根据领域模型中关于包含的可关联对象的规则来创建嵌套关系。
对于一个更真实的领域,你很可能需要更具体的工厂方法,这些方法利用关于领域的知识并提供更好的起点。
/// <summary>
/// Creates the object.
/// </summary>
public static BasementObject CreateObject(
Type type,
BasementObject parentObject)
{
BasementObject newObject =
Activator.CreateInstance(type) as BasementObject;
if (newObject != null)
{
newObject.ObjectIdentifier = Guid.NewGuid();
newObject.OnBasementObjectCreated(new BasementObjectEventArgs(newObject));
}
if (parentObject != null)
{
if (parentObject.Relatables.Contains(type))
parentObject.Relations.Add(newObject);
else
throw new InvalidOperationException(
String.Format("Type {0} not relatable to object {1}",
type, parentObject));
}
return newObject;
}
/// <summary>
/// Removes the object.
/// </summary>
public static void RemoveObject
(BasementObject basementObject, BasementObject parentObject)
{
if (!(parentObject != null &
basementObject != null)) return;
parentObject.Relations.Remove(basementObject);
basementObject.OnBasementObjectRemoved(new BasementObjectEventArgs(basementObject));
}
示例
以下屏幕截图显示了使用该领域模型的示例应用程序。treeview
使用仓储填充,工厂支持创建和删除实例,验证结果在列表视图中可见。

参考文献
致谢
BasementObject
类的一部分以及几个自定义特性要归功于 Mark Achten 和 Patrick Peters。我还使用了 CodeProject 上一篇文章中的 Vista 风格进度条:C# 中的 Vista 风格进度条。
结论
代码库可以也应该进行许多改进。然而,这里的重点是应用一些领域驱动设计原则,大量使用标准和自定义特性,并实现一个从基础程序集派生的领域模型,该基础程序集为任何领域模型提供了共同的基础。领域驱动设计的精髓在于将业务领域重新置于其应有的聚光灯下。
历史
- 2008年4月6日:初次发布