I am practicing tactical DDD and having trouble as exemplified below. Fundamentally, whether some fields of the value object should be nullable depends on another field of the same value object. Consider the following value object and enum (C#):
public class AdmissionQuotient
{
private decimal? _quota1Gpa;
private decimal? _standbyGpa;
private Admission _admission;
}
public enum Admission
{
Limited, // Some applicants were not admitted
Ao, // All applicants were admitted
Aolp // All applicants were admitted, and there are open positions
}
For a particular education and a particular year, the GPAs describe the least GPA at which an applicant was admitted. Naturally, when all applicants were admitted, the GPAs do not make sense, and they will be null in the database. I do not like this design, since it either forces clients to be aware that the GPAs are only not null if _admission
is Limited
or forces forces clients to check the GPAs for null (when using their getters).
I have considered using inheritance instead of the enum. That is, LimitedAdmissionQuotient would extend AdmissionQuotient with the GPA fields. I have also considered using STATE (GoF) with a Concrete State for each enum value. Still, in both cases, the client would have to consider conceptually whether the admission is Limited
, Ao
, or Aolp
.
Is there a way to model this that would mitigate null check propagation or type/enum check propagation?
3
Yes, but it’s usually not worth it. You need to implement operators for your class. ie add, subtract, isequal, gt, lt etc
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading
Now I don’t need to see the underling nullable decimals and enum, I can just add up/average/do whatever with all the AdmissionQuotient
and your +/-/* operators will ignore or otherwise deal with the nulls.
Your example seems super esoteric, so lets use something more easy to understand like a TripleJumpResult
.
In the triple jump athletics event contestants do a Hop, Skip and a Jump sequentially to try and travel the furthest. However if they trip of otherwise foul the result is not counted.
TripleJumpResult
{
decimal? HopDist
decimal? SkipDist
decimal? JumpDist
JumpType type; //legal/foul enum
}
If a foul occurs any or all of these could be null.
We have a couple of use cases,
- given a set of results find who won the event
- given a set of results work out the average (legal) distance travelled
The nulls make these tasks hard, but if we implement >, + and divide we can do the calcs without null checks.
public static TripleJumpResult operator >(TripleJumpResult a, TripleJumpResult b)
{
//check for any nulls in a
//check for any nulls in b
if(both null) { return false }
if(a has nulls) { return false}
if(b has nulls { return true }
//alternatively check type enum for foul
if(both foul) { return false }
if(a is foul) { return false}
if(b is foul) { return true }
return (a.HopDist + a.SkipDist ... ) > (b.HopDist + ...)
}
Now I might expose the properties anyway, but users generally don’t need to look at them. You can say who won or what the average is without checking for fouls/nulls on each property because you can perform operations with the whole object which take these things into account
eg
var longestJump = jumps.First()
foreach(jump in jumps)
{
if(jump > longestJump) { longestJump = jump} //no need to look at enum or check for nulls on distances
}
The reason I say that its not normally worth it, is that doing this means you need to know the operations you want to perform with the object in avance and assumes that those operations are well defined and work when combined and stuff.
This is great for maths, or areas with clear unchanging specification, but generally isn’t true for business rules.
If say one athletics body decides that foul jumps should have the distance for the legally completed parts count, but an other doesn’t, or the rules change for every season, well now I cant use the operation overloading without two sets of objects. It would be easier just to put the logic in the calling code.
5
Your question is somewhat hard to understand for me, since you talk about GPAs without explaining what that precisely means (I guess “grade point average”), then using the term “GPA” once for the real-world values (I guess), once for the variables _quota1Gpa
and standbyGpa
. Moreover, you did not explain the difference between _quota1Gpa
and standbyGpa
.
Still, let me make a guess on my limited understanding. I think the range of possible values for GPAs is the same as the range of possible grades. Where I live, it is usual to have grades from 1 to 6, where 1 is best and 6 is worst. So when you just need these GPA values as a limit which applicants were admitted and which not, set the limit to the lowest possible value. Then make the GPA variables non-nullable. That makes any null check superfluous.