![](https://habrastorage.org/webt/mc/xv/xx/mcxvxx5zbueqfamfftgldnyupx4.png)
最後の点は特に重要です。 彼のスピーチの1つで、グレッグヤングはDDDを実践している人々の手を上げるように求めました。 そして、彼は、一連のパブリックゲッターとセッターでクラスを作成し、「サービス」と「ヘルパー」のロジックを持ち、それをDDDと呼ぶ人を除外するように要求しました。 ホールにくすくす笑いがありました:)
DDDスタイルでビジネスロジックを構成する方法 「動作」を保存する場所:サービス、エンティティ、拡張メソッド、またはどこでも少しですか? この記事では、サブジェクト領域の設計方法と使用するルールについて説明します。
すべての人がうそをつく
![](https://habrastorage.org/webt/6c/fh/ty/6cfhty-ssjpgtzcz2xl-eaadua4.png)
最初に、分析、次に設計、そしてそれから-開発
![](https://habrastorage.org/files/2b1/eea/d27/2b1eead27a6a45b4927002fb06797ec3.jpg)
アブストラクトトークは時間の無駄です。 人々は詳細は重要ではなく、「すべてがすでに明確であるため、それらを議論する必要はまったくない」と確信しています。 テストケースの表に記入するリクエストは、オプションが実際には3ではなく26であることを明確に示しています(これは誇張ではなく、プロジェクトの1つでの分析の結果です)。
表とグラフは、ビジネス、分析、開発の間の主要なコミュニケーションツールです。 BPMNダイアグラムとテストケースのテーブルのコンパイルと並行して、 プロジェクトのシソーラスに用語を書き始めます。 辞書は、後でエンティティの設計に役立ちます。
コンテキストを選択
![](https://habrastorage.org/webt/ph/b0/1a/phb01a61skk0baii2mr9udyxtvy.png)
実生活では、私はこれを見たことがない。 したがって、対象モデルをすぐにいくつかの部分に大まかに「カット」することが望ましい。 接続が少ないほど良い。 しかし、通常、特定の一般用語のセットを見つけることが判明しています。 私はそれを主題領域の中核と呼んでいます。 コンテキストはカーネルに依存する場合があります。 コンテキスト間の依存関係を回避することを強くお勧めします。 潜在的に、このアプローチは核の「膨張」につながりますが、コンテキストの相互依存性は強力な接続性を生み出し、これは「厚い」核よりも悪いです。
建築
![](https://habrastorage.org/web/bb3/430/769/bb3430769e6c4e8fbca757a8bc071f2e.png)
ポートとアダプタ、 タマネギアーキテクチャ 、 クリーンアーキテクチャ -これらのアプローチはすべて、 ドメインをアプリケーションのコアとして使用するという考え方に基づいています 。 エヴァンスは、「ドメイン」および「インフラストラクチャ」について話すときに、この問題に何気なく対処します。 ビジネスロジックは、「トランザクション」、「データベース」、「コントローラー」、「遅延負荷」などの概念では動作しません。 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を返します。 AutoMapperとMapsterの
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つの最も有用なパターンのみに注目します。 それらは非常に似ています。 主なアイデアは、古いコードベースと新しいコードベースの間に「ファサード」を構築する