0

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?

asked Aug 31, 2024 at 13:49
3
  • I'm not sure what you are looking for. Those values either are present or not, depending on some condition. Meaning whoever wants to use that data has to be aware of values and the condition. How can it be different? Switching to inheritance changes nothing conceptually. Commented Aug 31, 2024 at 14:13
  • So this would be an acceptable way of modeling this, you think? Commented Aug 31, 2024 at 14:23
  • 2
    Of course acceptable. Not convenient, but acceptable. Honestly this looks like a job for discriminated union, which C# still does not support (even though F# does). Well, you have to play with the hand you have, it is how it is. Commented Aug 31, 2024 at 14:26

2 Answers 2

1

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,

  1. given a set of results find who won the event
  2. 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.

answered Aug 31, 2024 at 16:02
5
  • That's nice. Thanks. How about for a standard getter property? If one such is needed, I guess its return type must still be nullable even if operators were implemented, right? Commented Aug 31, 2024 at 16:21
  • 1
    Im not sure I follow. When you do this kind of thing you are implementing a new value type in the sense of Date or ComplexNumber or something. you Don't expose any properties really Commented Aug 31, 2024 at 16:52
  • I am not aware of anything that says or implies that a value object cannot expose getters for attributes. It can do that just fine while being immutable. Can you explain the reason why this should not be done? Also, I appreciate your example of how to override operators. It is not analogous however, since no enum or generalization affects whether the attributes are relevant. Commented Sep 1, 2024 at 7:14
  • its not that you cant expose properties, its just that the whole point is to enable to operations without the user having to look at those properties Commented Sep 1, 2024 at 10:09
  • I'll add an enum to the example if its not clear Commented Sep 1, 2024 at 10:09
0

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.

answered Sep 1, 2024 at 12:55

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.