NDDD

ドメイン駆動開発_フォルダー構成編_# 05_Domainのフォルダー構成

前回までで,各種プロジェクトの作成と,プロジェクトそれぞれの意味を説明してきましたが,ここからは,プロジェクトの中に作成するフォルダーについて解説していきます。一つのプロジェクトの中には,直接クラスファイルを置くのではなく,意味のあるフォルダーを作成し,意味のある単位でクラスを分けていかないと,どこに何が置いてあるか,判らなくなります。そのため,このフォルダー構成というのは,ファイルを整理するうえで,非常に重要な部分になります。それでは,どんなフォルダーがあるかを説明していきます。

Domainのフォルダー構成

まず,フォルダー構成としては「Entities」「Repositories」「ValueObjects」というフォルダーがドメイン駆動開発においてはメインのフォルダーになります。それでは1つずつ説明していきます。

Entitiesフォルダー

一意なデータの塊,データベースの1行,行のなかで完結するロジックの置き場所 

Entitiesフォルダーには「Entity」と呼ばれるクラスを配置していきます。このEntityは,データベースでデータを取得してきたときの「行」に相当するものをよく使います。1行ずつが,一意の値として扱います。行の中の列に対するビジネスロジックがあればここに記述します。

ValueObjects フォルダー

値として扱うクラス。数値の「3」等はそのまま扱うとビジネスロジックが散らばるため,ValueObjectの「3」として扱う。データベースの列のValueObject化が有効。 

ドメイン駆動開発ではValueObjectというクラスを大量に作ります。ValueObjectとは,値を表現するためのクラスです。例えば数値の「3」という値があったとします。仕様上,この値が3の時のふるまいや,2の時のふるまいというものがありますが,int型でこの値を表現してしまうと,それらのふるまいである「ビジネスロジック」を記述する場所が存在しません。なので,基本的に,ドメイン駆動開発では,値はすべてクラスで表現します。だから,単純に「3」という値を処理する際も,意味のあるValueObjectに変換して処理をしていきます。このフォルダーはそれらのValueObjectを配置します。

Repositories フォルダー

アプリケーションの外部と接触する部分にはすべてインタフェースを作る。テスト容易性。 

ここにはRepositoryというものを配置します。Repositoryは基本的にはインタフェースで表現しますが,具体的なクラスになる場合もあります。Repositoryは外部と接触するInfrastructureの処理の定義をインタフェースに記述したものです。それらをここに配置します。インタフェースをInfrastructureのプロジェクトではなく,Domain層に配置するには理由があります。Infrastructureにインタフェースと,実装を配置すると,Infrastructureを参照しているプロジェクトからしか,処理を呼び出せなくなります。今回の場合であればInfrastructure自身か,参照しているWinFormプロジェクトからしか呼び出すことができません。インタフェースをDomain層におけば,実装はInfrastructureにまとめて,外部接触部分を局所的にしながら,Domain層でインタフェースに対して,処理を実行できるので,Domain層からも,データ取得などの呼び出しが可能となります。呼び出しが可能なのに,外部のテクノロジーには一切依存しないという造りになります。

あと,外部接触部分にインタフェースが必要なのは,テスト容易性も兼ねています。外部に接触する部分は,通常,外部の機器がないと実行エラーになりますが,その部分をインタフェースにすることで,ダミーのデータに切り替えたり,テストコードからMockと呼ばれる,テストデータに切り替えたりすることが可能です。なので,外部のテクノロジーと接触する場所は,すべてRepositoryという名のインタフェースを作成すると思っておいてください。

以上,Entities,ValueObjects ,Repositoriesの3つがドメイン駆動開発の肝になります。基本的にはこの3つのフォルダーにビジネスロジックを集めていきます。ただ,この3つのパターンだけでは,ロジックをすべて表現することはできないので,それ以外のクラスの置き場所であるフォルダーも作成していきます。

Exceptionsフォルダー

Exceptionを継承した例外クラスの集まり。基本的に,異常は例外で表す。戻り値をboolにして判断しない,呼び元にifを書かせない。値が戻ったら正常。エラー時はcatch! 

Exceptionsフォルダーには,独自で作成する例外クラスを入れます。マイクロソフトの標準に「Exception」というクラスがありますが,そのクラスを継承して作成します。

エラーは例外で表現する

基本的にエラーは例外クラスを通知することで表現します。昔はbool型や数値型でエラーを表現していることもありましたが,それだと関数の戻り値でエラー内容をバケツリレーのように呼び出し元まで戻さないといけません。関数を4つや5つ呼び出した先でエラーになったら,すべての関数の戻り値で「マイナス1」を返すなどとやっていては,いろいろと不都合が出ます。

まず,戻り値というのは,処理が成功した際に戻るデータを戻すべきです。GetDateTime()というメソッドは日付を返すべきです。Nullならエラーとか,マイナス1ならエラーとか,falseならエラーとかを,戻り値にする場合は,エラーの戻り値と,正常の時の戻り値の2つを返さないといけません。その場合,「refキーワード」の引数等で戻すなど,戻り値以外の値で戻さないといけません。

さらに,戻り値や引数で値を戻すと,その値を「見ていない」という問題が起こります。戻り値で「マイナス1」を返却したのに,呼び元でチェックしなければ,意味がありません。コーディングをする際に,注意しないと,エラーなのに,正常時の処理を通してしまう可能性があります。

例外を使用すると,そのあたりの不都合が解消されます。関数を4つ呼んだ先でエラーになっても,エラーになった箇所で例外を通知すれば,呼び元まで通知されます。通知された呼び元は,「catch」構文に落ちてくるので,エラーチェック漏れや,エラー時に正常パターンに流れ込む心配がありません。さらに4つの関数をバケツリレーでエラーコードを返す必要もありません。

よって,可能な限り,エラーは例外で通知し,呼び出し元では「catch」するようにします。可能な限り,というのは,一つ例外には欠点があります。それはパフォーマンスが悪いという事です。エラー時に1回エラー通知する程度なら,問題ありませんが,for文等,連続でエラー通知されるような場合は,例外以外のエラー処理を検討する必要もあります。そこはパフォーマンスとトレードオフになります。

値を返す関数と例外の基本思想

値を返す関数の基本的な考え方は,値が帰ってきたら「正常!」,エラー時は「例外が通知される!」という考え方で実装していきましょう。そして,エラーになった時点で例外を通知して,呼び出し元まで返す。途中の関数でエラー時にやることがある場合,例えばファイルを閉じる等がある場合はその場で「catch」して「throw」することで,引き続き呼び元まで返しましょう。これが基本思想です。仕様によっては合わないケースもあるかもしれませんが,その場合はそれに合わせればいいのですが,基本思想として覚えてください。

Helpersフォルダー

staticでどこにあってもいいもの。害がないもの。考え方としてはマイクロソフトが作ってくれていたら不要だったよ!的なもの。 

ここにはstaticな独立したロジックを入れていきます。マイクロソフトには色々なクラスがありますが,マイクロソフトが提供してくれていたら別にいらなかったけど,無いから作ろう。みたいな関数を入れます。例えば小数点以下を2桁で切り捨てて,ゼロで埋める(1.2なら1.20にする)みたいな処理が欲しければ,FloatHelperみたいな感じで,Float型に対して,行う処理を書きます。Stringに対して補助的な処理を書く場合は「StringHelper」などです。現状は拡張メソッドというのが,書けるようになっているので,拡張メソッドを使って表現してもOKです。

StaticValues

値をキャッシュとして保持する場合

アプリケーションを作成していると,どうしてもメモリ上に値を置いておきたい場合があります。例えばマスターデータ(商品マスターなど頻繁に変更されないデータ)などは,毎回データベースアクセスせず,メモリ上に置いて使いまわしたいと思います。手を伸ばせばだれでも手の届くところにあって,特に害がない,というデータのキャッシュ領域を作る場合はこのStaticValuesに入れます。ここではどこのテーブルの値が保持しているか?がわかりやすい名前のクラスで作成していきます。

Logics

Staticでビジネスロジックを含むもの。診断ロジック等

基本的にビジネスロジックはEntitiesやValueObjectsに入れる方向で実装していきますが,どこにも属さないような,独立したビジネスロジックというのが存在します。医療系のアプリであれば,計測した血圧や体温などから,何かの診断をする場合は,「血圧」「温度」を引数にして,staticな独立した診断ロジックを作りたくなります。こういったビジネスロジックでありながら,値とは切り離して,ロジックのみでクラスを構成したい場合はこのLogicsフォルダーに入れます。ちなみに,HelpersとLogicsの違いは,ビジネスロジックを含むかどうかという違いです。医療系アプリでは,医療系の仕様に関係するものはLogicsに入れますが,小数点位置で切り捨て,といったような,どのアプリケーションでも共通で使えそうなものはHelpersに入れます。

以上がDomainの中にあるフォルダー構成になります。

これらの構成は私が,ドメイン駆動開発や,オブジェクト指向を学んで,現場でいくつもの製品を開発するうえで,最終的に最適化したものなので,強制するものではありません。より良いと思うものがあれば,改良してください。

NDDD

Udemyで5400円で公開しているC#のコースを1つ無料でプレゼントしています。
こちらから確認してください。

#01_プロジェクトの作成
#02_プロジェクトの追加
#03_依存関係
#04_ドメイン駆動開発でApplication層は必要?
#05_Domainのフォルダー構成
#06_Infrastructureのフォルダー構成
#07_WinFormのフォルダー構成
#08_Testsのフォルダー構成
#09_テスト駆動で実装するための事前準備
#10_テストコードとViewModelの追加
#11_テストコードを追加する
#12_ Repositoriesフォルダーの作成
#13_ Entitiesフォルダーの作成
#14_ Mockの作成
#15_フォーム画面の作成
#16_画面のコントロールデータバインドする
#17_Fakeを使ってタミーデータを画面に表示させる
#18_Fakeデータを画面に通知する
#19_PropertyChangedの方法を変更する
#20_Fakeとデータベースの値を切り替える方法
#21_Sharedクラスを作成する
#22_クラスを生成するファクトリークラスを作る
#23_#if DEBUGでFakeデータがリリースされないようにする
#24_DEBUGモードであることをわかりやすくしておく
#25_Factories以外から生成できないようにしておく
#26_Factoriesの呼び出しはViewModelで行う
#27_外部の設定ファイルの値で判断する
#28_Fakeデータを切り替える方法
#29_FakePathを設定ファイルとSharedに移す
#30_Fakeデータのバリエーション
#31_Shareクラスの活用方法
#32_ベースフォームを作る
#33_SharedにログインIDを記憶する
#34_BaseFormでログインユーザーを表示する
#35_ValueObject
#36_ValueObjectを作成する
#37_抽象クラスValueObjectを使用してイコールの問題の解消
#38_AreaIdにビジネスロジックを入れる
#39_AreaIdクラスをEntityに乗せる
#40_MeasureDateの作成
#41_MeasureValueの作成
#42_オブジェクト指向の自動化
#43_Repositoryの具象クラス
#44_例外処理
#45_例外の作成
#46_インナーエクセプション
#47_例外の欠点
#48_メッセージの区分
#49_エラー処理の共通化
#50_ログの出力
#51_タイマー処理はどこに置く?
#52_タイマークラスの作成
#53_StaticValues
#54_Logics
#55_Helpers
#56_Module
#57_トランザクションはどこでかける?
#58_特徴を見極める
#59_さいごに

Udemyで5400円で公開しているC#のコースを1つ無料でプレゼントしています。
こちらから確認してください。