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<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 для представления денег, как концепции — ради бога, просто понимайте все плюсы и минусы различных подходов.
    Share post

    Comments 50

      +1
      # var money = new Money(200); // что это: 200 рублей или 200 копеек=2руб.?
      а почему не 200 $

      Не очень понимаю зачем столько кода вокруг копеек — судя по MSDN decimal в dotnet это уже структура содержащая целочисленные значения и кол-во знаков после запятой, то есть ваша же Money только factor выражается не как 100, а как 2.
        0
        а почему не 200 $

        А может и баксов. Здесь важно не это, я в статье намеренно опустил рассмотрение разных валют. Дело в том, что неясно даже само значение.

        Не очень понимаю зачем столько кода вокруг копеек — судя по MSDN decimal в dotnet это уже структура содержащая целочисленные значения и кол-во знаков после запятой, то есть ваша же Money только factor выражается не как 100, а как 2.

        decimal не несёт в себе никакой информации о том, что представлено этим числом.
        decimal a = 10; //что это?
          +4
          В ПО, которое разрабатывает наша команда используются денежные значения в рублях и копейках. Мы изначально знали, что использование примитивов для выражения денежных значений — это антипаттерн.

          Антипаттерн — это использовать денежные значения в рублях и копейках. По моему скромному мнению, конечно же.

          decimal не несёт в себе никакой информации о том, что представлено этим числом.
          decimal a = 10; //что это?

          Я не программирую на C# и под .NET, а сужу по аналогии с другими языками и с базами данных, но гугл вот что мне выдал первой же ссылкой:

          Compared to floating-point types, the decimal type has more precision and a smaller range, which makes it appropriate for financial and monetary calculations.

          If you want a numeric real literal to be treated as decimal, use the suffix m or M, for example:
          decimal myMoney = 300.5m;
          Without the suffix m, the number is treated as a double and generates a compiler error.


          То есть, 10m — это 10 рублей. 10.15m — 10 рублей 15 копеек. Зачем вообще нужно amountInKopecs/amountInRubles, если есть целая и дробная часть числа для хранения того и другого? Я за свою практику занимался разработкой нескольких финансовых приложений (причем, мультивалютных), и сейчас даже в самых извращенных фантазиях не могу себе представить, зачем в каких-то местах кода использовать суммы в копейках, а в каких-то — суммы в рублях, а потом заниматься путанными пересчетами и создавать себе другим всем лишние проблемы? А если все-таки когда-нибудь понадобится конвертировать в другую валюту — будете использовать копейки или рубли? А конвертировать будете в центы или в доллары? А для каждой валюты будете писать свой класс, где kopeks в названии функции будет заменять на cents/penny/sen и т.п.? А еще в мире до сих пор не вымерли валюты-динозавры, где бывают 1/1000 части основной единицы, 1/5, 1/12, 1/20 и даже 1/240, и несколько разных могут совместно сосуществовать в одной валюте. Ну это уже, конечно, лирика, задумки на очень отдаленный рефакторинг, которого в принципе случиться не может, потому что кому вообще эти валюты могут понадобиться? Рубль — он и в африке рубль!

          P.S. Stackoverflow тоже советует использовать decimal для хранения денежных значений (там опять же про sql, но не думаю, что decimal в C# сильно отличается в этом плане).
          P.P.S. Таки да, я прекрасно понимаю, что на собственноручно изобретенном велосипеде педали крутить спицы подтягивать гораздо приятнее.
            0
            Вот мой ответ, только другому хабравчанину. Вам там в раздел «по существу» :)
              0
              Что касается ответов на ваши вопросы.
              Да, валюты всё усложняют, согласен.
              А если все-таки когда-нибудь понадобится конвертировать в другую валюту — будете использовать копейки или рубли?
              А конвертировать будете в центы или в доллары?

              Для конвертации будет метод конвертации, который будет учитывать полную сумму, выраженную в обеих частях. Плюс нужен будет доступ к сервису курсов валют. А как вы собираетесь это делать без типа Money? Прикручивать методы-расширители?
              А для каждой валюты будете писать свой класс, где kopeks в названии функции будет заменять на cents/penny/sen и т.п.?

              Нет, поменяют имена двух методов на подходящие для всех валют, например HigherUnit и LowerUnit. Менее понятно, чем Kopecs и Rubles, но программисту работающему с классом нужно понять, что это такое — ровно один раз. Это всё равно будет более эксплицитно, чем записывать decimal a = 10m — что это?
              А еще в мире до сих пор не вымерли валюты-динозавры, где бывают 1/1000 части основной единицы, 1/5, 1/12, 1/20 и даже 1/240, и несколько разных могут совместно сосуществовать в одной валюте.

              Совершенно верно. Это легко решается объектно-ориентированным программированием, которое и демонстрируется паттерном Value-Object вместо одержимости примитивами. В случае использования decimal вы эту задачу никак не решите.

              P.S. Stackoverflow тоже советует использовать decimal для хранения денежных значений (там опять же про sql, но не думаю, что decimal в C# сильно отличается в этом плане).

              Сравнивать SQL с C# я бы вообще не стал. Разные это вещи.
                +2
                Попробую ответить сразу на 2 ваших комментария.

                1:

                Другой хабровчанин развернул вам мысль, которую я тоже пытался донести:
                3. Я попробую выразить мысль и скорее всего не только мою — наличествование класса Money это не плохо, но реализация с «копексами» это плохо, Money должен быть общим случаем — это decimal + currency code.

                ValueObject класс — это прекрасно, это действительно позволяет отделять слонов от рублей, но зачем использовать long для хранения суммы в копейках внутри класса? Для этого существует decimal. Да и для дальнейших рассчетов это гораздо удобнее. Скажем, при рассчетах сложного процента из-за использования long через какое-то время у вас накопится такой уровень ошибки из-за округления, что никакие балансы не сойдутся. Придет клиент, посчитает сумму на калькуляторе и начнет вопить, что вы воруете у него деньги.

                2:
                Нет, поменяют имена двух методов на подходящие для всех валют, например HigherUnit и LowerUnit.

                И будет у вас для каких-то валют LowerUnit1, LowerUnit2, а для каких-то — просто LowerUnit, так что ли? Lower Unit внутри программы вообще не нужен, для отображения в интерфейсе суммы в копейках или рублях с копейками используется концепция Value Formatter (наверняка в C# она тоже в каком-то виде существует), которая формирует из данных нужное пользователю представление. Понятно, что у вас есть legacy-код, но ведь вы затеяли рефакторинг для исправления положения — так исправляйте сразу так, чтобы избавиться от костылей, а не оборачивать их в дополнительный класс.

                Сравнивать SQL с C# я бы вообще не стал. Разные это вещи.

                Вещи разные, но концепция decimal у них одинаковая, и придумана она специально для денежных целей, чтобы не использовать float, который не гарантирует точности, и не городить велосипеды с int/long.
                  0
                  но зачем использовать long для хранения суммы в копейках внутри класса?

                  Согласен, внутри лучше сделать decimal. Только насколько я понял, другой хабравчанин имел ввиду, что Money это плохо и надо тупо использовать decimal.
                  для отображения в интерфейсе суммы в копейках или рублях с копейками используется концепция Value Formatter

                  Если планируется поддерживать такие валюты, то да, тогда какой-нибудь форматтер надо будет использовать. Я про это даже не думаю, в любом случае это ляжет на ООП спокойно. Посыл статьи — не использовать decimal для представления денег.
                  Вещи разные, но концепция decimal у них одинаковая, и придумана она специально для денежных целей, чтобы не использовать float, который не гарантирует точности, и не городить велосипеды с int/long.

                  Она не придумана для денежных целей, она придумана для десятичных значений. SQL не умеет классы, поэтому концепцию Money вы в нём не выразите. Придётся использовать decimal.
                    0
                    И каким образом накопятся ошибки округления? Money — неизменяемый тип. В данном случае работает строго с двумя знаками после запятой. Хотя в целом, да, с тем, что внутри надо сделать decimal — согласен. Это правильно.
                      0
                      Ошибки копятся элементарно. Скажем, есть у вас amount = Money.fromKopecs(10), вы в течение года каждый день должны увеличивать сумму на 1%: newAmount = amount * 0.01. При вашем подходе сумма меняться не будет: Long(Long(10) + Long(10) * 0.01) == Long(10). Даже если вы сконвертируете из Money в decimal, посчитаете и сконвертируете назад в Money — вы получите все те же 10 копеек. К концу года вы украдете у кого-то 10 * (1.01 ^ 365) — 10 = 3 рубля 67 копеек, вернув ему всего лишь те 10 копеек, которые он вложил изначально.
                        –1
                        > каждый день должны увеличивать сумму на 1%: newAmount = amount * 0.01

                        поэтому Money. Арифметика decimal не применима к деньгам, ни умножением, ни возведением в степень.

                        Нет такой бухгалтерии, что бы клиенту первые 4 дня не начислялось ничего, а на пятый день упала копеечка «суммарно».
            +2
            >> Фаулер при аналогичной реализации держит два открытых конструктора, один из которых принимает double, другой принимает long. Мне это не нравится категорически

            ИМХО можно закрыть конструктор и создавать инстансы Money только в фабрике с двумя методами — createByRubles, createByKopecs.

            P.S. Ставя минус, не забудьте написать, с чем вы не согласны
              +1
              Именно так и сделано в коде в статье.
                +1
                Виноват, проглядел
              +8
                    public decimal AmountInRubles {
                          get {
                              if (amountInKopecs < KopecFactor)
                                  return amountInKopecs;
                              return (decimal)amountInKopecs / KopecFactor;
                          }
                      }
              


              Шикарно.
                –1
                Жесть :-D
                Извините, код сегодня написал, накатал тесты, но этот кейс забыл. Просмотр элементарный. Код не в продакшне, разумеется :-D
                Уже поправил в статье. Спасибо, что указали.
                  –4
                  Вы не умеете программировать. Вас нельзя пускать писать код. Всё, что вы написали в статье, в языке C# пишется одним словом decimal. Спасибо, что вы придумали класс для умножения на сто, больше так не делайте, пожалуйста.
                    –2
                    Согласен. Еще интересует оверхед, когда нужно к примеру массив из 1000 и более значений. Что то мне подсказывает, что структура сожрет больше памяти и принесет еще большей головной боли при работе с большим кол-вом элементов.
                    ИМХО decimail и отдельный класс для работы с конвертацией.
                      –1
                      Если согласны, то вам пара вопросов из моего комментария. Начинайте с раздела «по существу» :) Нет, структура будет работать катастрофически быстро. С трудом представляю сценарий с продолбами в производительности со структурой.
                      +1
                      Ну, значит Фаулер и Марк Симан тоже не умеют программировать? Они-то рекомендуют использовать ValueObject. Что вы скажете по этому поводу? Пуститесь в рассуждения о том, что они вам не кумиры?

                      По существу:
                      1. С точки зрения читабельности: объясните что написано здесь: decimal a = 10m; Сколько это? Без учёта валюты.
                      2. Нужна интероперабельность с легаси-системой, которая в нашем случае есть. Она принимает всё только в копейках. Городим конвертацию?
                      3. Что если нужно нужно сделать перераспределение по разным счетам? Стандартная банковская фича. Разделить в некотором процентном соотношении сумму денег и перевести на разные счета. Городим ещё метод-расширение?
                        0
                        В msdn написано, что decimal это ValueObject. А рекомендуется это супротив использования float/double, что помитуя о способе их хранения действительно плохая идея.

                        1. Money(10), а здесь сколько? В вашем случае с 2мя конструкторами это будет 0.1 рубля как я понимаю
                        2. А если вам будет нужно общаться с системами которым нужна точность до 3 знака? Конечно конвертацию, только почему городим — если система принимает значение умноженное на 100, так с ней и общайтесь — 10m * 100
                        3. Я попробую выразить мысль и скорее всего не только мою — наличествование класса Money это не плохо, но реализация с «копексами» это плохо, Money должен быть общим случаем — это decimal + currency code. Даже если вы опускаете код валюты, то общий способ работы со значением целиком, то есть если у вас валюта рубли, то 10 копеек нет, есть 0.10m рубля — тут и минусов float нет и сразу видно до какой степени точно это значение.
                          0
                          1. Money(10), а здесь сколько? В вашем случае с 2мя конструкторами это будет 0.1 рубля как я понимаю

                          Конструкторы закрыты. Сделаны специальные фабрики.
                          2. А если вам будет нужно общаться с системами которым нужна точность до 3 знака? Конечно конвертацию, только почему городим — если система принимает значение умноженное на 100, так с ней и общайтесь — 10m * 100

                          Нам это не нужно, но если бы было нужно, то сделали бы всё равно Money. Такая бизнес-логика должна быть инкапсулирована в объекте. Не надо повсюду делать умножения на 100. То, что вы предлагает 10m*100 — плохо.
                          Я попробую выразить мысль и скорее всего не только мою — наличествование класса Money это не плохо, но реализация с «копексами» это плохо,

                          Это решается текущими потребностями, а не понятиями типа "...Money должен быть". Money, как и его имплементация никому ничего не должны :-D Шучу, конечно, но в каждой шутке есть доля шутки. Всё дело в домене. Нужна валюта — ок. Не нужна — значит ок, не нужна.

                          Подытоживая — объект Money стоит завести, чтобы конвертации были проще, чтобы бизнес-логика в будущем могла быть накручена, проверки различные. Для этого и используются объекты в ООП. То, что в C# есть decimal — это прекрасно. Это великолепный тип, но он слабый для того, чтобы представлять концепцию. Чтобы представлять именно деньги он должен быть инкапсулирован в объект денег. То же самое относится к таким понятиям как IPAddress и ZipCode. Их нельзя представлять строками или другими любыми примитивами.

                          decimal предназначен для работы со значениями десятичными, типа денег, но это не сама концепция денег. Нельзя путать одно с другим.
                            –1
                            Ещё вдогонку.

                            10m*100 = одна тысяча десятичных слонов

                            Надеюсь так понятнее :)
                            –5
                            Очень жалко, что на хабре нельзя матом писать. Фаулер и Марк Симан программировать не умеют, они умеют писать про то, что «деньги надо сделать ValueObject». После этого вы делаете деньги ValueObject, пишете всё прекрасно, паттерны, красиво, блестяще и, простите, путаете копейки с рублями в одном месте. Привет, вы попали на деньги. Ах нет, это же не продакшн код, это для хабрахабра.

                            Моё глупое мнение — человека надо пускать работать с паттернами хотя бы после того, как он сраную таблицу умножения выучит. Что такое таблица умножения можно посмотреть в википедии, если не найдете, дам ссылку. Спасибо за внимание.
                              0
                              Вам просто необходимо научиться хорошим манерам. В первую очередь понять, что вы не самый умный. И прочитать хорошую статью о том, как правильно себя вести в цивилизованном обществе, а не в обществе дикарей.
                                –3
                                Не. Я сторонник говно называть говном. А вы продолжайте умножать на сто при помощи специального класса.
                      0
                      Про странности кода выше уже написали, от себя же добавлю вот такой пример:
                          public struct Pressure
                          {
                              private float value;
                              private Pressure(float value) { this.value = value; }
                      
                              public float Pa() { return value; }
                              public static Pressure Pa(float value) { return new Pressure(value); }
                      
                              public float hPa() { return value / 100; }
                              public static Pressure hPa(float value) { return new Pressure(value * 100); }
                      
                              public float mmHg() { return value / 133.322368f; }
                              public static Pressure mmHg(float value) { return new Pressure(value * 133.322368f); }
                          }
                      


                      Этот подход мне очень даже пригодился, когда пришлось получать метеоданные из разных открытых веб-сервисов — а каждый сервис использовал свои единицы измерения…
                        +1
                        лучше было бы var money = new Money(200, 00);
                        а ещё лучше var money = new Money(200, 00, CurrencyType.RUB);
                        коды валют en.wikipedia.org/wiki/ISO_4217 можно даже найти в виде C#-кода csharpmoney.codeplex.com/SourceControl/latest#Useful.Money/CurrencyCode.cs
                          0
                          [del]
                            –1
                            Оперирование разными валютами просто не рассматривал. А так, да, вы правы. Промазал уровнем.
                            • UFO just landed and posted this here
                              +5
                              А почему не хранить все в копейках, а конвертацию в рубли производить только на границе с UI через MoneyFormatter какой нибудь?
                                0
                                О том и речь, возникает необходимость конвертации так и так. Нет смысла выдумывать конвертеры, нужно просто сделать нормальный тип, который представляет концепцию. Использование примитивов в таком случае будет означать отсутствие концепции.
                                  0
                                  У нас так сделано — все значения хранились и обрабатывались в копейках (центах), и только при выводе форматировались. А потом пришлось поддерживать форинт, и всё перестало работать было понаделано много костылей, так как в их валюте центы не используются, и даже спустя лет восемь там-сям вылезают баги, так как во многих местах рядом лежат как центы других валют, так и целочисленные форинты, и я рад, что мне не пришлось это поддерживать :)
                                    0
                                    Да, однажды всё начнёт расползаться. Ваш пример тому доказательство. Не надо городить огороды там, где нужен тип.
                                  0
                                  Можете начать бросать в меня помидоры, потому что я не осилил это гору комментов, но я скажу вот что…

                                  В той статье про «одержимость примитивами» сказано:
                                  «Инкапсулируем примитивное значение в объект-значение, который содержит соответствующие защитные выражения и т.д., для того, чтобы гарантировать, что возможно создать только корректные экземпляры.»

                                  Уже на этом этапе имела место ошибка в понимании этого текста, т.к. decimal уже гарантирует только корректные экземпляры.

                                  Я сам разрабатываю довольно сложное финансовое приложение, которое давно крутится в продакшене, и мне даже в кошмарном сне не мог присниться подобный класс. Я бегло пробежался по тексту и комментам, но не увидел ответа на простой вопрос: зачем хранить сумму в копейках?

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

                                  Если Вы решите хранить и оперировать деньгами в decimal и перепишете этот класс соответствующим образом, чем он станет? Оболочкой над decimal.

                                  И поверьте, если у Вас в системе фигурируют суммы в «long kopecs» (что изначально являлось недоработкой проектирования), то запись «decimal money = kopecs/100» будет гораздо очевиднее незнакомому с вашим кодом человеку, чем «var money = new Money(kopecs)», т.к. с Вашим Money он вообще не знаком, и ему придётся заглянуть в него и ужаснуться.

                                  Ваш класс только тогда начнёт иметь смысл, когда не будет являться оболочкой над decimal (который и так уже всё гарантирует), а будет содержать в себе ещё что-то. Например, код валюты.

                                  А до тех пор лучше заняться избавлением проекта от «long kopecs», а не потворствовать этому. А эта абстракция, раз уж она уже есть, пусть существует в виде оболочки над decimal, может, когда-нибудь и пригодится…
                                    0
                                    Читайте комменты. Обратите внимание, что конструкторы закрыты. Не делайте поспешных выводов. Фаулер однажды сказал, что люди его ненавидят, но рано или поздно приходят к его же мнению, только через набитие шишек.
                                    0
                                    Если вы напишите класс ZipCode, то он станет оболочкой над string и что дальше?
                                      0
                                      Обратите внимание, что конструкторы закрыты

                                      Без разницы. Money.FromKopecs — ничем не лучше.

                                      Если вы напишите класс ZipCode, то он станет оболочкой над string и что дальше?

                                      Я уже цитировал, и процитирую ещё раз:
                                      Инкапсулируем примитивное значение в объект-значение, который содержит соответствующие защитные выражения и т.д., для того, чтобы гарантировать, что возможно создать только корректные экземпляры.

                                      Где у Вас проверки на корректность как в случае в ZipCode?

                                      Фаулер однажды сказал, что люди его ненавидят, но рано или поздно приходят к его же мнению, только через набитие шишек.

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

                                      Хорошие решения призваны бороться со сложностью, а не создавать её на пустом месте.
                                        0
                                        Money.FromRubles(10) гораздо лучше чем decimal a = 10m; Если вы не согласны, это ваше мнение. Ну, так тот разработчик сделал глупости, вероятно. В статье всё нормально. Здесь нет десяти шаблонов и иерархии классов. Есть структура, которая делает код более читабельным, более простым для интероперабельности с системами, которые требуют всё только в копейках и расширяема, если потребуется введение валют.
                                          0
                                          Money.FromRubles(10) гораздо лучше чем decimal a = 10m;

                                          Есть структура, которая делает код более читабельным

                                          Да Вы, верно, шутите? По-Вашему это более читабельно? Дело не в согласии несогласии, а в фактах. Сколько человек на Земле знают про Вашу структуру Money? И сколько человек на Земле знают про тип decimal? Глядя на запись «Money.FromRubles(10)» у меня сразу же возникает куча вопросов: а как эта структура будет хранить данные — с какой точностью? А не конвертирует ли она, случайно, рубли во что-то ещё в этой ф-ции? А смогу ли я использовать её в мат. выражениях? А насколько она производительна? И т.д. Вы сами посмотрите на этот код через полгодика и будете судорожно пытаться вспомнить, зачем Вы этот Money вообще ввели.

                                          Представьте, что Вы открыли чужой код. Что для Вас будет более читабельно — вот это: «new DateTime(2015, 4, 5)», или вот это: «5.April(2015)»?

                                          Ну, так тот разработчик сделал глупости, вероятно

                                          Самая главная его глупость — это то, что он слепо применял всё подряд о чём прочёл. И здесь я вижу ровно то же самое. Меж тем,, как надо совершенно ясно понимать, для чего создавались те или иные решения, и всегда думать, прежде чем применять что-то, проходя самостоятельно тот путь, который прошли до Вас, но уже гораздо быстрее, т.к. на пути есть указатели. А если Вы сразу переноситесь в конечную точку рассуждений, Вы с очень большой вероятностью оказываетесь не там.

                                          и расширяема, если потребуется введение валют.

                                          А если потребуется учесть нашествие инопланетян? Предусматривать какие-то абстракции нужно только в том случае, когда вероятность их применения достаточно высока. Иначе от них никакой пользы, кроме вреда.

                                          И даже в случае проверки корректности данных (что совершенно не относится к Вашему примеру)… Предположим, что Вам нужно принимать от пользователя, сохранять и отображать Zip-Code, а вы, скажем, работаете с EntityFramework, в котором корректность обеспечивается атрибутами на свойствах модели. Нужна ли в этом случае обёртка над string? Нет. Всё зависит от ситуации, и универсальных на все 100% решений не существует. Никто и никогда не избавит Вас от необходимости думать, когда Вы принимаете решение использовать какой-либо готовый подход.

                                          Ну, или же, оборачивайте в структуры все подряд: string'и, int'ы, float'ы. Оберните заодно и все объекты Framework'а. А что, вдруг пригодится? ;)
                                            0
                                            Что для Вас будет более читабельно — вот это: «new DateTime(2015, 4, 5)», или вот это: «5.April(2015)»?

                                            Для меня будет более читабельно Money.FromRubles(10).

                                            И здесь я вижу ровно то же самое. Меж тем,, как надо совершенно ясно понимать, для чего создавались те или иные решения, и всегда думать, прежде чем применять что-то, проходя самостоятельно тот путь, который прошли до Вас, но уже гораздо быстрее, т.к. на пути есть указатели. А если Вы сразу переноситесь в конечную точку рассуждений, Вы с очень большой вероятностью оказываетесь не там.

                                            Если вы внимательно прочитаете статью, то увидите, что мы изначально использовали, где могли decimal и приходилось заниматься конвертацией, потому что есть другая система, которая принимает всё в копейках. Мы пришли к заключению, что нам нужен Value-Object. Мы не применяли его сразу, несмотря на то, что знали о существовании этого подхода. Некоторые люди упорно не читают статьи и комментарии, пускаясь в рассуждения, основанные на собственных домыслах.

                                            Что касается EntityFramework. Это вообще отдельная история. Такие фрэймворки проталкивают насильно использование DTO. Не объектов, как объектов, которые имеют поведение, а объектов, которые являются структурами данных. На все 100% решений нет, а кто об этом говорил? Никто не собирается всё оборачивать. Мне жаль, что у меня не получается донести до вас мысль о том, что концепцию нельзя представить примитивом.
                                              0
                                              потому что есть другая система, которая принимает всё в копейках.

                                              Надо было так и написать. Предложение «В ПО, которое разрабатывает наша команда используются денежные значения в рублях и копейках.» не говорит об этом.

                                              Лично я бы преобразовывал суммы сразу при общении с этой другой системой в decimal, только и всего. И удалил бы напрочь «long kopeks» из проекта, как кошмарный сон, чтобы вот вообще никаких следов это безобразия не осталось :)

                                              Мне жаль, что у меня не получается донести до вас мысль о том, что концепцию нельзя представить примитивом.

                                              Эмм… А «концепция»-то в чём заключается? Чем вот по сути эта структура отличается от long? Умножением и делением на 100? Это и есть та «концепция», которую непременно надо куда-то инкапсулировать?

                                              Эта проблема решается не усложнением кода всего проекта, а правильным написанием кода, который отвечает за взаимодействие с этой другой системой, который должен быть обособлен и полностью абстрагирован от всего остального. Чтобы основной проект вот даже слыхом не слыхивал о том, что суммы где-то хранятся в копейках ))

                                              Не объектов, как объектов, которые имеют поведение, а объектов, которые являются структурами данных

                                              Ну, лично в моём проекте там полно поведения :) Некоторые вещи прекрасно решаются интерфейсами и методами расширения. Я не идеализирую EF, ни в коем случае. Там огромная куча недоработок…

                                              Может быть я не прав, но я воспринял эту статью. как воспитание ненависти к примитивам ради неких призрачных и неописываемых словами мистических концепций. А это, по моему глубокому убеждению, путь исключительно ложный и вредный, особенно для незрелых умов, которые, не научившись пользоваться и примитивами, стремятся наплодить как можно больше структур и классов невнятного назначения, потому что так написано в умной книжке. Это я не о Вас, о многих из тех, кто это прочтёт.

                                              Подчёркиваю, любой паттерн создан для решения каких-то проблем. Если этих проблем нет и не предвидится, любое их использование — это дорога в ад. А если проблема есть, то надо сначала подумать, какими способами можно её решить, собственной головой. Умный не тот, кто прочитал много книг. А то, кто умеет перерабатывать информацию и генерировать новую (если она конечно полезна). Тот, у кого развита интуиция за базе собственного опыта. Какие-либо авторитеты и совершенно ненужные знания обычно не помогают, а вредят интеллектуальному развитию и личному росту. Вот ей Богу, если бы тот разработчик, которого я упоминал, не прочитал ни одной книги, он бы написал намного более качественный и понятный код, а главное — научился бы думать.
                                                0
                                                Может быть я не прав, но я воспринял эту статью. как воспитание ненависти к примитивам ради неких призрачных и неописываемых словами мистических концепций.

                                                Конечно же, цель была не в этом.

                                                Да, то, что люди забывают думать своей головой это проблема. Но это другая проблема и ей нужно посвящать отдельную статью. В инженерии нет универсальных ответов на все вопросы.

                                                Нам подходит наша реализация Money, её кто-то сможет адаптировать под свои нужды, особенно если появятся методы распределения в процентном соотношении для перевода по разным банковским счетам. И да, надо будет не забыть убрать long изнутрей структуры, а то мало ли что :) Как сделаю, так и статью проапдейчу.
                                                  0
                                                  Отлично, рад что мы поняли друг друга ;) Просто, глядя на этот конкретный пример, у меня зародились подозрения о слепом использовании каких-то подходов (о чём я писал). Но если это делается с умом, то это конечно другое дело. Будем ждать апдейта и новых статей, и успехов Вам в разработке!

                                                  В общем, как писал SergeyT:

                                                  Помимо пользы, от паттернов проектирования может быть и вред. Бездумное и неразумное использование любого даже самого полезного инструмента или технологии приведет к «абсурду и коррупции». Сколько раз вы сталкивались с проблемой “overengineering-а”, когда для реализации простой концепции использовался десяток паттернов проектирования? В общем, это очередной пример того, что прагматизм и здравый смысл, как всегда является лучшим выбором, и паттерны проектирования – не исключение.

                                                  Да восторжествует здравый смысл, и всем нам будет счастье! :)
                                                  0
                                                  Однако, повторюсь.
                                                  У меня есть подозрение, что decimal для выражения денежных величин может использовать на проекте год и даже два. Ровно до тех пор, пока кто-то не перепутает копейки с рублями и сравнение не отработает так, как ожидалось. С decimal перепутать инициализацию копейками и рублями гораздо проще. Останусь при своём мнении.
                                              0
                                              На самом деле, даже обёртка над decimal может пригодиться, если понадобится унифицировать обработку разных по смыслу данных. Самый простой пример — ToString():
                                              var items = new object [ ] { someMoney, someOtherValue };
                                              foreach (var item in items) Show(item.ToString());

                                              Если бы в Вашем примере был реализован ToString(), в нём был бы хоть какой-то смысл. Хотя, и то этот смысл сомнителен, т.к. необходимость ради одного ToString реализовывать кучу операторов (для превращения Money в полноценный decimal) — это большой вопрос, и зависит опять-таки от контекста, в роли которого выступает проект.
                                        –2
                                        Вообще-то, ValueObject — это больше не против одержимости примитивами (которая побеждается соблюдением инкапсуляции — держите близкие данные и методы для их обработки рядом), а для иммутабельности. У Вас все сделано правильно, но стоило подчеркнуть этот момент в статье.
                                          0
                                          Примитивы тоже неизменяемы. Ради одной только неизменяемости ValueObject заводить не нужно.

                                        Only users with full accounts can post comments. Log in, please.