Money как Value Object

в 13:54, , рубрики: .net, C#, 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<Money>, IComparable<Money> {
        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 для рублей и копеек). Сравнение значений на разных валютах скорее всего придётся запретить (если, конечно, у вас нет доступа к курсам валют).

Резюме: не испытывайте на пробу антипаттерн «одержимость примитивами», сделайте сразу нормальную абстракцию, иначе потом придётся убить несколько часов на рефакторинг. И не дай бог, если нарвётесь на баги, а на них, полагаясь на примитивы, нарваться очень просто.

Автор: EngineerSpock

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js