📜 ⬆️ ⬇️

Money as a Value Object

The problem described in the article has been well known for a long time, so it is mostly for beginners who are not familiar with the topic.

The software that our team develops uses monetary values ​​in rubles and kopecks. We initially knew that using primitives to express monetary values ​​is an anti-pattern. However, as the application was developed, we still could not stumble upon the problems associated with the use of primitives, we were apparently lucky and everything was fine. For the time being.
We completely forgot about this problem and the use of primitives of type int and decimal spread throughout the system. And now, when we wrote the first method, in which we felt the problem, we had to remember about this technical debt and rewrite everything to use money abstraction instead of primitives.

I would like to add that, in general, this anti-pattern is an “obsession with primitives” that occurs quite often, for example: string to represent an IP address, use int or string for ZipCode.

But govnokod, which was written:
public 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 } 

Here you can see what kind of mash turns work with five values. Everywhere it is necessary to understand what is happening now with the pennies or rubles. To convert between decimal and int, extension methods KopToRub and RubToKop were written, which, by the way, is one of the first signs of obsession with primitives.
')
As a result, its own Money structure was quickly written, calculated only for rubles (and pennies). Some operator overloads are omitted to save space. Code about the following:

 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)); } } 

In a similar implementation, Fowler keeps two open constructors, one of which takes a double, the other takes a long. I dislike it categorically, for what does the code mean?
 var money = new Money(200); // : 200   200 =2.? 

For the same reason it is bad to give the possibility of implicit casting. This is bad regardless of whether an implicit casting is allowed only through a long, or both through a long and decimal (one would think that allowing an implicit conversion for decimal is normal, but what someone wrote Money b = 200m does not mean that he did not mean 200 kopecks, but m attributed to just compile).
 Money a = 200; // : 200   200 =2.? Money b = 200m; //   ,    ? 

If you need to implement work in different currencies, then we simply create currency classes that know the factor of the reduction (for example, 100 for dollars and cents, 100 for rubles and kopecks). Comparison of values ​​in different currencies is likely to be banned (unless, of course, you do not have access to exchange rates).

Summary: do not try to test the anti-pattern “obsession with primitives”, make a normal abstraction right away, otherwise you will have to kill a few hours to refactor. And God forbid, if you run into bugs, and relying on primitives on them is very easy to run into.

PS As a result of the ensuing discussions in the comments, I would like to add that there are no universal answers to all questions. If you want to use decimal to represent money as a concept - for God's sake, just understand the pros and cons of different approaches.

Source: https://habr.com/ru/post/254837/


All Articles