Описываемая проблема в статье давно и хорошо известна, поэтому она по большей части для новичков, которые не знакомы с темой.
В ПО, которое разрабатывает наша команда используются денежные значения в рублях и копейках. Мы изначально знали, что использование примитивов для выражения денежных значений — это антипаттерн. Однако по мере разработки приложения мы всё никак не могли наткнуться на проблемы связанные с использованием примитивов, нам, видимо, везло и всё было нормально. До поры до времени.
Мы совсем забыли про эту проблему и использование примитивов типа int и decimal расползлось по всей системе. И теперь, когда мы написали первый метод, в котором прочувствовали проблему, пришлось вспомнить про это технический долг и переписать всё на использование денежной абстракции вместо примитивов.
Хочется добавить, что в целом данный антипаттерн — это «одержимость примитивами», который встречается достаточно часто, например: string для представления IP-адреса, использование int или string для ZipCode.
А вот говнокод, который был написан:
Здесь можно увидеть в какое месиво превращается работа с пятью значениями. Везде надо понимать с чем сейчас происходит работа — с копейками или рублями. Для конвертации между decimal и int были написаны методы-расширения KopToRub и RubToKop, что, кстати, является одним из первых признаков одержимости примитивами.
В результате быстренько была написана своя структура Money, рассчитанная только на рубли (и копейки). Некоторые перегрузки операторов опущены для экономии места. Код примерно следующий:
Фаулер при аналогичной реализации держит два открытых конструктора, один из которых принимает double, другой принимает long. Мне это не нравится категорически, ибо что означает код
По этой же причине плохо давать возможность неявного приведения. Это плохо в независимости от того, разрешено ли неявное приведение только через long, или и через long и через decimal (можно было бы подумать, что разрешить implicit conversion для decimal это нормально, но то, что кто-то написал Money b = 200m ещё не означает, что он не имел ввиду 200 копеек, а m приписал, чтобы просто скомпилировалось).
Если нужно реализовать работу в разных валютах, то просто создаём классы валют, которые знают factor приведения (например, 100 для долларов и центов, 100 для рублей и копеек). Сравнение значений на разных валютах скорее всего придётся запретить (если, конечно, у вас нет доступа к курсам валют).
Резюме: не испытывайте на пробу антипаттерн «одержимость примитивами», сделайте сразу нормальную абстракцию, иначе потом придётся убить несколько часов на рефакторинг. И не дай бог, если нарвётесь на баги, а на них, полагаясь на примитивы, нарваться очень просто.
P.S. В результате развернувшихся дискуссий в комментариях хотелось бы добавить, что нет универсальных ответов на все вопросы. Если вы хотите использовать decimal для представления денег, как концепции — ради бога, просто понимайте все плюсы и минусы различных подходов.
В ПО, которое разрабатывает наша команда используются денежные значения в рублях и копейках. Мы изначально знали, что использование примитивов для выражения денежных значений — это антипаттерн. Однако по мере разработки приложения мы всё никак не могли наткнуться на проблемы связанные с использованием примитивов, нам, видимо, везло и всё было нормально. До поры до времени.
Мы совсем забыли про эту проблему и использование примитивов типа int и decimal расползлось по всей системе. И теперь, когда мы написали первый метод, в котором прочувствовали проблему, пришлось вспомнить про это технический долг и переписать всё на использование денежной абстракции вместо примитивов.
Хочется добавить, что в целом данный антипаттерн — это «одержимость примитивами», который встречается достаточно часто, например: string для представления IP-адреса, использование int или string для ZipCode.
А вот говнокод, который был написан:
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
}
Здесь можно увидеть в какое месиво превращается работа с пятью значениями. Везде надо понимать с чем сейчас происходит работа — с копейками или рублями. Для конвертации между decimal и int были написаны методы-расширения KopToRub и RubToKop, что, кстати, является одним из первых признаков одержимости примитивами.
В результате быстренько была написана своя структура Money, рассчитанная только на рубли (и копейки). Некоторые перегрузки операторов опущены для экономии места. Код примерно следующий:
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));
}
}
Фаулер при аналогичной реализации держит два открытых конструктора, один из которых принимает double, другой принимает long. Мне это не нравится категорически, ибо что означает код
var money = new Money(200); //что это: 200 рублей или 200 копеек=2руб.?
По этой же причине плохо давать возможность неявного приведения. Это плохо в независимости от того, разрешено ли неявное приведение только через long, или и через long и через decimal (можно было бы подумать, что разрешить implicit conversion для decimal это нормально, но то, что кто-то написал Money b = 200m ещё не означает, что он не имел ввиду 200 копеек, а m приписал, чтобы просто скомпилировалось).
Money a = 200; //что это: 200 рублей или 200 копеек=2руб.?
Money b = 200m; //казалось бы это рубли, но кто его знает?
Если нужно реализовать работу в разных валютах, то просто создаём классы валют, которые знают factor приведения (например, 100 для долларов и центов, 100 для рублей и копеек). Сравнение значений на разных валютах скорее всего придётся запретить (если, конечно, у вас нет доступа к курсам валют).
Резюме: не испытывайте на пробу антипаттерн «одержимость примитивами», сделайте сразу нормальную абстракцию, иначе потом придётся убить несколько часов на рефакторинг. И не дай бог, если нарвётесь на баги, а на них, полагаясь на примитивы, нарваться очень просто.
P.S. В результате развернувшихся дискуссий в комментариях хотелось бы добавить, что нет универсальных ответов на все вопросы. Если вы хотите использовать decimal для представления денег, как концепции — ради бога, просто понимайте все плюсы и минусы различных подходов.