価値オブジェクトとしてのお金

この記事で説明されている問題は長い間よく知られているため、ほとんどの場合、このトピックに精通していない初心者向けです。



私たちのチームが開発するソフトウェアは、ルーブルとコペックで金銭的価値を使用しています。 プリミティブを使用して金銭的価値を表現することはアンチパターンであることを最初に知っていました。 しかし、アプリケーションが開発されたとき、プリミティブの使用に関連する問題につまずくことができず、明らかに幸運であり、すべてがうまくいきました。 とりあえず。

この問題と、システム全体にわたるintやdecimalのようなプリミティブの使用を完全に忘れていました。 そして今、私たちが問題を感じた最初の方法を書いたとき、この技術的負債を覚えて、プリミティブの代わりに金銭的抽象化を使用するようにすべてを書き直さなければなりませんでした。



一般に、このアンチパターンは「プリミティブに対する執着」であり、非常に一般的です。たとえば、ZipCodeのintまたはstringを使用して、IPアドレスを表す文字列です。



そして、ここに書かれたたわごとです:

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 }
      
      





ここでは、5つの値の混乱を確認できます。 セントまたはルーブル-あなたは仕事が何で起こっているのかを理解する必要があるすべての場所。 10進数とintの間で変換するために、拡張メソッドKopToRubとRubToKopが作成されました。これは、ちなみに、プリミティブへの執着の最初の兆候の1つです。



その結果、独自の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)); } }
      
      





同様の実装を持つFowlerは、2つのオープンコンストラクターを保持します。1つはdoubleを受け入れ、もう1つはlongを受け入れます。 コードが何を意味するのか、私は絶対に嫌いです

 var money = new Money(200); // : 200   200 =2.?
      
      





同じ理由で、暗黙的なキャストの可能性を与えるのは良くありません。 これは、暗黙的なキャストがlongのみで許可されているか、longとdecimalで許可されているかに関係なく悪いです(decimitの暗黙的な変換は通常は可能ですが、だれかがMoney b = 200mと書いたと思うでしょう)彼は200コペックを意味しなかったという意味ではありませんが、mはそれを単にコンパイルしたと考えています)。

 Money a = 200; // : 200   200 =2.? Money b = 200m; //   ,    ?
      
      





異なる通貨で作業を実装する必要がある場合は、キャストファクターを知っている通貨のクラスを作成するだけです(たとえば、ドルとセントの場合は100、ルーブルとセントの場合は100)。 異なる通貨での値の比較は、ほとんどの場合禁止する必要があります(もちろん、為替レートにアクセスできる場合を除きます)。



要約:テストでアンチパターン「プリミティブへの執着」をテストせず、すぐに通常の抽象化を行います。そうしないと、リファクタリングのために数時間を殺す必要があります。 そして、神に禁じられています。もしバグに遭遇し、プリミティブに依存している場合、それらに遭遇するのは非常に簡単です。



PSコメントでの継続的な議論の結果として、すべての質問に対する普遍的な答えはないことを付け加えたいと思います。 お金を概念として表すために小数を使用したい場合-神のために、異なるアプローチのすべての長所と短所を理解してください。



All Articles