第10章:VO実装② Money(通貨・丸め・演算)💰🧮✨
(学内カフェ注文アプリ☕️🧾)
1) この章のゴール🎯✨
この章が終わると、こんな状態になります👇😆
decimalをそのまま金額に使うのを卒業できる💪💰- 金額=Amount+Currency で表現できる🌍✨
- 同じ通貨どうしだけ足せる、みたいなルールをコードで守れる🔒✅
- 丸め(Round)を散らさず、Moneyの中で統一できる🧹✨
- テストで安心して運用できる🧪🛡️
(※今どきの前提として、.NET 10(LTS)が 2025-11-11 にリリースされていて、C# 14 と一緒に使えます📦✨)(Microsoft for Developers)
2) まず「decimalだけ金額」って何が怖いの?😱💥
たとえばカフェで、こういうコードを書きがち👇
decimal price = 480;decimal total = price * qty;- 税や割引も
decimalで計算…
これ、通貨が消えるんです😵 JPYなの?USDなの?EURなの?って情報がどこにもない…🌪️
さらに、丸めがバラバラになりがち👇
- ある場所では
Math.Round - ある場所では
decimal.Round - ある場所では「丸めずに放置」
結果:合計が1円ズレるとか、地味に嫌なバグが出ます😇💸
3) Money VOの設計の芯💎✨

Moneyは、最低これを持つのが鉄板です👇
Amount(金額)💰Currency(通貨)🌍
通貨コードは基本 ISO 4217(JPY, USD, EUR…) を使うのが定番です📌(ISO)
そして、通貨には「小数桁(最小単位)」があるよね、って話🪙✨
- JPYは小数0桁(円)
- USDは小数2桁(セント) みたいな世界観です🌍💡(代表例)(ウィキペディア)
4) 金額の型は decimal が基本でOK🙂💰
C#では decimal が「金融・通貨計算に向く」型として位置づけられています✅(Microsoft Learn)
ただし!ここ大事👇😤
decimal を使っても 丸めが不要になるわけじゃない です。
「丸め方をどこで統一するか」が勝負です🔥(Microsoft Learn)
5) 丸めの基本(超重要)🔁⚠️

✅ decimal.Round のデフォルトは「ToEven(銀行丸め)」
decimal.Round(x, 2) みたいに modeを指定しないと、
MidpointRounding.ToEven(いわゆる銀行丸め)になります🏦✨(Microsoft Learn)
例(小数2桁)👇
2.345 → 2.342.355 → 2.36みたいな挙動になることがあります🧠(Microsoft Learn)
✅ 業務では「AwayFromZero(いわゆる四捨五入寄り)」が欲しいことも多い
なので Money 側で 明示的に mode を決めるのが安全です🔒✨
6) 実装してみよう:Currency(通貨)🌍✨
まずは「通貨の情報」を1か所に集めます📦
namespace Cafe.Domain;
public readonly record struct Currency
{
public string Code { get; }
public int MinorUnit { get; } // 小数桁(JPY=0, USD=2 みたいな)
private Currency(string code, int minorUnit)
{
if (string.IsNullOrWhiteSpace(code)) throw new ArgumentException("Currency code is required.");
code = code.Trim().ToUpperInvariant();
if (code.Length != 3) throw new ArgumentException("Currency code must be 3 letters (ISO 4217).");
if (minorUnit is < 0 or > 4) throw new ArgumentOutOfRangeException(nameof(minorUnit));
Code = code;
MinorUnit = minorUnit;
}
// 学習用に、まずは代表だけ持つ(必要になったら増やす)
public static readonly Currency Jpy = new("JPY", 0);
public static readonly Currency Usd = new("USD", 2);
public static readonly Currency Eur = new("EUR", 2);
public static Currency Of(string code) => code.Trim().ToUpperInvariant() switch
{
"JPY" => Jpy,
"USD" => Usd,
"EUR" => Eur,
_ => throw new ArgumentException($"Unsupported currency: {code}")
};
public override string ToString() => Code;
}
ポイント💡
- 「小数桁(MinorUnit)」をここに置くと、丸めが統一できる✨
- 全通貨を網羅するのは大変なので、学習では代表だけでOK👌
7) 実装してみよう:Money(本体)💰✨
狙いはこれ👇
- 生成時に 通貨の桁数に丸める
- 足し算は 同じ通貨だけ
- 比較も 同じ通貨だけ
namespace Cafe.Domain;
public readonly record struct Money : IComparable<Money>
{
public decimal Amount { get; }
public Currency Currency { get; }
public Money(decimal amount, Currency currency, MidpointRounding rounding = MidpointRounding.ToEven)
{
Currency = currency;
Amount = RoundToCurrency(amount, currency, rounding);
}
private static decimal RoundToCurrency(decimal value, Currency currency, MidpointRounding rounding)
=> decimal.Round(value, currency.MinorUnit, rounding);
private static void EnsureSameCurrency(Money a, Money b)
{
if (a.Currency.Code != b.Currency.Code)
throw new InvalidOperationException($"Currency mismatch: {a.Currency} vs {b.Currency}");
}
public Money Add(Money other)
{
EnsureSameCurrency(this, other);
return new Money(Amount + other.Amount, Currency);
}
public Money Subtract(Money other)
{
EnsureSameCurrency(this, other);
return new Money(Amount - other.Amount, Currency);
}
// 係数を掛ける(例:2杯分、割引、税率など)
public Money Multiply(decimal factor, MidpointRounding rounding = MidpointRounding.ToEven)
=> new Money(Amount * factor, Currency, rounding);
public int CompareTo(Money other)
{
EnsureSameCurrency(this, other);
return Amount.CompareTo(other.Amount);
}
public static Money operator +(Money a, Money b) => a.Add(b);
public static Money operator -(Money a, Money b) => a.Subtract(b);
public override string ToString() => $"{Amount} {Currency.Code}";
}
ここが「Moneyにしてよかった〜😭✨」ってなる所👇
Currency mismatchを **即死(例外)**させられる🧨 → 変な合算が「静かに」起きない✅
8) 税・割引・丸めはどこでやる?🤔🧾
ここ、現場でめっちゃ差が出ます⚠️ 「税の丸め」って、たとえば👇
- 明細ごとに丸めるのか
- 合計してから丸めるのか で結果が変わることがあります😇💸
学習用の落とし所としては👇
- Moneyの中で丸めの道具は用意する🧰✨
- 「どのタイミングで丸めるか」は、Order側(ルール側)で決める🧠🧾
たとえば「税込み金額」を簡易に作るなら👇
namespace Cafe.Domain;
public static class MoneyExtensions
{
// taxRate: 0.10m = 10%
public static Money AddTax(this Money baseMoney, decimal taxRate)
{
// 例:四捨五入寄り(AwayFromZero)にしたい、などのルールはここで明示
var tax = baseMoney.Multiply(taxRate, MidpointRounding.AwayFromZero);
return baseMoney + tax;
}
}
※ decimal.Round の既定が ToEven なので、ここで意思を持って選ぶのが大事です🫶(Microsoft Learn)
9) 使ってみる:カフェの明細で合計を出す☕️➕💰
namespace Cafe.Domain;
public sealed class OrderLine
{
public string ItemName { get; }
public Money UnitPrice { get; }
public int Quantity { get; }
public OrderLine(string itemName, Money unitPrice, int quantity)
{
if (string.IsNullOrWhiteSpace(itemName)) throw new ArgumentException("Item name is required.");
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
ItemName = itemName.Trim();
UnitPrice = unitPrice;
Quantity = quantity;
}
public Money LineTotal => UnitPrice.Multiply(Quantity);
}
これで「単価×数量」の時点で Moneyが保たれるので、 どこまで行っても通貨が消えません🌍✨
10) テストを書こう(Moneyはテスト相性が最高)🧪💖
xUnitでサクッといきます👇
using Cafe.Domain;
using Xunit;
public class MoneyTests
{
[Fact]
public void Add_SameCurrency_Works()
{
var a = new Money(100m, Currency.Jpy);
var b = new Money(50m, Currency.Jpy);
var sum = a + b;
Assert.Equal(new Money(150m, Currency.Jpy), sum);
}
[Fact]
public void Add_DifferentCurrency_Throws()
{
var jpy = new Money(100m, Currency.Jpy);
var usd = new Money(1m, Currency.Usd);
Assert.Throws<InvalidOperationException>(() => _ = jpy + usd);
}
[Fact]
public void Jpy_RoundsTo0Decimals()
{
var m = new Money(100.6m, Currency.Jpy, MidpointRounding.AwayFromZero);
Assert.Equal(new Money(101m, Currency.Jpy), m);
}
[Fact]
public void Usd_RoundsTo2Decimals()
{
var m = new Money(1.239m, Currency.Usd, MidpointRounding.AwayFromZero);
Assert.Equal(new Money(1.24m, Currency.Usd), m);
}
}
テストで守れるポイント👇😍
- 通貨違いを足す事故🚫
- JPYが小数を持ってしまう事故🚫
- 丸め方のズレ事故🚫
11) ミニ演習(10〜20分)⏱️✨
演習A:Moneyを実戦っぽくする💪
MoneyにIsZeroを追加してみよう🟰✨MoneyにZero(Currency c)を追加してみよう0️⃣💰
演習B:OrderLineの合計を作る🧾
List<OrderLine>を足し上げて、注文合計をMoneyで返してみよう➕✨- 通貨が混ざったら例外になるのも確認しよう🧨
12) 🤖 AI活用(Copilot / Codexで爆速)✨
そのままコピペで使える指示例💬
- 「
Moneyをreadonly record structで作って。通貨違いの加算は例外、生成時に MinorUnit で丸め、xUnitテストも付けて。丸めモードは引数で指定可能にして。」 - 「JPY(0桁)とUSD(2桁)の丸めテストケースを10個出して。境界値(ちょうど0.005とか)も含めて。」
- 「この Money 実装の危険ポイントをレビューして。『丸めのタイミング』『税の計算位置』『負の値の扱い』に注目して。」
AIは雛形とテスト案が得意なので、最後の判断(どこで丸めるか)は人間が決めるのがコツです🧠🤝🤖
13) よくある落とし穴まとめ🧯✨
- ✅ 丸めをあちこちでやる → Moneyに寄せる🧹
- ✅ 通貨がstringのまま散らばる →
Currencyに集める📦 - ✅ 税の丸めタイミングが未定 → Orderのルールとして決める🧾
- ✅
decimal.Roundの既定が ToEven なのを知らない → 明示しよう🏦⚠️(Microsoft Learn)
14) まとめ(この章で得た武器)🗡️✨
- Money = Amount + Currency 🌍💰
- 丸めは Money に寄せて統一🔒
- 「同通貨だけ演算」をコードで守る✅
- テストが超書きやすくて、安心が増える🧪💖
次の章(第11章)では、Money以外の Quantity / Percentage / Code を量産できる形にして、プリミティブ地獄をどんどん減らしていこうね〜😆📦💎✨