6

我正在创建一个结帐流程,其中一个步骤涉及配置产品。用例如下:

产品配置

产品配置是一组可配置的选项组。

选项组

每个选项组可以由一个选定的选项(或无)组成,一个组由多个选项组成。

用户可以在产品组中添加和删除选项。

例如,一个选项组可以称为数据库。

选项

选项是选项组的特定选项。

作为属于数据库选项组的选项的示例,特定选项可能是 MySQL 或 MS-SQL。

选项组依赖性 选项组可以依赖于另一个选项组,以便在不满足对目标选项组的要求时过滤掉特定项目。

只有一个目标依赖关系,我们不需要担心一个产品选项组中的选项指向多个目标产品选项组。

例如,为了允许在数据库产品组中选择 MS-SQL 选项,必须从操作系统选项组中选择 Windows 选项。

同样,为了允许在数据库产品组上选择 MySQL 选项,必须从操作系统选项组中选择 Windows 或 Linux 选项。

结构

在此处输入图像描述

在上图中,MySQL (ID = 201) 产品选项依赖于 OS 产品选项组的 Windows (ID = 101) 或 Linux (ID = 102) 产品选项。如果选择了这些操作系统选项中的任何一个,则会显示 MySQL。

MS-SQL (ID = 202) 产品选项依赖于操作系统产品选项组的 Windows (ID = 101) 产品选项。只有在选择 Windows 操作系统时才会显示 MS-SQL。

问题 - 在哪里存储依赖映射数据?

随着代码的发展,现在的问题是在哪里存储产品选项及其组之间的关系依赖映射。我质疑的主要问题是:

分离聚合,管理交易

我们是否将映射存储在其自己的聚合中,如果是,我们将如何检测并停止删除引用的 Products 和 ProductOptionGroups?

例如,如果对操作系统 Windows 存在依赖关系,我们必须保护它,并且如果其他 OptionGroup 对它有依赖关系,则不允许从操作系统 ProductOptionGroup 中删除。

这会由应用程序服务来完成吗?如何在我们的代码中构建事务?

内部聚合,更容易的事务管理,更高的并发问题的可能性

我们是否将映射存储在 OptionGroup 聚合中,但是如果我们这样做,那么如果有人更新了 OptionGroup 的名称和描述,而另一个用户正在编辑映射数据,那么提交时会出现并发异常。

这实际上没有意义,因为如果有人更新名称,映射数据不应该失败,它们是两个不相关的概念。

在这种情况下其他人会怎么做,我将如何最好地构建上述场景的代码?或者我是否错过了一些从我的聚合中盯着我的更深入的洞察力,如果重新设计将使事情变得更容易。

我认为 DDD 设计禁止从外部访问 ProductOptionGroup 内部的 ProductOptions,但我现在想不出如何以任何其他方式对其进行建模。

编辑 Giacomo Tesio 提出的答案

感谢您提出的答案并花时间提供帮助。我真的很喜欢整洁简洁的编码风格。您的回答确实提出了一些进一步的问题,如下所示,我很可能是在吠叫错误的树,但希望能澄清一下:

  1. OptionGroup中,有一个_descriptions字典,用于包含选项的描述。

    为什么选项描述属性不是选项对象的一部分?

  2. 您提到 anOption是一个值对象。

    在这种情况下,它有一个名为_idtype的成员OptionIdentity,是否允许值对象具有标识 ID?

  3. 在 的代码中Option,它采用 的构造函数id和 的列表dependencies

    我理解 aOption仅作为 a 的一部分存在OptionGroup(因为OptionIdentity类型需要 type 的成员_groupOptionGroupIdentity。是否Option允许持有对Option可能位于不同OptionGroup聚合实例中的另一个的引用?这是否违反了 DDD 规则,即仅持有对聚合根的引用而不引用里面的东西?

  4. 通常,我将聚合根及其子实体作为整个对象而不是单独保存,我通过将对象/列表/字典作为聚合根中的成员来做到这一点。对于Option代码,它需要一组依赖项(类型OptionIdentity[])。

    如何Options从存储库中补水?如果它是包含在另一个实体中的实体,那么它不应该作为聚合根的一部分并传递给 的构造函数OptionGroup吗?

4

1 回答 1

5

这是一个很好的问题,即使领域模型应该使用专家谈论的领域语言,我猜领域专家不会谈论 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# 中的代码具有持久性、参数检查和所有...)、
异常例如和)以及组更改其状态时引发的事件。OptionsManagementOptionGroupDuplicatedOptionExceptionMissingOptionException

的部分定义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 的名称和描述,而另一个用户正在编辑映射数据,那么提交时会出现并发异常。

在你真正遇到它们之前,不要害怕这些问题。它们可以简单地通过通知用户的显式异常来解决。事实上,当依赖项更改名称时,我不太确定添加依赖项的用户是否会认为安全成功提交。

您应该与客户和领域专家交谈以决定这一点。

顺便说一句,解决方案总是让事情变得明确!

编辑以回答新问题

  1. OptionGroup中,有一个_descriptions字典,用于包含选项的描述。

    为什么选项描述属性不是选项对象的一部分?

OptionGroup(或Feature)有界上下文中,没有Option对象。乍一看,这可能看起来很奇怪,甚至是错误的,但该上下文中的 Option 对象不会在该上下文中提供任何附加值。持有描述不足以定义一个类。

然而,在我看来,OptionIdentity 应该包含描述,而不是整数。为什么?因为整数不会对领域专家说什么。“OS:102”对任何人都没有任何意义,而“OS:Debian GNU/Linux”将在日志、异常和头脑风暴中明确显示。

这就是为什么我会用更多面向业务的条款(功能而不是选项组,解决方案而不是选项和需求而不是依赖项)替换您示例中的条款的原因:只有当您的业务规则如此复杂时,您才需要域模型迫使领域专家设计一种新的、通常是神秘的、传统的语言来精确地表达它们,而您需要充分理解它来构建您的应用程序。

  1. 您提到 anOption是一个值对象。

    在这种情况下,它有一个名为_idtype的成员OptionIdentity,是否允许值对象具有标识 ID?

嗯,这是个好问题。

当我们关心它的变化时,身份是我们用来交流的东西。
ProductsManagement我们不关心 Option 的演化的上下文中,我们想要建模的只是ProductConfiguration演化。实际上,在这种情况下Option(或可能Solution用更好的措辞)是我们希望成为 immutable的值。

这就是为什么我说 Option 是一个值对象:我们不关心“OS:Debian GNU/Linux”在那种情况下的演变:我们只想确保手头的 ProductConfiguration 满足它的要求。

  1. 在 的代码中Option,它采用 的构造函数id和 的列表dependencies

    我理解 aOption仅作为 a 的一部分存在OptionGroup(因为OptionIdentity类型需要 type 的成员_groupOptionGroupIdentity。是否Option允许持有对Option可能位于不同OptionGroup聚合实例中的另一个的引用?这是否违反了 DDD 规则,即仅持有对聚合根的引用而不引用里面的东西?

不,这就是我设计共享标识符建模模式的原因。

  1. 通常,我将聚合根及其子实体作为整个对象而不是单独保存,我通过将对象/列表/字典作为聚合根中的成员来做到这一点。对于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)
}
于 2013-11-18T16:33:56.150 回答