In a project I am currently involved with, a core part of the system involves a couple of fairly complicated (at least to me :)) computations involving time, power, energy, fuel, volumes etc.
At first, we just implemented the computations “as specified”, i.e. we went ahead and did stuff like this:
1 2 3 4 5 6 |
public double GetRemainingCapacity() { double remainingRuntimeHours = remainingRuntimeSeconds / 3600; double remainingCapacityMWh = currentProductionKW * remainingRuntimeHours / 1000; return remainingCapacityMWh; } |
to calculate how many MHhs a given device can deliver. This is just an example, we have a lot of this stuff going on, and this will be a major extensibility point in the system in the future.
I don’t know about you, but I got a tiny headache every time I looked at code like this, part from trying to understand what was going on, part because I knew errors could hide in there forever.
To remedy my headache (and the other team members’ headaches), we started migrating all that funky double stuff to some types, that do a better job at representing – i.e. Power for power, Energy for energy, and so on!
All of them, of course, as proper immutable value types (even though we use classes for that).
And then we utilized C#’s ability to supply operator overloading on all our domain types, allowing stuff like this to happen:
1 2 3 4 |
public Energy GetRemainingCapacity() { return currentProduction * remainingRuntime; } |
where currentProduction is an instance of Power and remainingRuntime is a TimeSpan. And whoever gets the Energy that comes out of this, will never have to doubt whether its Whs, KWhs, or MWhs – it’s just pure energy!
Now, this may seem like a small change, but it has already proven to have huge ramifications for the way we implement our computations:
- There is no such thing as multiplying two powers by accident, or subtracting two values that cannot be subtracted and still make sense, etc.
- We have no errors due to wrong factors, e.g. like KWs mistakenly being treated as MWs
- We have gained a cleaner, more intuitive core API, which is just friggin’ sweet!
In retrospective, I have done so much clumsy work in the past that could have been avoided by introducing proper value types for core domain concepts, like e.g. money, percentages, probabilities, etc.
I usually call myself “pretty domain-driven”, but now I realize that there’s an entire aspect of being just that, that I have overseen.
Do you think you’re domain-driven?
Very nice! Could you update the post with the actual implementation of the operator overloading? I’d love to see how clean and clear the code is.
Very excited about using this approach from now on throughout our systems… the numbers of times we have had the ‘we have to tell our clients VS just fix it and move forward’ argument based on a deep down in the bowels math unit/rounding error that has a big knock on effect as its used everywhere.
Tell the client always wins but 😐 its only natural to argue against anything that makes clients angry.
It’s not just a nice thing to read in the code. It also has the benefit of forcing the developer to start investigating the domain even more. When you do your unittests you need to determine if some input variable is a Power, Frequency etc.
Just to make it even better we’ve just added extensions to make it even more readable, like 3.kW() is an int extension that gives you a , Power of 3 kW, and a double extension like 50.5.Hz() that gives a Frequency. Its really great to work with
You must be aware of it, but F#’s units of measure are similar in their usage of the type system to enforce correct usage of units when doing calculations. Indeed your blog post points to a good application of the jest behind domain driven design, language design and static typing.
So F# can do that “out of the box”? I did not know that.
There’s definitely a certain amount of push-ups involved when wanting to do this in C#, which may be a reason why most developers are pushing decimal and string around instead of defining proper Money, InterestRate, Email, PostalCode, Ssn, etc.