実際のドメイン駆動設計

エヴァンスは良いアイデアで良い本を書きました。 しかし、これらのアイデアには方法論的根拠がありません。 経験豊富な開発者とアーキテクトは、顧客の対象領域にできるだけ近づける必要があること、顧客と話す必要があることを直感的に理解しています。 しかし、ユビキタス言語と顧客の実際の言語のコンプライアンスについてプロジェクトを評価する方法が明確ではありませんか? ドメインが境界付きコンテキストに正しく分割されていることをどのように理解するのですか? プロジェクトでDDDが使用されているかどうかを確認する方法



最後の点は特に重要です。 彼のスピーチの1つで、グレッグヤングはDDDを実践している人々の手を上げるように求めました。 そして、彼は、一連のパブリックゲッターとセッターでクラスを作成し、「サービス」と「ヘルパー」のロジックを持ち、それをDDDと呼ぶ人を除外するように要求しました。 ホールにくすくす笑いがありました:)



DDDスタイルでビジネスロジックを構成する方法 「動作」を保存する場所:サービス、エンティティ、拡張メソッド、またはどこでも少しですか? この記事では、サブジェクト領域の設計方法と使用するルールについて説明します。



すべての人がうそをつく



もちろん、具体的にはありません:)実際には、ビジネスアプリケーションは幅広いタスク用に作成され、さまざまなユーザーグループの関心を満たします。 せいぜい、最初から最後までビジネスプロセスを理解できるのはトップマネジメントだけです。 ちなみに、まれに誤解しないでください。 区画内では、一部のみが表示されます。 したがって、すべての利害関係者へのインタビューの結果は通常、矛盾のもつれになります。 この規則から次のことがわかります。



最初に、分析、次に設計、そしてそれから-開発



データベース構造またはクラスのセットではなく、ビジネスプロセスから開始する必要があります。 BPMNとUMLアクティビティをテストケースと組み合わせて使用​​します。 チャートは、規格に精通していない人でもよく読まれます。 表形式のテストケースは、境界ケースをより適切に識別し、矛盾を排除するのに役立ちます。



アブストラクトトークは時間の無駄です。 人々は詳細は重要ではなく、「すべてがすでに明確であるため、それらを議論する必要はまったくない」と確信しています。 テストケースの表に記入するリクエストは、オプションが実際には3ではなく26であることを明確に示しています(これは誇張ではなく、プロジェクトの1つでの分析の結果です)。



表とグラフは、ビジネス、分析、開発の間の主要なコミュニケーションツールです。 BPMNダイアグラムとテストケースのテーブルのコンパイルと並行して、 プロジェクトのシソーラスに用語を書き始めます。 辞書は、後でエンティティの設計に役立ちます。



コンテキストを選択



組織全体で単一の一貫した言語を使用するポリシーがトップ管理レベルで採用および実装されている場合にのみ、アプリケーション全体の単一のサブジェクトモデルを作成できます。 すなわち 営業部門が制作に「アカウント」と言うと、両者は同じ言葉を理解します。 これは同一のアカウントであり、「CRMアカウント」や「クライアントの法人」ではありません。



実生活では、私はこれを見たことがない。 したがって、対象モデルをすぐにいくつかの部分に大まかに「カット」することが望ましい。 接続が少ないほど良い。 しかし、通常、特定の一般用語のセットを見つけることが判明しています。 私はそれを主題領域の中核と呼んでいます。 コンテキストはカーネルに依存する場合があります。 コンテキスト間の依存関係を回避することを強くお勧めします。 潜在的に、このアプローチは核の「膨張」につながりますが、コンテキストの相互依存性は強力な接続性を生み出し、これは「厚い」核よりも悪いです。



建築





ポートとアダプタ、 タマネギアーキテクチャクリーンアーキテクチャ -これらのアプローチはすべて、 ドメインをアプリケーションのコアとして使用するという考え方に基づいています 。 エヴァンスは、「ドメイン」および「インフラストラクチャ」について話すときに、この問題に何気なく対処します。 ビジネスロジックは、「トランザクション」、「データベース」、「コントローラー」、「遅延負荷」などの概念では動作しません。 n層-アーキテクチャでは、これらの概念を広めることはできません。 要求はコントローラーに送られ、「ビジネスロジック」に転送され、「ビジネスロジック」はDAL



と対話します。 また、 DAL



は連続した「トランザクション」、「テーブル」、「ロック」などです。 Clean Architectureを使用すると、依存関係を反転し、ハエをカツレツから分離できます。 もちろん、実装の詳細を完全に無視することは不可能です。 とにかく、RDBMS、ORM、ネットワーキングはそれらの制限を課します。 ただし、 Clean Architecture



を使用する場合はClean Architecture



これを制御できます。 n-layer



では、最下層のストレージ構造により、「単一言語」 n-layer



固執することははるかに困難です。



Clean Architecture



は、 Bounded Context.



うまく機能しBounded Context.



異なるコンテキストは異なるサブシステムを表す場合があります。 単純なコンテキストは、単純なCRUD



実装するのが最適です。 非対称ロードコンテキストの場合、 CQRSが最適です 。 監査ログを必要とするサブシステムでは、イベントソーシングを使用するのが理にかなっています。 帯域幅と待機時間の制限がある読み取りと書き込みが読み込まれたサブシステムの場合、イベント駆動型のアプローチを検討するのは理にかなっています。 一見、これは不快に思えるかもしれません。 たとえば、 CRUD



サブシステムで作業し、 CQRS



サブシステムからタスクを受け取りました。 しばらくの間、これらすべてのCommand



Query



新しいゲートとして見る必要があります。 別の方法-システムを単一のスタイルで設計する-は近視眼的です。 アーキテクチャはツールのセットであり、各ツールは特定のタスクに適しています。



プロジェクト構造



.NETプロジェクトを次のように構成します。



 /App /ProjectName.Web.Public /ProjectName.Web.Admin /ProjectName.Web.SomeOtherStuff /Domain /ProjectName.Domain.Core /ProjectName.Domain.BoundedContext1 /ProjectName.Domain.BoundedContext1.Services /ProjectName.Domain.BoundedContext2 /ProjectName.Domain.BoundedContext2.Command /ProjectName.Domain.BoundedContext2.Query /ProjectName.Domain.BoundedContext3 /Data /ProjectName.Data /Libs /Problem1Resolver /Problem2Resolver
      
      





Libs



フォルダーのプロジェクトはドメインに依存しません。 レポート、csv解析、キャッシングメカニズムなど、ローカルの問題のみを解決します。 ドメイン構造はBoundedContext'



対応しBoundedContext'



Domain



フォルダーのプロジェクトはData



から独立しています。 Data



DbContext



、移行、DALに関連する構成があります。 Data



は、移行を構築するためのDomain



エンティティに依存します。 App



フォルダーのプロジェクトは、IOCコンテナーを使用して依存関係を注入します。 したがって、ドメインコードをインフラストラクチャから最大限に分離することが可能です。



エンティティのモデリング



エンティティとは、一意の識別子を持つドメインのオブジェクトを意味します。 たとえば、特定の部門で認定を取得するという文脈でロシアの会社を説明するクラスを取り上げます。



 [DisplayName("  ()")] public class Company : LongIdBase , IHasState<CompanyState> { public static class Specs { public static Spec<Supplier> ByInnAndKpp(string inn, string kpp) => new Spec<Supplier>(x => x.Inn == inn && x.Kpp == kpp); public static Spec<Supplier> ByInn(string inn) => new Spec<Supplier>(x => x.Inn == inn); } //  EF protected Company () { } public Company (string inn, string kpp) { DangerouslyChangeInnAndKpp(inn, kpp); } public void DangerouslyChangeInnAndKpp(string inn, string kpp) { Inn = inn.NullIfEmpty() ?? throw new ArgumentNullException(nameof(inn)); Kpp = kpp.NullIfEmpty() ?? throw new ArgumentNullException(nameof(kpp)); this.ValidateProperties(); } [Display(Name = "")] [Required] [DisplayFormat(ConvertEmptyStringToNull = true)] [Inn] public string Inn { get; protected set; } [Display(Name = "")] [DisplayFormat(ConvertEmptyStringToNull = true)] [Kpp] public string Kpp { get; protected set; } [Display(Name = " ")] public CompanyState State { get; protected set; } [DisplayFormat(ConvertEmptyStringToNull = true)] public string Comment { get; protected set; } [Display(Name = "  ")] public DateTime? StateChangeDate { get; protected set; } public void Accept() { StateChangeDate = DateTime.UtcNow; State = AccreditationState.Accredited; } public void Decline(string comment) { StateChangeDate = DateTime.UtcNow; State = AccreditationState.Declined; Comment = comment.NullIfEmpty() ?? throw new ArgumentNullException(nameof(comment)); }
      
      





適切な集約と関係を選択するには、多くの場合、1回の反復だけでは不十分です。 最初に、クラスの基本構造をマップし、1対1、1対多、および多対多の関係を定義し、データ構造を説明します。 次に、ビジネスプロセスごとに構造をトレースし、BMPNとテストケースで確認します。 いくつかのケースが構造に適合しない場合、設計中に間違いが発生し、構造を変更する必要があります。 結果として生じる構造は、ダイアグラムの形式で配置でき、さらに主題分野の専門家と合意できます。



専門家は、設計エラーや不正確さを指摘する場合があります。 その過程で、一部のエンティティには適切な用語がないことが判明する場合があります。 その後、オプションを提案し、しばらくしてから正しいものを見つけます。 tezarusに新しい用語が導入されました。 用語を一緒に話し合って合意することが非常に重要です。 これにより、将来の誤解の問題の大きな層が排除されます。



一意の識別子を選択してください



幸いなことに、エヴァンスはこの点に関して明確な推奨事項を提示します。まず、サブジェクトエリアでTIN、PPC、パスポートデータなどの識別子を探します。 見つかった場合は、それを使用します。 見つかりませんGUID



またはデータベース生成Id



依存します。 ドメインIDが存在する場合でも、ドメインID以外のIDを使用することをお勧めします。 たとえば、エンティティが汎用性があり、システムが以前のすべてのバージョンを保存する必要がある場合や、オブジェクトモデルの識別子が複雑なコンポジットであり、永続性に対応していない場合などです。



本物のデザイナー



ORMオブジェクトを具体化するには、反射が最もよく使用されます。 EFは保護されたコンストラクターに到達できますが、プログラマーは到達できません。 彼らは正しい法律を作成する必要があります。 TINおよびKPPによって識別される人物。 デザイナーにはガードが装備されています。 間違ったオブジェクトを作成しても、うまくいきません。 拡張メソッドValidateProperties



DataAnnotation



属性のValidateProperties



呼び出し、 NullIfEmpty



は空の行の送信をNullIfEmpty



ます。



 public static class Extensions { public static void ValidateProperties(this object obj) { var context = new ValidationContext(obj); Validator.ValidateObject(obj, context, true); } public static string NullIfEmpty(this string str) => string.IsNullOrEmpty(str) ? null : str; }
      
      





TINを検証するために、次の形式の属性が特別に記述されています。



 public class InnAttribute : RegularExpressionAttribute { public InnAttribute() : base(@"^(\d{10}|\d{12})$") { ErrorMessage = "     10/12 ."; } public InnAttribute(CivilLawSubject civilLawSubject) : base(civilLawSubject == CivilLawSubject.Individual ? @"^\d{12}$" : @"^\d{10}$") { ErrorMessage = civilLawSubject == CivilLawSubject.Individual ? "       12 ." : "       10 ."; } }
      
      





パラメーターのないコンストラクターは、ORMにのみ使用されるように保護されていると宣言されています。 反射は実体化に使用されるため、アクセス修飾子は邪魔になりません。 両方の必要なフィールドが「実際の」コンストラクター(TINおよびKPP)に転送されます。 システムのコンテキストにおける法人の残りのフィールドはオプションであり、会社の担当者が後で記入します。



カプセル化と検証



プロパティTINおよびPPCは、protected-setterで宣言されます。 EFは再びそれらにアクセスできるようになり、プログラマはDangerouslyChangeInnAndKpp



関数を使用する必要があります。 関数の名前は、TINとチェックポイントの変更が通常の状況ではないことを明確に示唆しています。 2つのパラメーターが関数に渡されます。つまり、TINとPPCを変更すると、一緒にのみ変更されます。 TIN + PPCを複合キーにすることもできます。 しかし、互換性のために、 long Id



を残しました。 最後に、この関数が呼び出されると、バリデーターが機能し、TINとPPCが正しくない場合、 ValidationException



がスローされます。

型システムはさらに強化できます。 ただし、参照で説明されているアプローチには、標準のASP.NETインフラストラクチャからのサポートがないという重大な欠点があります。 サポートを追加することもできますが、このようなインフラストラクチャコードには価値があり、付随する必要があります。


読み取り用のプロパティ、変更するための特別な方法



ビジネスプロセスに応じて、組織は「承認」または「拒否」される可能性があり、拒否の場合はコメントを残す必要があります。 すべてのプロパティがパブリックである場合、ドキュメントからのみこれについて知ることができます。 この場合、ステータス変更ルールはメソッドシグネチャから見ることができます。 記事では、法人クラスの断片のみを引用しました。 実際、より多くのフィールドがあり、特に新しいチームメンバーを接続する場合は、何が関連しているのかを理解することは非常に役立ちます。 明示的なビジネスオペレーションを行わずに、プロパティを他から隔離して制御不能に変更できる場合は、セッターを公開することもできます。 ただし、このプロパティは警告する必要があります。データに関連する明示的な操作がない場合、おそらくこのデータは不要ですか?

別の方法は、「 状態 」パターンを使用して、動作を別のクラスに配置することです。


仕様書



しばらくの間、 Queryable



変更したり、式ツリーをいじったりする拡張機能を作成する方が良いことは明確ではありませんでした。 最終的に、 LinqSpecs実装最も便利であることが判明しました。



拡張メソッド



インターフェイスのアドホックポリモーフィズム(後継者ごとにメソッドを実装する必要がないように) は、遅かれ早かれC#に表示されます。 これまでのところ、拡張メソッドに満足する必要があります。



  public interface IHasId { object Id { get; } } public interface IHasId<out TKey> : IHasId where TKey: IEquatable<TKey> { new TKey Id { get; } } public static bool IsNew<TKey>(this IHasId<TKey> obj) where TKey : IEquatable<TKey> { return obj.Id == null || obj.Id.Equals(default(TKey)); }
      
      





拡張メソッドは、表現力を高めるためにLINQでの使用に適しています。 ただし、 ByInnAndKpp



およびByInn



は、他の式の内部では使用できません。 プロバイダーを解析することはできません。 拡張メソッドの使用の詳細については、 DSL



がDino EspositoにDotNextの1つについて語っています。



 public static class CompanyDataExtensions { public static CompanyData ByInnAndKpp( this IQueryable<CompanyData> query, string inn, string kpp) => query .Where(x => x.Company, Supplier.Specs.ByInnAndKpp(inn, kpp)) .FirstOrDefault(); public static CompanyData ByInn( this IQueryable<CompanyData> query, string inn) => query .Where(x => x.Company, Supplier.Specs.ByInn(inn)); }
      
      





2つのパラメーターを持つ異常なWhereに注意してくださいEF Core



現在InvokeExpression



サポートしていInvokeExpression



。 アプリケーションでは、次のようにコードが使用されます。



 var priceInfos = DbContext .CompanyData .ByInn("") .ToList();
      
      





別の方法は、 SelectMany



を使用することです。



 var priceInfos = DbContext .Company //     extension-    .ByInnAndKpp("", "") .SelectMany(x => x.Company) .ToList();
      
      





IQueryProvider



の観点からのSelect



SelectMany



オプションの等価性の問題は、まだ完全には研究していません。 コメントでこのトピックに関する情報をいただければ幸いです。


関連コレクション



 public virtual ICollection<Document> Documents { get; protected set; }
      
      





company.Documents.Where(…).ToList()



の形式のコードはデータベースへのクエリを作成せず、最初に関連するすべてのエンティティをRAMに適用するため、 Select



ブロックでのみ使用してSQLクエリに変換することをお勧めしますメモリ内のサンプリング。 したがって、モデル内のコレクションの存在は、アプリケーションのパフォーマンスに悪影響を与える可能性があります。 同時に、必要なIQueryable



を外部から転送する必要があるため、リファクタリングは困難です。 リクエストの品質を制御するには、 miniProfilerを調べる必要があります。



サービス



貧弱モデルでは、一般に、すべてのロジックはサービスに保存されます。 ロジックが集約コードで不適切であるか、集約間の相互作用を記述している場合、必要な場合にのみサービスを追加することを好みます。 最適なオプションは、ドメインにサービスの正確な名前(「レジ」、「倉庫」、「コールセンター」)が含まれている場合です。 この場合、接尾辞「Service」は省略できます。 各クラスのメソッドのセットは、ユーザーインターフェイス要素でグループ化されたユースケースのセットに対応しています。 インターフェイスがTask Based UI



スタイルで設計されている場合、うまく機能します。



書き込みメソッドは、エンティティまたはDTOを入力として受け入れます。 リクエストは、メソッドが実行される前に厳密に別のレイヤーで検証されます。 メソッドが失敗する可能性がある場合は、 Result



タイプを使用して署名で明示的に指定する必要があります。 例外的な状況については例外が残っています。



読み取りメソッドは、シリアル化してクライアントに送信するDTOを返します。 AutoMapperMapsterの Queryable Extensions



おかげで、マッピングを使用してSelect



式に変換することができます。これにより、データベース全体からエンティティ全体をドラッグできなくなります。



マネージャー



同じユニット内での操作にはほとんど使用しません。 たとえば、 AspNet.Identity



にはUserManager



が含まれています。 基本的に、管理者は、ドメインに直接関連していない集約にロジックを実装する必要がある場合に必要です。



ユニオン型のTPT



場合によっては、1つのエンティティが他のいくつかのエンティティの1つに関連付けられることがあります。 一貫したストレージシステムを作成するには、 TPTを使用し、制御フローにはパターンマッチングを使用できます。 このアプローチについては、 別の記事で詳しく説明しています



DTOのプロジェクションのクエリ可能な拡張機能



DataMapper



を使用するとボイラープレートコードの数が減り、 Queryable Extensions



使用すると、 Select



手動で記述することなくDTO要求を作成できます。 したがって、RAMでのマッピングおよびIQueryProvider



式ツリーの構築に式を再利用できます。 AutoMapper



メモリ内AutoMapper



非常に貪欲で高速ではないため、時間が経つにつれてMapsterに置き換えられました。



個々のサブシステムのCQRS



不確実性の高い条件で作業する場合、設計エラーのリスクも大きくなります。 データベース構造を設計したり、非正規化について決定したり、ストアドプロシージャを記述したりする前に、迅速なプロトタイピングと仮説のテストに頼ることは理にかなっています。 自信がある場合:入力と出力に何があるかを最適化できます。



実装コマンドがない場合、 IQuery



は同じ入力で同じ結果を返します。 したがって、このようなメソッドの本体は積極的にキャッシュできます。 したがって、実装を置き換えた後、インフラストラクチャコード(コントローラー)は変更されず、 IQuery



メソッドの本体のみを変更する必要があります。 このアプローチにより、アプリケーションをすべてではなく、小さな部分で個別に最適化できます。

このアプローチは、IOCコンテナのオーバーヘッドとリクエストごとのライフスタイルのメモリトラフィックのために、非常に忙しいリソースに適用できません。 ただし、データベースの依存関係をコンストラクターに注入せず、代わりにusing構文を使用する場合、すべてのIQuery



をシングルトンにすることができます。


レガシーコードを使用する



既存のコードベースを使用する場合、作業の形式(「サポート」または「開発」)を決定する必要があります。 最初のケースでは、新しい機能の出現とシステムの改良は期待されていません。 最大は、いくつかの新しいレポートを追加することです、いくつかのフォームはあちこちにあります。 第二に-主題モデルおよび/またはアーキテクチャ全体の重要な処理が必要です。 プロジェクトを「開発」ではなく「サポート」する必要がある場合、それらがどれほど成功しているかに関係なく、既存のルールに従うことをお勧めします。 あなたがあなたの前に率直なgovnokodを持っているならば、それを台無しにするという申し出を拒否するほうがよいです。



プロジェクト開発はより複雑なタスクです。 リファクタリングのトピックは、この記事の範囲外です。 「 腐敗防止レイヤー 」と「 ストラングラー 」という2つの最も有用なパターンのみに注目します。 それらは非常に似ています。 主なアイデアは、古いコードベースと新しいコードベースの間に「ファサード」を構築することであり、徐々にシステム全体を断片的書き換える象がいます。 ファサードは、古いコードベースの問題が新しいコードベースに浸透することを許可せず、古いビジネスロジックを新しいビジネスロジックに確実にマッピングする障壁の役割を果たします。 ファサードが完全にハッキング、トリック、松葉杖で構成され、遅かれ早かれ、古いコードベース全体とともに忘却に沈むことに注意してください。



All Articles