产品配置关系

g18*_*18c 6 design-patterns domain-driven-design

我正在创建一个签出流程,其中一步涉及配置产品.用例如下:

产品配置

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

选项组

每个选项组可以包含一个选定的选项(或者不包含),该组包含多个选项.

用户可以添加和删除产品组中的选项.

例如,选项组可以称为数据库.

选项

选项是选项组的特定选项.

作为属于数据库选项组的选项的示例,特定选项可以是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提出的答案

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

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

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

  2. 你提到的Option是一个值对象.

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

  3. 在代码中Option,它需要一个构造函数id和列表dependencies.

    据我所知的Option仅存在作为一个部分OptionGroup(作为OptionIdentity类型需要构件_group类型的OptionGroupIdentity).是否Option允许一个Option可以在另一个OptionGroup聚合实例中的另一个引用?这是否违反了DDD规则,只允许引用聚合根,而不引用内部的东西?

  4. 通常我将聚合根和它们的子实体持久化为整个对象而不是单独的,我通过将对象/列表/字典作为聚合根中的成员来实现.对于Option代码,它需要一组依赖项(类型OptionIdentity[]).

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

Gia*_*sio 5

这是一个很好的问题,即使域模型应该使用专家所谈论的领域的语言,我猜想领域专家不会谈论ProductConfigurations,ProductOptionsGroups和Options.因此,您应该与域上的专家(通常是应用程序的目标用户)进行交谈,以了解他在"纸上"执行此类任务时将使用的术语.

然而,在其余的答案中,我将假设这里使用的术语是正确的.
此外,请注意我的答案是根据您对域的描述建模的,但不同的描述可能会导致一个完全不同的模型.

有界上下文
您有3个有界上下文来建模:

  • 共享内核,包含与合同一样的通用概念.其他BC都将依赖于此.
  • 选项'管理,与OptionsGroups及其依赖项的创建和管理相关(我将使用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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

选项'管理
OptionsManagement你只有一个可变实体被命名OptionGroup,类似这样(C#中的代码,持久性,参数检查和所有......),异常(例如DuplicatedOptionExceptionMissingOptionException)以及当组改变状态时引发的事件.

部分定义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();
    }
}
Run Code Online (Sandbox Code Playgroud)

产品管理
ProductsManagement命名空间中你将拥有 - 一个Option值对象(因此是不可变的),它能够在给定一组先前选择的选项的情况下检查自己的依赖关系 - 一个ProductConfiguration实体,由一个ProductIdentity能够决定应该启用哪些选项的实体来识别选项已启用. - 一些例外,持久性等等......

您在以下(真正简化的)代码示例中可以注意到的是获取Option每个代码示例的s 列表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...
}
Run Code Online (Sandbox Code Playgroud)

域模型应该只包含这样的业务逻辑.

实际上,当且仅当业务逻辑足够复杂以至于值得与其他应用程序关注点(例如持久性)隔离时,才需要域模型.当您需要向域专家付款以了解整个应用程序的内容时,您知道需要域模型.
我使用事件来获得这种隔离,但您可以使用任何其他技术.

因此,回答你的问题:

存储依赖关系映射数据的位置?

存储与DDD无关,但遵循最少知识原则,我只将它们存储在专用于选项管理BC持久性的模式中.域和应用程序的服务可以在需要时简单地查询这些表.

此外

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

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

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

顺便说一下,解决方案总是让事情变得清晰!

编辑以回答新问题

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

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

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

但是,对于我的钱,OptionIdentity应该包含描述,而不是整数.为什么?因为整数不会对域专家说什么."操作系统:102"对任何人都没有意义,而"OS:Debian GNU/Linux"将在日志,异常和头脑风暴中明确表示.

这就是为什么我会用更多面向业务的方法替换你的例子的条款(功能而不是选项组,而不是选项和需求而不是依赖项)的原因:只有当你的业务规则如此复杂时,你才能拥有一个域模型迫使领域专家设计一种新的,通常含糊不清的传统语言来精确表达它们,你需要了解它以构建你的应用程序.

  1. 你提到的Option是一个值对象.

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

嗯,这是一个很好的问题.

身份是我们在关心其变化时用来传达信息的方式.
ProductsManagement上下文中我们不关心Option的演化,我们想要模拟的就是ProductConfiguration进化.事实上,在这方面的Option(或Solution可能是更好的措辞)是一个值,我们希望是不可改变的.

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

  1. 在代码中Option,它需要一个构造函数id和列表dependencies.

    据我所知的Option仅存在作为一个部分OptionGroup(作为OptionIdentity类型需要构件_group类型的OptionGroupIdentity).是否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)
}
Run Code Online (Sandbox Code Playgroud)