Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>This is a well formulated question, even if a domain model should use the language that the domain that the experts talk, and I would guess that domain experts don't talk about ProductConfigurations, ProductOptionsGroups and Options. Thus, you should get a talk with an expert on the domain (tipically a target user of the application) to understand the terms he would use while doing such task "on paper".</p> <p>However in the rest of the answer I will assume that the term used here are correct.<br> Moreover, note that my answer is modeled after your description of the domain, but a different description could lead to a deeply different model.</p> <p><strong>Bounded Context</strong><br> You have 3 bounded context to model:</p> <ul> <li>A shared kernel, that contains common concept that works like contracts. Both the other BC will depend on this.</li> <li>Options' Management, related to the creation and management of the OptionsGroups and their dependencies (I would use a namespace named <code>OptionsManagement</code> for this BC)</li> <li>Products' Management, related to the creation and management of the Products' Configurations (I would use a namespace named <code>ProductsManagement</code> for this BC)</li> </ul> <p><strong>Shared kernel</strong><br> This step is easy, you just need a few identifiers here, that will work as <a href="http://epic.tesio.it/doc/manual/shared_identifiers.html">shared identifiers</a>:</p> <pre class="lang-cs prettyprint-override"><code>namespace SharedKernel { public struct OptionGroupIdentity : IEquatable&lt;OptionGroupIdentity&gt; { 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 &amp;&amp; Equals((OptionGroupIdentity)obj); } public override int GetHashCode() { return _name.GetHashCode(); } public override string ToString() { return _name; } } public struct OptionIdentity : IEquatable&lt;OptionIdentity&gt; { 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) &amp;&amp; _id == other._id; } public override bool Equals(object obj) { return obj is OptionIdentity &amp;&amp; Equals((OptionIdentity)obj); } public override int GetHashCode() { return _id.GetHashCode(); } public override string ToString() { return _group.ToString() + ":" + _id.ToString(); } } } </code></pre> <p><strong>Options' Management</strong><br> In <code>OptionsManagement</code> you have only one mutable entity named <code>OptionGroup</code>, something like this (code in C# with persistence, argument checks and all...), <a href="http://epic.tesio.it/2013/03/04/exceptions-are-terms-ot-the-ubiquitous-language.html">the exceptions</a> (such as <code>DuplicatedOptionException</code> and <code>MissingOptionException</code>) and <a href="http://epic.tesio.it/doc/manual/observable_entities.html">the events</a> raised when the group change it's state.</p> <p>A partial definition of <code>OptionGroup</code> could be something like</p> <pre class="lang-cs prettyprint-override"><code>public sealed partial class OptionGroup : IEnumerable&lt;OptionIdentity&gt; { private readonly Dictionary&lt;OptionIdentity, HashSet&lt;OptionIdentity&gt;&gt; _options; private readonly Dictionary&lt;OptionIdentity, string&gt; _descriptions; private readonly OptionGroupIdentity _name; public OptionGroupIdentity Name { get { return _name; } } public OptionGroup(string name) { // validation here _name = new OptionGroupIdentity(name); _options = new Dictionary&lt;OptionIdentity, HashSet&lt;OptionIdentity&gt;&gt;(); _descriptions = new Dictionary&lt;OptionIdentity, string&gt;(); } public void NewOption(int option, string name) { // validation here OptionIdentity id = new OptionIdentity(option, this._name); HashSet&lt;OptionIdentity&gt; requirements = new HashSet&lt;OptionIdentity&gt;(); if (!_options.TryGetValue(id, out requirements)) { requirements = new HashSet&lt;OptionIdentity&gt;(); _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&lt;OptionIdentity&gt; GetRequirementOf(int option) { // validation here OptionIdentity id = new OptionIdentity(option, this._name); return _options[id]; } public IEnumerator&lt;OptionIdentity&gt; GetEnumerator() { return _options.Keys.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } </code></pre> <p><strong>Products' Management</strong><br> In the <code>ProductsManagement</code> namespace you will have - an <code>Option</code> value object (thus immutable) that is able to check his own dependencies given a set of previously selected options - A <code>ProductConfiguration</code> entity, identified by a <code>ProductIdentity</code> that is able to decide which Options should be enabled given the options already enabled. - A few exceptions, persistence and so on...</p> <p>What you can note in the following (really simplified) code sample is that obtaining the list of <code>Option</code>s for each <code>OptionGroupIdentity</code>, and initializing the <code>ProductConfiguration</code> is out of the domain itself. Indeed simple SQL queries or custom application code can handle both.</p> <pre class="lang-cs prettyprint-override"><code>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&lt;OptionIdentity&gt; 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&lt;OptionIdentity&gt; _selectedOptions; public ProductConfiguration(ProductIdentity name, OptionGroupIdentity[] optionsToSelect) { // validation here _name = name; _optionsToSelect = optionsToSelect; } public ProductIdentity Name { get { return _name; } } public IEnumerable&lt;OptionGroupIdentity&gt; 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&lt;ProductIdentity&gt; { 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 &amp;&amp; Equals((ProductIdentity)obj); } public override int GetHashCode() { return _name.GetHashCode(); } public override string ToString() { return _name; } } // Exceptions, Events and so on... } </code></pre> <p><strong>The domain model should only contains business logic like this.</strong></p> <p>Indeed, you need a domain model if and only if the business logic is complex enough to worth isolation from the rest of applicative concerns (like persistence, for example). You know you need a domain model when you need to pay a domain expert to understand what the whole application is about.<br> I use events to obtains such isolation but you can use any other technique.</p> <p>Thus, to answer your question:</p> <blockquote> <p>Where to store the dependency mapping data?</p> </blockquote> <p>Storage is not that relevant in DDD, but following the <a href="http://en.wikipedia.org/wiki/Law_of_Demeter">principle of least knowledge</a> I would store them only in the schema dedicated to the persistence of the options' management BC. Domain's and application's services could simply query such tables when they need them.</p> <p>Moreover</p> <blockquote> <p>Do we store the mapping within the OptionGroup aggregate, however if we do then if someone updated the name and description of an OptionGroup, whilst another user was editing the mapping data, then there would be a concurrency exception on commit.</p> </blockquote> <p>Don't be afraid of such issues until you actually meet them. They can simply be solved with an explicit exception that inform the user. Indeed I'm not so sure that the user adding a dependency would consider safe a successful commit when a dependency change names.</p> <p>You should talk to the customer and to the domain expert to decide this.</p> <p>And BTW, <strong>the solution is ALWAYS to make things explicit!</strong></p> <p><strong>Edit to answer the new questions</strong> </p> <blockquote> <ol> <li><p><strong>In <code>OptionGroup</code>, there is a <code>_descriptions</code> dictionary, this is used to contain the descriptions of the Options.</strong></p> <p>Why is the option description property not part of the Option object?</p></li> </ol> </blockquote> <p>In the <code>OptionGroup</code> (or <code>Feature</code>) bounded context, there's no <code>Option</code> object. This might look strange, even wrong at first, but an Option object in that context wouldn't provide any added value in that context. Holding a description is not enough to define a class.</p> <p>To my money, however, OptionIdentity should contain the description, not an integer. Why? Because the integer won't say anything to the domain expert. "OS:102" has no meaning for anyone, while "OS:Debian GNU/Linux" will be explicit in logs, exceptions and brainstorms.</p> <p>That's the same reason why I would replace the terms of your example with more business oriented ones (feature instead of optionGroup, solution instead of option and requirement instead of dependency): you nead a domain model only if you have a business rules so complex than forced the domain experts to design a new, often cryptic, conventional language to express them precisely <strong>and</strong> you need to understand it enough to build your application.</p> <blockquote> <ol> <li><p><strong>You mentioned an <code>Option</code> is a value object.</strong></p> <p>In this case it has a member called <code>_id</code> of type <code>OptionIdentity</code>, are value objects allowed to have an identifying Id?</p></li> </ol> </blockquote> <p>Well, this is a good question. </p> <p>An identity is what we use to communicate something when we care about its changes.<br> In the <code>ProductsManagement</code> context we don't care about Option's evolution, all we want to <strong>model</strong> there is <code>ProductConfiguration</code> evolution. Indeed in that context the <code>Option</code> (or <code>Solution</code> with a <em>probably</em> better wording) is a value that <strong>we want to be immutable</strong>.</p> <p>That's why I said that Option is a value object: we don't care about the evolution of "OS:Debian GNU/Linux" in that context: we just want to ensure that its requirements are met by the ProductConfiguration at hand.</p> <blockquote> <ol> <li><p><strong>In the code for <code>Option</code>, it takes a constructor of <code>id</code>, and list of <code>dependencies</code>.</strong></p> <p>I understand an <code>Option</code> only exist as part of an <code>OptionGroup</code> (as the <code>OptionIdentity</code> type requires the member <code>_group</code> of type <code>OptionGroupIdentity</code>). Is one <code>Option</code> allowed to hold a reference to another <code>Option</code> that could be inside a different <code>OptionGroup</code> aggregate instance? Does this violate the DDD rule of holding references only to aggregate roots and not reference the things inside?</p></li> </ol> </blockquote> <p>No. That's why I designed the <a href="http://epic.tesio.it/doc/manual/shared_identifiers.html">shared identifiers</a> modelling patterns.</p> <blockquote> <ol> <li><p><strong>Typically I have persisted aggregate roots and their child entities as the whole object and not separately, I do this by having the object/list/dictionary as a member within the aggregate root. For the <code>Option</code> code, it takes a set of dependencies (of type <code>OptionIdentity[]</code>).</strong></p> <p>How would <code>Options</code> be rehydrated from the repository? If it is an entity contained within another entity, then should it not come as part of the aggregate root and be passed in to the constructor of <code>OptionGroup</code>?</p></li> </ol> </blockquote> <p>No Option is not an entity at all! It's a value!</p> <p>You can cache them, if you have proper clean-up policy. But they wont be provided by a repository: your application will call an application service like the following to retrieve them when needed.</p> <pre class="lang-cs prettyprint-override"><code>// documentation here public interface IOptionProvider { // documentation here with expected exception IEnumerable&lt;KeyValuePair&lt;OptionGroupIdentity, string&gt;&gt; ListAllOptionGroupWithDescription(); // documentation here with expected exception IEnumerable&lt;Option&gt; ListOptionsOf(OptionGroupIdentity group); // documentation here with expected exception Option FindOption(OptionIdentity optionEntity) } </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload