Primitive Types Obsession Problem
Today I’m going to discuss the problem of using primitive types instead of abstractions. This problem was discussed in the blog of Mark Seemann. Read it, if you haven’t read it yet.
In this post I’m going to talk about Money type as an abstraction instead of using decimal type for representing money-values.
In the last project I’ve been participating in, we relied on a decimal and integer types for a long time. From the beginning, we knew that using primitive types for values of that kind is an anti-pattern, but we stubbornly have been using them. In the US there are cents and dollars. In the Russian Federation – rubles and kopeks. 1 ruble = 100 kopeks. Our system inter-operated with an external system which performed all its calculations in kopeks. So it required kopeks as the input and returned kopeks as the output. If we wanted to pass in 2rubles and 50kopeks, then we passed in Int32 amount = 250;
And we haven’t seen any problems up to a certain time. But those kopeks represented in Int32 started to spread throughout the whole system. In many cases we’ve had to convert them into a decimal type to correctly display the value on a screen (you definitely don’t want to show users 250 kopeks, you want to show 2.50 rubles). In other cases, when we handled user’s input of money values, we’ve had to convert them into kopeks in order to pass them into that external system.
Eventually we’ve found ourselves in writing the following function:
[code lang=”csharp”]
bool HasMismatchBetweenCounters(
DispensingCompletedEventArgs eventArgs,
decimal acceptedInRub) {
decimal expectedChangeInRub = eventArgs.ChangeAmount.KopToRub();
int dispensedTotalCashAmountInKopecs = expectedChangeInRub.RubToKop() – eventArgs.UndeliveredChangeAmount;
if (dispensedTotalCashAmountInKopecs != eventArgs.State.DispensedTotalCashAmount) {
return true;
}
if (acceptedInRub != eventArgs.State.AcceptedTotalCashAmount.KopToRub()) {
return true;
}
return false
}
[/code]
As you can see, this code is very smelly. This is actually a ball of mud. Everywhere you have to elicit with what you are working right now, with kopeks or rubles?
Also, you can see from the code above that we ended up creating two extension methods: RubToKop and KopToRub. Creating extensions methods on primitive types is the first sign of the disease.
The Cure for the Disease
In the end we decided to solve the problem by introducing Money structure as a form of Value Object Design Pattern.
[code lang=”csharp”]
public struct Money : IEqualityComparer<Money>, IComparable<Money> {
private const int KopecFactor = 100;
private readonly decimal amountInRubles;
private Money(decimal amountInRub) {
amountInRubles = Decimal.Round(amountInRub, 2);
}
private Money(long amountInKopecs) {
amountInRubles = (decimal)amountInKopecs / KopecFactor;
}
public static Money FromKopecs(long amountInKopecs) {
return new Money(amountInKopecs);
}
public static Money FromRubles(decimal amountInRubles) {
return new Money(amountInRubles);
}
public decimal AmountInRubles {
get { return amountInRubles; }
}
public long AmountInKopecs {
get { return (int)(amountInRubles * KopecFactor); }
}
public int CompareTo(Money other) {
if (amountInRubles < other.amountInRubles) return -1;
if (amountInRubles == other.amountInRubles) return 0;
else return 1;
}
public bool Equals(Money x, Money y) {
return x.Equals(y);
}
public int GetHashCode(Money obj) {
return obj.GetHashCode();
}
public Money Add(Money other) {
return new Money(amountInRubles + other.amountInRubles);
}
public Money Subtract(Money other) {
return new Money(amountInRubles – other.amountInRubles);
}
public static Money operator +(Money m1, Money m2) {
return m1.Add(m2);
}
public static Money operator -(Money m1, Money m2) {
return m1.Subtract(m2);
}
public static bool operator ==(Money m1, Money m2) {
return m1.Equals(m2);
}
public static bool operator >(Money m1, Money m2) {
return m1.amountInRubles > m2.amountInRubles;
}
public override bool Equals(object other) {
return (other is Money) && Equals((Money) other);
}
public bool Equals(Money other) {
return amountInRubles == other.amountInRubles;
}
public override int GetHashCode() {
return (int)(AmountInKopecs ^ (AmountInKopecs >> 32));
}
}
[/code]
Martin Fowler in the similar case keeps two public constructors, one of which takes double type and the other takes long. I don’t like it very much.
And that’s why. Can you explain what the following code does?
[code lang=”csharp”]
var money = new Money(200);
[/code]
What was that? 200 dollars or 200 cents = 2 dollars? This is also the reason why it’s bad to allow implicit casts. It is bad in both cases: either it’s allowed to implicitly convert long or decimal into Money. Really, will you be sure that the expression Money m = 200m does mean that it’s 200 dollars? Maybe someone just put ‘m’ in order to compile it, but actually wanted to initialize it by 2 dollars? Consider the following:
[code lang=”csharp”]
Money a = 200; // what it is 200 dollars or 200 cents?
Money b = 200m; //seems like this is expressed in dollars, but are you sure?
[/code]
You can say that the introduced structure does not support different currencies. Yes, you’re right. But that was sufficient for us. If you want to gain the support of different currencies, then you are forced to introduce the Money type. Especially if you need to convert between currencies according to their exchange rates.
Conclusion
Do not examine the anti-pattern “Primitives Obsession”, rely on an abstraction instead. Otherwise you will spend hours to refactor the mess you brought to a project. It’s also extremely easy to introduce bugs relying on primitive types. Remember, primitive types cannot reflect conceptions! Money is the concept.
Subscribe to my blog!