我正在创建一个结帐流程,其中一步涉及配置产品。用例如下:
产品配置
产品配置是一组可配置的选项组。
选项组
每个选项组可以包含一个选定的选项(或者没有),该组由多个选项组成。
用户可以在产品组中添加和删除选项。
例如,选项组可以称为数据库。
方法
选项是选项组的特定选项。
作为属于数据库选项组的选项的示例,特定选项可以是MySQL或MS-SQL。
选项组依赖 选项组可以依赖于另一个选项组,因此如果不满足目标选项组的要求,则会过滤掉特定项目。
只有一个目标依赖项,我们无需担心指向多个目标产品选项组的产品选项组中的选项。
例如,为了允许在数据库产品组中选择MS-SQL选项,必须从“操作系统”选项组中选择Windows选项。
同样,为了允许在数据库产品组上选择MySQL选项,必须从操作系统选项组中选择Windows或Linux选项。
结构
在上图中,MySQL(ID = 201)产品选项依赖于OS产品选项组的Windows(ID = 101)或Linux(ID = 102)产品选项。如果选择了这些操作系统选项中的任何一个,则会显示MySQL。
MS-SQL(ID = 202)产品选项依赖于OS产品选项组的Windows(ID = 101)产品选项。仅当选择Windows操作系统时才会显示MS-SQL。
问题 - 存储依赖关系映射数据的位置?
现在随着代码的发展,问题在于存储产品选项与其组之间的关系依赖关系映射。我质疑的主要问题是:
分开汇总,管理交易
我们是否将映射存储在自己的聚合中,如果是这样,我们如何检测和停止被引用的Products和ProductOptionGroup的删除?
例如,如果操作系统Windows存在依赖关系,我们必须对其进行保护,如果其他OptionGroup依赖于它,则不允许从OS ProductOptionGroup中删除。
这是由应用服务完成的吗?如何在我们的代码中构建一个事务?
内部聚合,更容易的事务管理,更高的并发问题潜力
我们是否将映射存储在OptionGroup聚合中,但是如果我们这样做,如果有人更新了OptionGroup的名称和描述,而另一个用户正在编辑映射数据,则提交时会出现并发异常。
这并不合理,因为如果有人更新名称,地图数据不会失败,它们是两个不相关的概念。
在这种情况下,其他人会做些什么,以及如何最好地构建上述场景的代码?或者我错过了一些更深入的见解,从我的聚合中盯着我看,如果重新设计会让事情变得更容易。
我认为DDD设计禁止从外部访问ProductOptionGroup内的ProductOptions,但我现在无法想到如何以其他方式对其进行建模。
编辑Giacomo Tesio的建议答案
感谢您提出的答案并花时间提供帮助。我真的很喜欢整洁简洁的编码风格。您的回答确实提出了一些问题,如下所示,我可能正在咆哮错误的树,但希望澄清:
在OptionGroup
中,有一个_descriptions
字典,用于包含选项的说明。
为什么选项描述属性不是Option对象的一部分?
您提到Option
是值对象。
在这种情况下,它有一个名为_id
的{{1}}类型的成员,是否允许值对象具有标识ID?
在OptionIdentity
的代码中,它需要Option
的构造函数和id
的列表。
我了解dependencies
仅作为Option
的一部分存在(因为OptionGroup
类型需要OptionIdentity
类型的成员_group
。是否允许一个OptionGroupIdentity
持有对可能位于其他Option
聚合实例中的另一个Option
的引用?这是否违反了DDD规则,只允许引用聚合根,而不引用内部的东西?
通常我将聚合根和它们的子实体持久化为整个对象而不是单独的,我通过将对象/列表/字典作为聚合根中的成员来实现。对于OptionGroup
代码,它需要一组依赖项(类型为Option
)。
如何从存储库中重新水化OptionIdentity[]
?如果它是包含在另一个实体中的实体,那么它是否应该作为聚合根的一部分并传递给Options
的构造函数?
答案 0 :(得分:5)
这是一个精心设计的问题,即使域模型应该使用专家所谈论的域的语言,我猜想领域专家也不会谈论ProductConfigurations,ProductOptionsGroups和Options。因此,您应该与域上的专家(通常是应用程序的目标用户)进行交谈,以了解他在执行此类任务时将使用的术语"在纸上#34;。
然而,在其余的答案中,我会假设这里使用的术语是正确的 此外,请注意我的答案是以您对域名的描述为蓝本的,但不同的描述可能会导致一个完全不同的模型。
有界上下文
你有3个有界的上下文来建模:
OptionsManagement
的命名空间)ProductsManagement
的名称空间用于此BC) 共享内核
这一步很简单,您只需要一些标识符,这些标识符可以用作shared identifiers:
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();
}
}
}
选项&#39;管理强>
在OptionsManagement
中,您只有一个名为OptionGroup
的可变实体,类似于此(C#中的代码,具有持久性,参数检查和所有...),the exceptions(例如DuplicatedOptionException
}}和MissingOptionException
)和the events在群组更改状态时提出。
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();
}
}
<强>产品&#39;管理强>
在ProductsManagement
命名空间中,您将拥有
- 一个Option
值对象(因此是不可变的),它能够在给定一组先前选择的选项的情况下检查自己的依赖关系
- 由ProductConfiguration
标识的ProductIdentity
实体,如果已启用选项,则可以决定应启用哪些选项。
- 一些例外,持久性等......
您在以下(真正简化的)代码示例中可以注意到的是,为每个Option
获取OptionGroupIdentity
的列表,并初始化ProductConfiguration
不属于域本身。实际上,简单的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无关,但在principle of least knowledge后,我只会将它们存储在专用于持久性选项的模式中。管理BC。域和应用程序的服务可以在需要时简单地查询这些表。
此外
我们是否将映射存储在OptionGroup聚合中,但是如果我们这样做,如果有人更新了OptionGroup的名称和描述,而另一个用户正在编辑映射数据,则提交时会出现并发异常。
在你真正遇到这些问题之前,不要害怕这些问题。它们可以通过明确的异常来解决,通知用户。实际上,我并不确定添加依赖项的用户在依赖项更改名称时会认为安全成功提交。
您应该与客户和域专家讨论以确定这一点。
顺便说一句,解决方案始终是明确的事情!
编辑以回答新问题
- 醇>
在
OptionGroup
中,有一个_descriptions
字典,用于包含选项的说明。为什么选项描述属性不是Option对象的一部分?
在OptionGroup
(或Feature
)有界上下文中,没有Option
个对象。这可能看起来很奇怪,一开始甚至是错误的,但是在该上下文中的Option对象在该上下文中不会提供任何附加值。持有描述不足以定义一个类。
但是,对于我的钱,OptionIdentity应该包含描述,而不是整数。为什么? 因为整数不会对域专家说什么。 &#34; OS:102&#34;没有任何意义,而且#34;操作系统:Debian GNU / Linux&#34;将在日志,例外和头脑风暴中明确。
这就是为什么我会用更多面向业务的方式替换你的例子的条款(功能而不是optionGroup,而不是选项和需求而不是依赖)的原因:你只有在你有一个领域模型有一个业务规则如此复杂,而不是强迫领域专家设计一种新的,通常含糊不清的传统语言来精确地表达它们和你需要足够理解它来构建你的应用程序。
- 醇>
您提到
Option
是值对象。在这种情况下,它有一个名为
_id
的{{1}}类型的成员,是否允许值对象具有标识ID?
嗯,这是一个很好的问题。
当我们关心其变化时,身份就是我们用来传达某种东西的东西
在OptionIdentity
背景下,我们并不关心Option的进化,我们只想建模进行ProductsManagement
进化。事实上,在这种情况下ProductConfiguration
(或Option
可能更好的措辞)是一个值我们想要不可变。
这就是为什么我说Option是一个价值对象:我们不关心&#34;操作系统的演变:Debian GNU / Linux&#34;在这种情况下:我们只是想确保手头的ProductConfiguration满足其要求。
- 醇>
在
Solution
的代码中,它需要Option
的构造函数和id
的列表。我了解
dependencies
仅作为Option
的一部分存在(因为OptionGroup
类型需要OptionIdentity
类型的成员_group
。是否允许一个OptionGroupIdentity
持有对可能位于其他Option
聚合实例中的另一个Option
的引用?这是否违反了DDD规则,只允许引用聚合根,而不引用内部的东西?
没有。这就是我设计shared identifiers建模模式的原因。
- 醇>
通常我将聚合根和它们的子实体持久化为整个对象而不是单独的,我通过将对象/列表/字典作为聚合根中的成员来实现。对于
OptionGroup
代码,它需要一组依赖项(类型为Option
)。如何从存储库中重新水化
OptionIdentity[]
?如果它是包含在另一个实体中的实体,那么它是否应该作为聚合根的一部分并传递给Options
的构造函数?
No Option根本不是实体!这是一个价值!
如果您有适当的清理政策,您可以缓存它们。但它们不会由存储库提供:您的应用程序将调用以下应用程序服务以在需要时检索它们。
OptionGroup