Money как Value Object

Описываемая проблема в статье давно и хорошо известна, поэтому она по большей части для новичков, которые не знакомы с темой.В ПО, которое разрабатывает наша команда используются денежные значения в рублях и копейках. Мы изначально знали, что использование примитивов для выражения денежных значений — это антипаттерн. Однако по мере разработки приложения мы всё никак не могли наткнуться на проблемы связанные с использованием примитивов, нам, видимо, везло и всё было нормально. До поры до времени.Мы совсем забыли про эту проблему и использование примитивов типа 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, IComparable { private const int KopecFactor = 100; private readonly long amountInKopecs; private Money (decimal amountInRub) { amountInKopecs = (long) (Decimal.Round (amountInRub, 2) * KopecFactor); } private Money (long amountInKopecs) { this.amountInKopecs = amountInKopecs; } public static Money FromKopecs (long amountInKopecs) { return new Money (amountInKopecs); } public static Money FromRubles (decimal amountInRubles) { return new Money (amountInRubles); } public decimal AmountInRubles { get { if (amountInKopecs < KopecFactor) return amountInKopecs; return (decimal)amountInKopecs / KopecFactor; } } public long AmountInKopecsInKopecs { get { return amountInKopecs; } } public int CompareTo(Money other) { if (amountInKopecs < other.amountInKopecs) return -1; if (amountInKopecs == other.amountInKopecs) 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 (amountInKopecs + other.amountInKopecs); } public Money Subtract (Money other) { return new Money (amountInKopecs — other.amountInKopecs); } 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.amountInKopecs > m2.amountInKopecs; } public override bool Equals (object other) { return (other is Money) && Equals ((Money) other); } public bool Equals (Money other) { return amountInKopecs == other.amountInKopecs; } 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 для рублей и копеек). Сравнение значений на разных валютах скорее всего придётся запретить (если, конечно, у вас нет доступа к курсам валют).Резюме: не испытывайте на пробу антипаттерн «одержимость примитивами», сделайте сразу нормальную абстракцию, иначе потом придётся убить несколько часов на рефакторинг. И не дай бог, если нарвётесь на баги, а на них, полагаясь на примитивы, нарваться очень просто.

© Habrahabr.ru