I have an aggregate root Products
which contains a list of entities Selection
, which in turn contains a list of entities called Features
.
Product
has an identity of just nameSelection
has an identity of name (and its corresponding Product identity)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;
}
}
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 struct
s concrete? NO!!!
You can't throw them to hurt incompetent programmers!
They are as much abstract as the interface
s 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With