Product Configuration Relations

I am creating a validation process, one of which includes product customization. Use cases are as follows:

Product configuration

A product configuration is a set of custom groups of options.

Option group

Each group of parameters can consist of one selected option (or it is not), the group consists of several parameters.

The user can add and remove parameters from the product group.

As an example, a group of options can be called Bases.

Option

A parameter is a special variant of a group of parameters.

As an example, in the case of options belonging to the database parameter group, the specific parameters can be MySQL or MS-SQL.

Dependence of an option group Option groups can be dependent on one other parameter group, so individual elements are filtered out if the requirement for the target parameter group is not met.

There is only one target dependency, we don’t need to worry about the parameters in the product parameter group that points to several parameter groups of the target product.

For example, to enable MS-SQL selection in the database product group, you must select the Windows option from the operating system options group.

Similarly, for the MySQL option to be selected in the database product group, the Windows or Linux options must be selected in the operating system options group.

Structure

enter image description here

In the above diagram, the MySQL product parameter (ID = 201) depends on the parameters of the Windows product (ID = 101) or Linux (ID = 102) in the OS product parameter group. If one of these operating system options is selected, MySQL is displayed.

The MS-SQL product parameter (ID = 202) is dependent on the Windows product product (ID = 101) in the OS product parameter group. Only when you select the Windows operating system will MS-SQL be displayed.

Question. Where to store dependency mapping data?

The question of how code is now evolving is where to store the mapping of the relationship between the product parameters and their groups. The main questions I ask are as follows:

Split aggregate, manage transactions

Do we keep the display in our own aggregate, if so, how will we detect and stop the deletions of the Products and the ProductOptionGroups referenced?

For example, if there is a dependency on the Windows operating system, we must protect it and not allow removal from OS ProductOptionGroup, if other OptionGroups have dependencies on it.

Will this be done by the application service? How to build a transaction in our code?

Inside the aggregate, simplified transaction management, higher potential for concurrency problems

Do we keep the mapping in the OptionGroup aggregate, however, if we do this, if someone updated the OptionGroup name and description while another user edited the mapping data, then there would be a concurrency exception in commit.

This makes no sense, since the data mappings should not fail if someone updates the name, these are two unrelated concepts.

What would others do in this situation and how would I better structure the code for the above scenarios? Or I miss a deeper understanding, looking at me from my aggregates, that if redesigning is easier.

I think access to ProductOptions inside the ProductOptionGroup from the outside is prohibited by the DDD design, but I cannot think about how to model it in any other way at this time.

Edit for proposed by Giacomo Tesio

Thanks for the suggested answer and for taking the time to help. I really like the neat and concise coding style. Your answer raises some additional questions, as shown below, I may well bark the wrong tree, but I will be grateful for the clarification:

  • OptionGroup has a OptionGroup dictionary, which is used to describe parameter descriptions.

    Why is the parameter description property not part of the Option object?

  • You mentioned Option - a value object.

    In this case, it has a member called _id type OptionIdentity , are value objects allowed by an identifier?

  • In the code for Option it takes an id constructor and a dependencies list.

    I understand that Option exists only as part of OptionGroup (because the OptionIdentity type requires a _group member of the _group type). Is one Option allowed to refer to another Option that might be inside another instance of the OptionGroup aggregate? Does this violate the DDD rule for storing links to aggregate roots only and not refer to things inside?

  • Usually I saved aggregated roots and their children as an entire object, and not separately, I do this by having the object / list / dictionary as a member in the aggregate root. For Option code, it accepts a set of dependencies (of type OptionIdentity[] ).

    How would Options be rehydrated from storage? If this is an entity contained in another object, should it not be part of the aggregate root and passed to the OptionGroup constructor?

+6
source share
1 answer

This is a well-formulated question, even if the domain model should use the language that the experts speak, and I would suggest that the domain experts do not talk about ProductConfigurations, ProductOptionsGroups and Options. Therefore, you should speak with a domain expert (typically the target user of the application) to understand the terms that he would use to complete such a task "on paper".

However, in the remainder of the answer, I will assume that the term used here is correct.
Also, please note that my answer is modeled after your domain description, but another description can lead to a deeply different model.

Limited context
You have 3 limited context for the model:

  • A common core that contains a common concept that works like contracts. Both other aircraft will depend on this.
  • Managing options related to creating and managing OptionsGroups and their dependencies (I would use a namespace called OptionsManagement for this BC)
  • Product management related to creating and managing product configurations (I would use a namespace called ProductsManagement for this BC)

Joint core
This step is simple, you just need some identifiers here, which will work as common 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(); } } } 

Settings Management
In OptionsManagement you only have one mutable object named OptionGroup , something like this (C # code with saving, checking arguments and all ...), exceptions (for example, DuplicatedOptionException and MissingOptionException ) and events raised when the group changed .

A partial definition of OptionGroup may be something like

 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(); } } 

Product management
In the ProductsManagement namespace you will have - an Option value object (thus immutable) that can check its own dependencies, given the set of previously selected parameters - a ProductConfiguration object identified by ProductIdentity , which can decide which parameters should be activated if the parameters are already included. - A few exceptions, perseverance, etc.

What you can notice in the following (really simplified) example is getting an Option list for each OptionGroupIdentity , and initializing the ProductConfiguration is outside the domain itself. Indeed, simple SQL queries or custom application code can handle both.

 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... } 

The domain model should contain only such business logic.

Indeed, you need a domain model if and only if the business logic is complex enough to cost isolation from the rest of the applied problems (for example, persistence). You know that you need a domain model when you need to pay a domain expert to understand what an entire application is.
I use events to get this isolation, but you can use any other technique.

So, to answer your question:

Where to store dependency mapping data?

Storage does not apply to DDD, but following the principle of least knowledge, I would only save them in a scheme dedicated to maintaining control of BC options. Domain and application services can simply query such tables when they need them.

Besides

Do we keep the mapping in the OptionGroup aggregate, however, if we do this, if someone updated the OptionGroup name and description while another user edited the mapping data, then there would be a concurrency exception in commit.

Do not be afraid of such problems until you meet them. They can simply be resolved with a clear exception informing the user. In fact, I'm not sure if the user adding the dependency will consider a safe successful commit when changing the dependency names.

You must speak with a client and domain expert to resolve this.

And BTW, the solution is ALWAYS to make things explicit!

Change the answer to new questions

  • OptionGroup has a OptionGroup dictionary, which is used to describe parameter descriptions.

    Why is the parameter description property not part of the Option object?

In a limited context, OptionGroup (or Feature ) does not have an Option object. At first this may seem strange, even wrong, but the Option object in this context will not provide added value in this context. It is not enough storage of the description for definition of a class.

To my money, however, OptionIdentity should contain a description, not an integer. What for? Because the integer will not say anything to the domain expert. "OS: 102" doesn't matter to anyone, and "OS: Debian GNU / Linux" will be explicit in journals, exceptions, and brainstorming.

The same thing, why I would replace the terms of your example with more business-oriented ones (a function instead of a Group option, instead of options and requirements instead of a dependency): you are not a domain model, only if you have business rules so complex that it forced domain experts to develop a new, often mysterious, ordinary language to express them accurately and you need to understand this enough to create your application.

  • You mentioned the Option value object.

    In this case, it has a member called _id type OptionIdentity , are value objects identifiers?

Ok, that’s a good question.

Identity is what we use to communicate something when we care about its changes.
In the context of ProductsManagement we do not need the evolution of options, all we want the model to develop is ProductConfiguration . Indeed, in this context, Option (or Solution with probably the best wording) is the value that we want to be unchanged .

That's why I said that Option is an object of value: we don’t care about the evolution of “OS: Debian GNU / Linux” in this context: we just want to make sure that its requirements are met using ProductConfiguration.

  • In the code for Option it takes an id constructor and a dependencies list.

    I understand that Option exists only as part of OptionGroup (because the OptionIdentity type requires a _group member of the _group type). Is one Option allowed to refer to another Option , which may be inside another instance of the OptionGroup aggregate? Does this violate the DDD rule for storing links to aggregate roots only and not refer to things inside?

No. This is why I developed modeling patterns for common identifiers .

  • Usually I saved aggregated roots and their children as an entire object, and not separately, I do this by having the object / list / dictionary as a member in the aggregate root. For Option code, it accepts a set of dependencies (of type OptionIdentity[] ).

    How would Options be rehydrated from storage? If this is an entity contained in another object, should it not be part of the common root and passed to the OptionGroup constructor?

No Option is not an entity at all! This value!

You can cache them if you have the correct cleanup policy. But they will not be provided by the repository: your application will call an application service, such as the following, to get them if necessary.

 // 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) } 
+5
source

Source: https://habr.com/ru/post/957924/


All Articles