这是一个很好的问题,即使领域模型应该使用专家谈论的领域语言,我猜领域专家不会谈论 ProductConfigurations、ProductOptionsGroups 和 Options。因此,您应该与该领域的专家(通常是应用程序的目标用户)交谈,以了解他在“纸上”执行此类任务时会使用的术语。
但是,在其余答案中,我将假设此处使用的术语是正确的。
此外,请注意,我的答案是根据您对领域的描述建模的,但不同的描述可能会导致完全不同的模型。
有界上下文
您有 3 个有界上下文要建模:
- 一个共享内核,它包含像合约一样工作的通用概念。另一个 BC 都将依赖于此。
- 选项管理,与选项组及其依赖项的创建和管理有关(我将使用
OptionsManagement
为此 BC 命名的命名空间)
- 产品管理,与产品配置的创建和管理有关(我将使用
ProductsManagement
为此 BC 命名的命名空间)
共享内核
这一步很简单,您只需要这里的一些标识符,它们将用作共享标识符:
namespace SharedKernel
{
public struct OptionGroupIdentity : IEquatable<OptionGroupIdentity>
{
private readonly string _name;
public OptionGroupIdentity(string name)
{
// validation here
_name = name;
}
public bool Equals(OptionGroupIdentity other)
{
return _name == other._name;
}
public override bool Equals(object obj)
{
return obj is OptionGroupIdentity
&& Equals((OptionGroupIdentity)obj);
}
public override int GetHashCode()
{
return _name.GetHashCode();
}
public override string ToString()
{
return _name;
}
}
public struct OptionIdentity : IEquatable<OptionIdentity>
{
private readonly OptionGroupIdentity _group;
private readonly int _id;
public OptionIdentity(int id, OptionGroupIdentity group)
{
// validation here
_group = group;
_id = id;
}
public bool BelongTo(OptionGroupIdentity group)
{
return _group.Equals(group);
}
public bool Equals(OptionIdentity other)
{
return _group.Equals(other._group)
&& _id == other._id;
}
public override bool Equals(object obj)
{
return obj is OptionIdentity
&& Equals((OptionIdentity)obj);
}
public override int GetHashCode()
{
return _id.GetHashCode();
}
public override string ToString()
{
return _group.ToString() + ":" + _id.ToString();
}
}
}
选项管理您只有一个名为 的可变实体,类似这样(C# 中的代码具有持久性、参数检查和所有...)、
异常(例如和)以及组更改其状态时引发的事件。OptionsManagement
OptionGroup
DuplicatedOptionException
MissingOptionException
的部分定义OptionGroup
可能类似于
public sealed partial class OptionGroup : IEnumerable<OptionIdentity>
{
private readonly Dictionary<OptionIdentity, HashSet<OptionIdentity>> _options;
private readonly Dictionary<OptionIdentity, string> _descriptions;
private readonly OptionGroupIdentity _name;
public OptionGroupIdentity Name { get { return _name; } }
public OptionGroup(string name)
{
// validation here
_name = new OptionGroupIdentity(name);
_options = new Dictionary<OptionIdentity, HashSet<OptionIdentity>>();
_descriptions = new Dictionary<OptionIdentity, string>();
}
public void NewOption(int option, string name)
{
// validation here
OptionIdentity id = new OptionIdentity(option, this._name);
HashSet<OptionIdentity> requirements = new HashSet<OptionIdentity>();
if (!_options.TryGetValue(id, out requirements))
{
requirements = new HashSet<OptionIdentity>();
_options[id] = requirements;
_descriptions[id] = name;
}
else
{
throw new DuplicatedOptionException("Already present.");
}
}
public void Rename(int option, string name)
{
OptionIdentity id = new OptionIdentity(option, this._name);
if (_descriptions.ContainsKey(id))
{
_descriptions[id] = name;
}
else
{
throw new MissingOptionException("OptionNotFound.");
}
}
public void SetRequirementOf(int option, OptionIdentity requirement)
{
// validation here
OptionIdentity id = new OptionIdentity(option, this._name);
_options[id].Add(requirement);
}
public IEnumerable<OptionIdentity> GetRequirementOf(int option)
{
// validation here
OptionIdentity id = new OptionIdentity(option, this._name);
return _options[id];
}
public IEnumerator<OptionIdentity> GetEnumerator()
{
return _options.Keys.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
产品管理
在ProductsManagement
命名空间中,您将拥有 - 一个Option
值对象(因此是不可变的),它能够在给定一组先前选择的选项的情况下检查他自己的依赖关系 - 一个ProductConfiguration
由 a 标识的实体,ProductIdentity
能够决定在给定的情况下应该启用哪些选项选项已启用。- 一些例外,持久性等等......
您可以在以下(非常简化的)代码示例中注意的是,获取Option
每个 s的列表OptionGroupIdentity
,并初始化sProductConfiguration
超出域本身。事实上,简单的 SQL 查询或自定义应用程序代码都可以处理。
namespace ProductsManagement
{
public sealed class Option
{
private readonly OptionIdentity _id;
private readonly OptionIdentity[] _dependencies;
public Option(OptionIdentity id, OptionIdentity[] dependencies)
{
// validation here
_id = id;
_dependencies = dependencies;
}
public OptionIdentity Identity
{
get
{
return _id;
}
}
public bool IsEnabledBy(IEnumerable<OptionIdentity> selectedOptions)
{
// validation here
foreach (OptionIdentity dependency in _dependencies)
{
bool dependencyMissing = true;
foreach (OptionIdentity option in selectedOptions)
{
if (dependency.Equals(option))
{
dependencyMissing = false;
break;
}
}
if (dependencyMissing)
{
return false;
}
}
return true;
}
}
public sealed class ProductConfiguration
{
private readonly ProductIdentity _name;
private readonly OptionGroupIdentity[] _optionsToSelect;
private readonly HashSet<OptionIdentity> _selectedOptions;
public ProductConfiguration(ProductIdentity name, OptionGroupIdentity[] optionsToSelect)
{
// validation here
_name = name;
_optionsToSelect = optionsToSelect;
}
public ProductIdentity Name
{
get
{
return _name;
}
}
public IEnumerable<OptionGroupIdentity> OptionGroupsToSelect
{
get
{
return _optionsToSelect;
}
}
public bool CanBeEnabled(Option option)
{
return option.IsEnabledBy(_selectedOptions);
}
public void Select(Option option)
{
if (null == option)
throw new ArgumentNullException("option");
bool belongToOptionsToSelect = false;
foreach (OptionGroupIdentity group in _optionsToSelect)
{
if (option.Identity.BelongTo(group))
{
belongToOptionsToSelect = true;
break;
}
}
if (!belongToOptionsToSelect)
throw new UnexpectedOptionException(option);
if (!option.IsEnabledBy(_selectedOptions))
throw new OptionDependenciesMissingException(option, _selectedOptions);
_selectedOptions.Add(option.Identity);
}
public void Unselect(Option option)
{
if (null == option)
throw new ArgumentNullException("option");
bool belongToOptionsToSelect = false;
foreach (OptionGroupIdentity group in _optionsToSelect)
{
if (option.Identity.BelongTo(group))
{
belongToOptionsToSelect = true;
break;
}
}
if (!belongToOptionsToSelect)
throw new UnexpectedOptionException(option);
if (!_selectedOptions.Remove(option.Identity))
{
throw new CannotUnselectAnOptionThatWasNotPreviouslySelectedException(option, _selectedOptions);
}
}
}
public struct ProductIdentity : IEquatable<ProductIdentity>
{
private readonly string _name;
public ProductIdentity(string name)
{
// validation here
_name = name;
}
public bool Equals(ProductIdentity other)
{
return _name == other._name;
}
public override bool Equals(object obj)
{
return obj is ProductIdentity
&& Equals((ProductIdentity)obj);
}
public override int GetHashCode()
{
return _name.GetHashCode();
}
public override string ToString()
{
return _name;
}
}
// Exceptions, Events and so on...
}
领域模型应该只包含这样的业务逻辑。
实际上,当且仅当业务逻辑足够复杂以值得与其他应用程序关注点(例如持久性)隔离时,您才需要域模型。当您需要向领域专家支付费用以了解整个应用程序的内容时,您知道您需要一个领域模型。
我使用事件来获得这种隔离,但您可以使用任何其他技术。
因此,要回答您的问题:
在哪里存储依赖映射数据?
存储在 DDD 中没有那么重要,但按照最少知识的原则,我只会将它们存储在专用于选项管理 BC 持久性的模式中。域和应用程序的服务可以在需要时简单地查询这些表。
而且
我们是否将映射存储在 OptionGroup 聚合中,但是如果我们这样做,那么如果有人更新了 OptionGroup 的名称和描述,而另一个用户正在编辑映射数据,那么提交时会出现并发异常。
在你真正遇到它们之前,不要害怕这些问题。它们可以简单地通过通知用户的显式异常来解决。事实上,当依赖项更改名称时,我不太确定添加依赖项的用户是否会认为安全成功提交。
您应该与客户和领域专家交谈以决定这一点。
顺便说一句,解决方案总是让事情变得明确!
编辑以回答新问题
在OptionGroup
中,有一个_descriptions
字典,用于包含选项的描述。
为什么选项描述属性不是选项对象的一部分?
在OptionGroup
(或Feature
)有界上下文中,没有Option
对象。乍一看,这可能看起来很奇怪,甚至是错误的,但该上下文中的 Option 对象不会在该上下文中提供任何附加值。持有描述不足以定义一个类。
然而,在我看来,OptionIdentity 应该包含描述,而不是整数。为什么?因为整数不会对领域专家说什么。“OS:102”对任何人都没有任何意义,而“OS:Debian GNU/Linux”将在日志、异常和头脑风暴中明确显示。
这就是为什么我会用更多面向业务的条款(功能而不是选项组,解决方案而不是选项和需求而不是依赖项)替换您示例中的条款的原因:只有当您的业务规则如此复杂时,您才需要域模型迫使领域专家设计一种新的、通常是神秘的、传统的语言来精确地表达它们,而您需要充分理解它来构建您的应用程序。
您提到 anOption
是一个值对象。
在这种情况下,它有一个名为_id
type的成员OptionIdentity
,是否允许值对象具有标识 ID?
嗯,这是个好问题。
当我们关心它的变化时,身份是我们用来交流的东西。
在ProductsManagement
我们不关心 Option 的演化的上下文中,我们想要建模的只是ProductConfiguration
演化。实际上,在这种情况下Option
(或可能Solution
用更好的措辞)是我们希望成为 immutable的值。
这就是为什么我说 Option 是一个值对象:我们不关心“OS:Debian GNU/Linux”在那种情况下的演变:我们只想确保手头的 ProductConfiguration 满足它的要求。
在 的代码中Option
,它采用 的构造函数id
和 的列表dependencies
。
我理解 aOption
仅作为 a 的一部分存在OptionGroup
(因为OptionIdentity
类型需要 type 的成员_group
)OptionGroupIdentity
。是否Option
允许持有对Option
可能位于不同OptionGroup
聚合实例中的另一个的引用?这是否违反了 DDD 规则,即仅持有对聚合根的引用而不引用里面的东西?
不,这就是我设计共享标识符建模模式的原因。
通常,我将聚合根及其子实体作为整个对象而不是单独保存,我通过将对象/列表/字典作为聚合根中的成员来做到这一点。对于Option
代码,它需要一组依赖项(类型OptionIdentity[]
)。
如何Options
从存储库中补水?如果它是包含在另一个实体中的实体,那么它不应该作为聚合根的一部分并传递给 的构造函数OptionGroup
吗?
No Option 根本不是实体!这是一个价值!
如果您有适当的清理策略,您可以缓存它们。但它们不会由存储库提供:您的应用程序将在需要时调用如下应用程序服务来检索它们。
// documentation here
public interface IOptionProvider
{
// documentation here with expected exception
IEnumerable<KeyValuePair<OptionGroupIdentity, string>> ListAllOptionGroupWithDescription();
// documentation here with expected exception
IEnumerable<Option> ListOptionsOf(OptionGroupIdentity group);
// documentation here with expected exception
Option FindOption(OptionIdentity optionEntity)
}