Can an aggregates invariant include a rule based o

2019-04-06 06:34发布

问题:

In DDD can an aggregates invariant include a rule based on information in a another aggregate? Now I don't think so, however this causes me a problem and I don't know how to solve it.

I have an entity called Asset (equipment) which I'm modelling as the root of an aggregate. It has a list of Tags (properties) that describe things like Manufacturer, Model etc. It stores the identity of second aggregate called AssetType which has a list of TagTypes, of which some can be marked as mandatory.

Now it appears to me that one of the invariant conditions for Asset should make a reference to the associated AssetType to enforce non null values in the list of mandatory tags. But my guts are crawling with the thought of how I am going to enforce consistency.

Does this mean the aggregate should really comprise all four entities? If the root were to be AssetType and it had a list of Assets under it, it could solve my problem, however this is not going to fit very well with a core use case which has other aggregates maintaining lists of different types of Asset. Asset really has to be the root otherwise I'm going to have problems.

And AssetType can't very well go inside the Asset aggregate either. This seems just as absurd.

My guts still says Asset and AssetType are two separate aggregates, but how do I resolve the consistency problems? Or have I got my invariant wrong?

回答1:

In this scenario, there are several methods for enforcing invariants.

First of all, consider the behaviors around the Asset aggregate. I assume there is at least a CreateAssetCommand and a RemoveTagCommand. The invariants should be enforced during execution of these commands in the following way:

CreateAssetCommand

Since an asset is always associated with an asset type, an AssetTypeId must be provided as part of this command. This ID must be obtained by the caller, possibly by looking up a specific asset type. When AssetType is looked up, the corresponding TagType entities can also be retrieved, the mandatory ones in particular. This will allow the caller to construct the required Tag instances to send as part of the command. Note, it is the responsibility of the caller to provide a valid asset type and tags.

RemoveTagCommand

The handler for this command can retrieve the appropriate Asset which stores the AssetTypeId. Next, the handler retrieves the set of mandatory tags for the asset type and ensures that those tags aren't removed. In this case, the invariant is enforced by the handler itself.

Another way to handle these invariants is to introduce eventual consistency, if acceptable. With this approach, removal of a tag from an asset should publish a TagRemovedEvent. A handler for this event can then verify that a mandatory tag wasn't removed. If it was, it can create a task or notification stating that an Asset is in an invalid state. Note, this assumes that it is acceptable for an asset to be in an invalid state until something is corrected.

Now to behaviors around AssetType. One command that could compromise the integrity of the Asset aggregate is the introduction of a new mandatory Tag. In this case, the onyl way to ensure integrity is to create appropriate tags for each corresponding asset. Since this likely can't be done automatically, eventual consistency must be accepted until appropriate tags are provided via manual intervention.

With all these approaches, you don't have the kind of integrity you'd get with a RDMS. The responsibility of enforcing cross-aggregate invariants is delegated to command handlers, event handlers and calling code. However, in many instances, this kind of consistency is perfectly acceptable.

Take a look at Effective Aggregate Design for more on this.



回答2:

Can an aggregates invariant include a rule based on information from elsewhere?

Aggregates can always use the informations in their own states and the argument that their commands recieves.

Someone use to access applicative services via singletons, service locators and so on, but IMO, that's a smell of tightly coupled applications. They forget that methods' arguments are effective dependency injectors! :-)

In DDD can an aggregates invariant include a rule based on information in a another aggregate?

No.
Except if the second aggregate is provided via commands' arguments, of course.

WARNING ! ! !

I have an entity called Asset (equipment)...
... (and a) second aggregate called AssetType...

The last time that I had to cope with a similar structure, it was a pain.

Chances are that you are choosing the wrong abstractions.

have I got my invariant wrong?

Probably... Did you asked to the domain expert? Does he talks about "TagTypes"?

You should never abstract on your own.

Entities of type X holding a reference to an instance of an X-Type are almost always a smell of over-abstraction, that in the hope of reuse, makes the model rigid and inflexible to business evolution.

ANSWER

If (and only if) the domain expert actually described the model in these terms, a possible approach is the following:

  1. you can create an AssetType class with a factory method that turns an IEnumerable<Tag> into a TagSet and throws either MissingMandatoryTagException or UnexpectedTagException if some of the tag is missing or unexpected.
  2. in the Asset class, a command RegisterTags would accept an AssetType and an IEnumerable<Tag>, throwing the MissingMandatoryTagException and WrongAssetTypeException (note how important are exceptions to ensure invariants).

edit
something like this, but much more documented:

public class AssetType
{
    private readonly Dictionary<TagType, bool> _tagTypes = new Dictionary<TagType, bool>();
    public AssetType(AssetTypeName name)
    {
        // validation here... 
        Name = name;
    }

    /// <summary>
    /// Enable a tag type to be assigned to asset of this type.
    /// </summary>
    /// <param name="type"></param>
    public void EnableTagType(TagType type)
    {
        // validation here... 
        _tagTypes[type] = false;
    }

    /// <summary>
    /// Requires that a tag type is defined for any asset of this type.
    /// </summary>
    /// <param name="type"></param>
    public void RequireTagType(TagType type)
    {
        // validation here... 
        _tagTypes[type] = false;
    }

    public AssetTypeName Name { get; private set; }


    /// <summary>
    /// Builds the tag set.
    /// </summary>
    /// <param name="tags">The tags.</param>
    /// <returns>A set of tags for the current asset type.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="tags"/> is <c>null</c> or empty.</exception>
    /// <exception cref="MissingMandatoryTagException">At least one of tags required 
    /// by the current asset type is missing in <paramref name="tags"/>.</exception>
    /// <exception cref="UnexpectedTagException">At least one of the <paramref name="tags"/> 
    /// is not allowed for the current asset type.</exception>
    /// <seealso cref="RequireTagType"/>
    public TagSet BuildTagSet(IEnumerable<Tag> tags)
    {
        if (null == tags || tags.Count() == 0)
            throw new ArgumentNullException("tags");
        TagSet tagSet = new TagSet();

        foreach (Tag tag in tags)
        {
            if(!_tagTypes.ContainsKey(tag.Key))
            {
                string message = string.Format("Cannot use tag {0} in asset type {1}.", tag.Key, Name);
                throw new UnexpectedTagException("tags", tag.Key, message);
            }
            tagSet.Add(tag);
        }

        foreach (TagType tagType in _tagTypes.Where(kvp => kvp.Value == true).Select(kvp => kvp.Key))
        {
            if(!tagSet.Any(t => t.Key.Equals(tagType)))
            {
                string message = string.Format("You must provide the tag {0} to asset of type {1}.", tagType, Name);
                throw new MissingMandatoryTagException("tags", tagType, message);
            }
        }

        return tagSet;
    }
}

public class Asset
{
    public Asset(AssetName name, AssetTypeName type)
    {
        // validation here... 
        Name = name;
        Type = type;
    }

    public TagSet Tags { get; private set; }

    public AssetName Name { get; private set; }

    public AssetTypeName Type { get; private set; }

    /// <summary>
    /// Registers the tags.
    /// </summary>
    /// <param name="tagType">Type of the tag.</param>
    /// <param name="tags">The tags.</param>
    /// <exception cref="ArgumentNullException"><paramref name="tagType"/> is <c>null</c> or
    /// <paramref name="tags"/> is either <c>null</c> or empty.</exception>
    /// <exception cref="WrongAssetTypeException"><paramref name="tagType"/> does not match
    /// the <see cref="Type"/> of the current asset.</exception>
    /// <exception cref="MissingMandatoryTagException">At least one of tags required 
    /// by the current asset type is missing in <paramref name="tags"/>.</exception>
    /// <exception cref="UnexpectedTagException">At least one of the <paramref name="tags"/> 
    /// is not allowed for the current asset type.</exception>
    public void RegisterTags(AssetType tagType, IEnumerable<Tag> tags)
    {
        if (null == tagType) throw new ArgumentNullException("tagType");
        if (!tagType.Name.Equals(Type))
        {
            string message = string.Format("The asset {0} has type {1}, thus it can not handle tags defined for assets of type {2}.", Name, Type, tagType.Name);
            throw new WrongAssetTypeException("tagType", tagType, message);
        }
        Tags = tagType.BuildTagSet(tags);
    }
}