Pretend I have entities A
, B
, C
, D
. They each have a structure as follows
public class A{
public IEnumerable<B> Bs {get;}
public IEnumerable<C> Cs {get;}
public IEnumerable<D> Ds {get;}
}
public class B{
public IEnumerable<B> Bs {get;}
public IEnumerable<D> Ds {get;}
}
public class C{
public IEnumerable<B> Bs {get;}
public IEnumerable<C> Cs {get;}
}
public class D { }
(Imagine this is a bit like a windows folder structure but more complex)
But there is a set of 5+ functions relevant to any class that holds entities (e.g. functions to search the structure). Instead of rewriting these functions we introduce a class:
public class BHolder : ValueObject {
public IEnumerable<B> Bs {get;}
... 5+ DIFFERENT FUNCTIONS HERE
//(example of a write below) (might include validation logic later)
public BHolder AddB(B toBeAdded) => new BHolder(Bs.Concat(new[] {toBeAdded});
}
Now class A
has a BHolder
, CHolder
, and DHolder
(and so on for B
, C
, and D
).
From my understanding of DDD, BHolder
is not an Entity and therefore MUST be a ValueObject. I've heard the best practice is for these ValueObjects to be immutable. But that means for every "Write" inside of A
, B
, and C
I now need to wrap them as follows:
public class A{
public BHolder BHolder {get;}
....
//"wrapping function" below
public void AddB(B toBeAdded) => BHolder = BHolder.Add(toBeAdded);
}
But as the class grows, we start to get to the stage where there are 15+ "wrapping functions" that are essentially just empty code.
If I make the BHolder
mutable, I don't need to worry about these wrapping functions. But every time in the past I've thought making a ValueObject mutable was a good idea I've been proven wrong. (In this case the only thing I can think of is that these "wrapping functions" enforce invariants specific to A holding Bs (which there are none)).
So my question is, is it okay to make BHolder
mutable? Or is there a better way that I'm not seeing?
-
"BHolder is not an Entity and therefore MUST be a ValueObject" - why can't it be neither?Filip Milovanović– Filip Milovanović2021年02月08日 11:10:26 +00:00Commented Feb 8, 2021 at 11:10
-
I'm not sure if I follow correctly, but in your example BHolder resembles a Repository.5ar– 5ar2021年02月08日 18:09:57 +00:00Commented Feb 8, 2021 at 18:09
-
@FilipMilovanović from what I understand of DDD, every class in your domain model is either an entity (has an identity and is compared by identity) or a valueobject (no identity, compared by value). e.g. a car is an entity (2 cars can have the same colour/size/etc. but be different. My car is still my car even if I've changed its colour or wheels)user8051386– user80513862021年02月09日 03:42:00 +00:00Commented Feb 9, 2021 at 3:42
-
@5ar yeah that's true, I think the whole structure is similar to a repository. But there is complex functionality on top of the structure and these specific classes don't have to worry about database concerns.user8051386– user80513862021年02月09日 03:46:24 +00:00Commented Feb 9, 2021 at 3:46
-
@user8051386 there are more concepts in our DDD tool belt, such as a Domain Services, which are basically classes that contain domain logic but own no data and Domain Events. Not that relevant to this specific question, but good to know nonetheless.Rik D– Rik D2021年02月09日 09:54:11 +00:00Commented Feb 9, 2021 at 9:54
1 Answer 1
In cases like this I would encapsulate the logic to add elements to the value object, inside the value object. It's probably the easiest to demonstrate what I mean in code.
public class MyEntity
{
// The entity is not immutable, so we can set a new BHolder
public BHolder BHolder { get; private set; }
public MyEntity(BHolder bHolder)
{
BHolder = bHolder;
}
public void UpdateBHolder(BHolder bHolder)
{
// Some validation here
this.BHolder = bHolder;
}
}
public class BHolder : ValueObject
{
public IEnumerable<B> Bs { get; }
private BHolder(IEnumerable<B> bs)
{
Bs = bs;
}
// This could also take a collection of Bs as parameter
public static BHolder Of(B b)
{
return new BHolder(new[] {b});
}
// This could also take a collection of Bs as parameter
public static BHolder Add(BHolder original, B bToAdd)
{
return new BHolder(original.Bs.Concat(new[] { bToAdd } ));
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Bs;
}
}
public class B : ValueObject
{
public string SomeValue { get; }
public B(string someValue)
{
SomeValue = someValue;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return SomeValue;
}
}