🦁

値オブジェクトとインターフェースの実装でドメインモデリング ー シンプルなモデルを設計してみる

2024/02/13に公開

株式会社ジェイテックジャパン CTOの高丘 @tomohisaです。この記事は、C# TokyoというC#のコミュニティでの企画、新企画! お題のコードを書いてみよう! 第 2 回の実装を書いていたら、値オブジェクトとインターフェースとその実装の良いサンプルになると思って記事を書いています。

今回のお題と基本方針

今回のお題は以下のようなものです。

今回のお題

C# で書いてください。
次の文章の X などを引数として受け取って
おつりを出力するコンソールアプリを作ってください。

文章

隆くんは、一番町のラーメン屋さんで 1080 円の濃厚 MAX ラーメンを
980 円の叉焼スペシャル炒飯と一緒に食べるのが大好きです。
隆くんが X 円を持って濃厚 MAX ラーメンを Y 杯
食べたときのおつりはいくらでしょう?

お釣りを計算するだけでなく、エラーがちゃんと出たり、仕様追加をしやすいことなどを考えて、ドメインモデル的に設計してみました。

仕様の解釈

仕様が不確定な部分があるため、このように解釈しました。

  • X円でY杯のラーメンが買えなかったらエラー
  • ラーメンをY杯買った残金で、可能なだけチャーハンを買う
  • ラーメンとチャーハン購入後のお釣りを表示する

実装の方針

  • recordを使い、値オブジェクトを設計する
    • 日本円
    • 個数
    • 商品インターフェース
      • ラーメン
      • チャーハン
    • オーダー詳細 商品と個数
  • recordとInterfaceを使い、オーダーの状況の推移を型で表現する
    • オーダーインターフェース
      • AddingOrder 未確定のオーダー
        • 現在のオーダーに追加購入可能かを確認する
        • オーダーを確定する
      • ConfirmedOrder 確定後のオーダー
        • 受け取り、売り上げ、お釣りなどの情報を持つ
      • ErrorOrder
        • エラーメッセージを持つ
  • 入力値はしっかりとした検証が必要なのでコマンドオブジェクトとし、検証を担当
    • ラーメン購入コマンド
    • コマンドハンドラー(この場合はProgram.cs)
  • テストもできるだけ書く

上記のように設計して進めていきます。

完成したプログラムをこちらにアップロードしていますので、ぜひご覧ください。

https://github.com/tomohisa/CSharpTokyoPlayground/tree/main/202402

値オブジェクト

値オブジェクト(Value Object)は値の種類ごとに型を割り当てることにより、検証や、同じ値であることの検証や計算式の意味づけをしやすくすることなどに用いることができます。

まずシンプルな個数オブジェクトはこんな感じです。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/ItemAmount.cs

基本的にはC#のrecordオブジェクトを使ったシンプルなオブジェクトとして定義しています。データ検証は通常は属性を定義したらフレームワークが自動で行ってくれますが、このシステムはフレームワークを何も使っていないため、Parse(string)staticメソッドを定義します。ターミナルからは文字列で入力するので、文字列から数字に変換しつつ、正しい場合はオブジェクト、エラーがある場合は、エラー文字列を返します。安易にThrowせずに、エラー文字列を返すことにより、複数のエラーを同時に表示できるようにしています。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/JapaneseYen.cs

続いて商品ですが、いろいろな商品が考えられるので、インターフェースで商品であることを定義するのと、今回の対象商品であるラーメンとチャーハンの商品を作ります。基本機能である、名前と値段に関してはインターフェース機能からアクセスできるようにしてあります。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/IItem.cs

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/濃厚MAXラーメン.cs

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/叉焼スペシャル炒飯.cs

もう一つ必要なのは、オーダー詳細です。これは同じ商品を複数頼むことを可能にするために、商品ごとにレコードを作るために使われます。シンプルな内容です。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/OrderDetail.cs

ドメインロジックを型の推移で表現する

続いて、ドメインロジックを定義していきましょう。今回のお題では、データの永続化に関しては考えずに扱っていきます。

基本的な概念として、オーダーという概念があり、これはどの商品を購入したかを表します。ただ、オーダーにはフェーズ(段階)があります。以下のようなものです。

  • 支払い前のオーダーに商品を追加しているフェーズ(段階)
  • 支払いとともに注文が確定したフェーズ(段階)
  • (今回のシステムでは関係ありませんが)調理してテーブルに出す前のフェーズ
  • (今回のシステムでは関係ありませんが)食後に支払いを受けるフェーズ

上記の方法をインターフェースとその実装クラスで設計します。ですので以下のようなクラスの型遷移をします。

  • 支払い前の調整時は AddingOrder オーダー追加中クラスで管理
    • 支払い可能かの確認はこのクラスで行う
    • 支払い処理はこのクラスに対して行う
  • エラーが起きたらErrorOrderにメッセージと共に返す
  • オーダーが確定したら注文確定処理を行いConfirmedOrderとなる
    • お釣りなどの情報の取得はこのクラスで行う

ここでのポイントはIOrderの元のインターフェースに支払い可能か確認する機能はなく、AddingOrderのみに機能があるということです。それにより、正しくないデータの流れの際には行いたくないことは行いないという制限を、if文ではなく、データを保持している型で制限することにより、間違えにくいドメインモデルに育てていくことができます。

基底インターフェースはこちらです。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/IOrder.cs

そして、支払い前の実装、AddingOrderはこちらです。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/AddingOrder.cs

確定後のConfirmedOrderはこちらです。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/ConfirmedOrder.cs

エラーが起きた時のErrorOrderはこちらです。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/ErrorOrder.cs

最近僕が行っているドメインモデルの方法は、関数型を用いたシステム開発で用いられている代数的データ型がうまく表せないC#でもインターフェースとその実装でJSON型に型を保存できる、JsonDerivedTypeを使用しています。

このプログラムではJSONに保存することはありませんが、Cosmos DBなどのドキュメントデータベースにインターフェース型のまま保存すると、通常は全ての型のデータを保存できないのですが、JsonDerivedTypeを使用することにより、実装した型の情報を保存することができます。

JsonDerivedTypeの仕様に関しては僕の以前に書いたブログ、C#でインターフェースのプロパティをJSONにJsonDerivedTypeを使ってシリアライズ、デシリアライズするを参照ください。

関数型でエンタープライズシステムを構築する方法について書かれた名著Domain Modeling Made Functionalを書いたScott Wlaschin氏も、「実行できない状態で関数が実行できてしまう場合は、エラーを返したり、ガードしたりしたりする必要もあるし、テストを書く必要もある。実行できる状態を型として定義して、必ず実行できる状態の時にしか実行できないようにすればテストの必要すらない。」というようなことをYoutubeで言っていました。

その型の推移を表現して、データはイミュータブルにレコード内で管理することによって、状態の推移を型が入力によって別の型に変化するという形でデータをモデリングすることができます。

検証を担当するコマンドオブジェクトとコマンドハンドラー

コマンド

ドメインモデルはここまでで完成しましたので、入力機能を作成します。こちらはコマンドオブジェクトとなります。このコンソールアプリではCQRSを実装するわけではないですが、実装の基礎として、ドメインにアクセスする前に予期しない情報が入ってこないような検証を行なっておく必要があります。

それで、以下のコマンドを定義して、コンソールアプリの入力 string[] からコマンドのデータを作成します。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Modeling/ラーメン購入コマンド.cs

コマンドで検証を行い、エラーを返していることがわかります。検証のポイントは、パラメータが2つで所持金、個数の両方の検証を行うときに、両方ともエラーがある場合、両方のエラーを最初から同時に表示できるように作っています。このように、検証はできれば一気に内容を検証して、起きているエラーを全て表示することにより、使いやすいシステムとなります。

コマンドハンドラー(Program.cs)

最後にコマンドを実行するコードですが、今回はProgram.csに記述しました。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402/Program.cs

ここで以下のような流れとしています。

  • ラーメンが買えるかをチェック
  • ラーメンが買えればAddingOrderに追加
  • 何個チャーハンが買えるかをチェック
  • チャーハンをAddingOrderに追加
  • 決済処理
  • レシートの表示

こちらの実行として
10000円の所持金でラーメン3杯頼んだら以下のようになります。

$ 202402 10000 3
所持金10000円で、濃厚MAXラーメンを3個購入希望ですね。
濃厚 MAX ラーメンを3個カートに追加しました。
合計金額は3,240円です。
残りの所持金で買えるだけ叉焼スペシャル炒飯を購入希望ですね。
叉焼スペシャル炒飯を6個カートに追加しました。
決済を実行します。
------決済完了------
濃厚 MAX ラーメン : 3点
叉焼スペシャル炒飯 : 6点
合計金額:9,120円
受領金  :10,000円
売り上げ:9,120円
おつり  :880円
ありがとうございました。

テストもできるだけ書く

合わせて、このアプリの面倒な点としては、コマンドラインからの検証があります。コマンドライン引数をたくさん書いて、それぞれどのように処理が成功するか、失敗するかを確認するのは大変です。

ドメインモデルと値オブジェクトはそれぞれ独立したデータモデルの連携の記述にフォーカスされていて、テストが書きやすくなっています。コマンドラインからの初期の実効に関しても、以下のようなコードを書くと、テストの時点で問題を把握することができます。

https://github.com/tomohisa/CSharpTokyoPlayground/blob/main/202402/202402Test/UnitTest1.cs

各アイテムの計算に関しても今回は簡単なのでテストは書きませんでしたが、実際の業務アプリの場合はドメインモデル内でのテストを書くことにより、バグの多くを潰すことができます。

ということでこの記事のアイコンはライオンにしてみました。

まとめ

このように、単純に計算を書けば簡単なものも、実際に管理を行うと複雑になります。値オブジェクトとドメインモデルを使って関数的に、代数的な形で問題を解決すると、それぞれのビジネスロジックが各クラスに紐づくようになるので、データの計算、変換などはデータとデータの変換関数を定義することによって定義することができるようになります。

私たちの開発しているイベントソーシング・CQRSフレームワーク Sekibanは、ドメインモデルを使った開発が行いやすい、イベントソーシングを使用しています。オープンソースで使用できますのでぜひ一度ご覧ください。

ジェイテックジャパンブログ

Discussion