DDD navigation to entities inside aggregate root via composite identity
Asked Answered
Z

3

13

I have an aggregate root Products which contains a list of entities Selection, which in turn contains a list of entities called Features.

  • The aggregate root Product has an identity of just name
  • The entity Selection has an identity of name (and its corresponding Product identity)
  • The entity Feature has an identity of name (and also it's corresponding Selection identity)

Where the identities for the entities are built as follows:

var productId = new ProductId("dedisvr");
var selectionId = new SelectionId("os",productId);
var featureId = new FeatureId("windowsstd",selectionId);

Note that the dependent identity takes the identity of the parent as part of a composite.

The idea is that this would form a product part number which can be identified by a specific feature in a selection, i.e. the ToString() for the above featureId object would return dedisvr-os-windowsstd.

Everything exists within the Product aggregate where business logic is used to enforce invariant on relationships between selections and features. In my domain, it doesn't make sense for a feature to exist without a selection, and selection without an associated product.

When querying the product for associated features, the Feature object is returned but the C# internal keyword is used to hide any methods that could mutate the entity, and thus ensure the entity is immutable to the calling application service (in a different assembly from domain code).

These two above assertions are provided for by the two functions:

class Product
{
    /* snip a load of other code */

    public void AddFeature(FeatureIdentity identity, string description, string specification, Prices prices)
    {
       // snip...
    }

    public IEnumerable<Feature> GetFeaturesMemberOf(SelectionIdentity identity);
    {
       // snip...
    }
}

I have a aggregate root called Service order, this will contain a ConfigurationLine which will reference the Feature within the Product aggregate root by FeatureId. This may be in an entirely different bounded context.

Since the FeatureId contains the fields SelectionId and ProductId I will know how to navigate to the feature via the aggregate root.

My questions are:

Composite identities formed with identity of parent - good or bad practice?

In other sample DDD code where identities are defined as classes, I haven't seen yet any composites formed of the local entity id and its parent identity. I think it is a nice property, since we can always navigate to that entity (always through the aggregate root) with knowledge of the path to get there (Product -> Selection -> Feature).

Whilst my code with the composite identity chain with the parent makes sense and allows me to navigate to the entity via the root aggregate, not seeing other code examples where identities are formed similarly with composites makes me very nervous - any reason for this or is this bad practice?

References to internal entities - transient or long term?

The bluebook mentions references to entities within an aggregate are acceptable but should only be transient (within a code block). In my case I need to store references to these entities for use in future, storing is not transient.

However the need to store this reference is for reporting and searching purposes only, and even if i did want to retrieve the child entity bu navigate via the root, the entities returned are immutable so I don't see any harm can be done or invariants broken.

Is my thinking correct and if so why is it mentioned keep child entity references transient?

Source code is below:

public class ProductIdentity : IEquatable<ProductIdentity>
{
    readonly string name;

    public ProductIdentity(string name)
    {
        this.name = name;
    }

    public bool Equals(ProductIdentity other)
    {
        return this.name.Equals(other.name);
    }

    public string Name
    {
        get { return this.name; }
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public SelectionIdentity NewSelectionIdentity(string name)
    {
        return new SelectionIdentity(name, this);
    }

    public override string ToString()
    {
        return this.name;
    }
}

public class SelectionIdentity : IEquatable<SelectionIdentity>
{
    readonly string name;
    readonly ProductIdentity productIdentity;

    public SelectionIdentity(string name, ProductIdentity productIdentity)
    {
        this.productIdentity = productIdentity;
        this.name = name;
    }

    public bool Equals(SelectionIdentity other)
    {
        return (this.name == other.name) && (this.productIdentity == other.productIdentity);
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public override string ToString()
    {
        return this.productIdentity.ToString() + "-" + this.name;
    }

    public FeatureIdentity NewFeatureIdentity(string name)
    {
        return new FeatureIdentity(name, this);
    }
}

public class FeatureIdentity : IEquatable<FeatureIdentity>
{
    readonly SelectionIdentity selection;
    readonly string name;

    public FeatureIdentity(string name, SelectionIdentity selection)
    {
        this.selection = selection;
        this.name = name;
    }

    public bool BelongsTo(SelectionIdentity other)
    {
        return this.selection.Equals(other);
    }

    public bool Equals(FeatureIdentity other)
    {
        return this.selection.Equals(other.selection) && this.name == other.name;
    }

    public SelectionIdentity SelectionId
    {
        get { return this.selection; }
    }

    public string Name
    {
        get { return this.name; }
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public override string ToString()
    {
        return this.SelectionId.ToString() + "-" + this.name; 
    }
}
Zonked answered 24/11, 2013 at 20:12 Comment(3)
References to entities within an aggregate should be transient when the reference to the internal entity object is passed out of the aggregate. It does not apply to the actual aggregate.Galliwasp
Thanks @Eben, why transient to entities inside the aggregate root? How is this different from a reference identity of an aggregate which would be pulled from a repository? Feel free to drop an answer below :)Zonked
I hope I'm understanding you correctly :) --- but say a Truck contains a collection of Tyre instances. If, say, a ConditionMonitor is passed a Tyre instance from a Truck object then that Tyre instance should be transient. Any Truck holding on to its own entities forever is fine.Galliwasp
C
12

Composite identities formed with identity of parent - good or bad practice?

They are a good practice when they are used properly: when the domain expert identifies things locally (eg "the John from Marketing") they are correct, while they are wrong otherwise.

In general, whenever the code follows the expert's language, it's correct.

Sometimes you face a globally identified entity (like "John Smith") identified locally by the expert when he talks about a specific bounded context. In these cases, BC requirements win.
Note that this means that you will need a domain service to map identifiers between BCs, otherwise, all you need are shared identifiers.

References to internal entities - transient or long term?

If the aggregate root (in your case Product) requires the child entities to ensure business invariants, the references must be "long term", at least until the invariants must hold.

Moreover you correctly grasped the rationale behind internal entities: they are entities if the expert identifies them, mutability is a programming concern (and immutability is always safer). You can have immutable entities, either local to other or not, but what make them entities is the fact that the expert identifies them, not their immutability.

Value object are immutable just because they have no identity, not the other way!

But when you say:

However the need to store this reference is for reporting and searching purposes only

I would suggest you to use direct SQL queries (or queryobject with DTOs, or anything you can have for cheap) instead of domain objects. Reports and search don't mutate entities's state, so you don't need to preserve invariants. That's the main rationale of CQRS, that simply means: "use the domain model only when you have to ensure business invariants! Use WTF you like for components that just need to read!"

Extra notes

When querying the product for associated features, the Feature object is returned but the C# internal keyword is used to hide any methods that could mutate the entity...

Access modifiers to handle modifiers in this context are a cheap approach if you don't need to unit test clients, but if you need to test the client code (or to introduce AOP interceptor, or anything else) plain old interfaces are a better solution.

Someone will tell you that you are using "needless abstraction", but using a language keyword (interface) does not means introducing abstractions at all!
I'm not entirely sure that they really understand what an abstraction is, so much that they confuse the tools (a few language keywords that are common in OO) for the act of abstracting.

The abstractions reside in the programmer mind (and in the expert's mind, in DDD), the code just expresses them through the constructs provided by the language you use.

Are sealed classes concrete? Are structs concrete? NO!!!
You can't throw them to hurt incompetent programmers!
They are as much abstract as the interfaces or abstract classes.

An abstraction is needless (worse, it's dangerous!) if it makes the code's prose unreadable, hard to follow, and so on. But, believe me, it can be coded as a sealed class!

... and thus ensure the entity is immutable to the calling application service (in a different assembly from domain code).

IMHO, you should also consider that if the "apparently immutable" local entities returned by the aggregate can, actually, change part of their state, the clients that received them won't be able to know that such change occurred.

To me, I solve this issue by returning (and also using internally) local entities that are actually immutable, forcing clients to only hold a reference to the aggregate root (aka the main entity) and subscribe events on it.

Chaddy answered 2/12, 2013 at 14:11 Comment(0)
P
6

Composite identities formed with identity of parent - good or bad practice?

IMHO there is no reason to believe it is bad practice, as long as the entity id is unique within the aggregate root it makes no difference if the entity id is composite, or even unique outside the aggregate root. The only objection one might have is that these composite identifiers differ from the identifiers used in the vocabulary of your domain, 'the ubiquitous language'.

References to internal entities - transient or long term?

If these entities are immutable, these entities should have been modeled as value objects. Otherwise, by directly referencing to these entities you run the risk of accessing an entity that is no longer associated with the given aggregate root or has been changed in the meanwhile.

Pane answered 27/11, 2013 at 21:13 Comment(1)
Good point on aligning with the ubiquitous language. With regards to immutability I read from Vaughn Veron book and demo c# code he does something similar with Tasks, they are entities inside an aggregate with their own mutable properties that are marked internal. For the external application code these properties are hidden thus making them immutable for all but the domain model, these are entities with guid identities, and not value objectsZonked
B
1

Not totally related to the question but I'd like to start by mentioning that I don't find the interface appealing. It seems as if you are exposing the Feature class one-way. Either expose it or don't. I am not a C# developer so please don't mind if I make any syntactical errors. To demonstrate what I mean:

This exposes the attributes of a Feature. When those attributes change, in whatever manor, this interface needs to be changed also.

public void AddFeature(FeatureIdentity identity, string description,
                       string specification, Prices prices)

You may want to consider accepting a Feature object as an argument:

public void AddFeature(Feature feature)

This is way cleaner IMO.

On topic; your question reminds me of NoSQL designs. I am sort of familiar with those so I might be biased and may be missing the point.

Composing a child identifier with the parent identifier is possible in multiple ways and may or may not be a bad practice. Think of how your entities will be accessed. If you only access the child entities from the parent it makes sense to aggregate. If the child entities may be also root entities then you'll need to refer to them. You already sort of made this decision in your question.

it doesn't make sense for a feature to exist without a selection, and selection without an associated product.

It would make sense that your Product class has a collection of some sort containing Selection objects. The Selection class would have a collection containing Feature objects. Note that this may make a Product object very heavy, in terms of persistence, if it has a lot of Selection objects which may have a lot of Feature objects. In such cases you may be better off by having them as references by identifier.

The identifiers used in your code, other than in the persistence layer, do not have to be composed of both child and the parent identifiers since they are already in a specific context. This can however be a design decision to improve readability of the data.

The composed identity has it's roots with SQL I think, I have seen code that uses classes to model these identities. I like to think this is more a limitation of an persistence framework or a language. This is just an end to justify the means. When a persistence framework somewhat forces you to do this, it is acceptable.

Referencing internal entities sounds like something that you shouldn't do. In the example of Product, Selection and Feature; it doesn't make sense to have a selection without a product. So it would make more sense to refer to the product. For reporting you may want to consider duplicating data. This is actually a common technique in NoSQL. Especially when entities are immutable you want to consider duplicating those entities elsewhere. Referring to an entity will result in another 'get-the-entity-operation' whereas the data never changes, this is quite pointless if I may say so.

Referring to a parent or a child is not bad practice at all. These relations are enforced, this is part of the modeling, it's not that an entity exists without a parent. If you want to force a child entity to have a parent; require the parent in the child's constructor. Please do not implement a create-child method in the parent. As I stated above, this will complicate stuff. I would personally not force having a parent, when creating a child you set the parent yourself.

Belier answered 30/11, 2013 at 14:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.