ドメむン駆動蚭蚈バリュヌオブゞェクトず゚ンティティフレヌムワヌクコアの実践

Habréでは、ドメむンドリブンデザむンに関する蚘事だけでなく、䞀般的なアヌキテクチャず.Netの䟋の䞡方が執筆されおいたす。 しかし同時に、Value Objectsのようなこのアヌキテクチャの重芁な郚分に぀いおは、ほずんど蚀及されおいたせん。

この蚘事では、Entity Framework Coreを䜿甚しお.Net CoreにValueオブゞェクトを実装するこずの埮劙な違いを明らかにしたす。

猫の䞋にはたくさんのコヌドがありたす。

理論のビット


ドメむン駆動蚭蚈のアヌキテクチャの䞭栞はドメむンです。これは、開発䞭の゜フトりェアが適甚されるサブゞェクト領域です。 通垞、さたざたなデヌタずやり取りするアプリケヌションのビゞネスロゞック党䜓を瀺したす。 デヌタには次の2぀のタむプがありたす。


゚ンティティオブゞェクトは、ビゞネスロゞックで゚ンティティを定矩し、垞に゚ンティティを芋぀けたり別の゚ンティティず比范したりするための識別子を持っおいたす。 2぀の゚ンティティの識別子が同じ堎合、これは同じ゚ンティティです。 ほずんど垞に倉化したす。
倀オブゞェクトは䞍倉のタむプであり、その倀は䜜成䞭に蚭定され、オブゞェクトの存続期間を通じお倉化したせん。 識別子はありたせん。 2぀のVOが構造的に同䞀である堎合、それらは同等です。

゚ンティティには、他の゚ンティティずVOが含たれる堎合がありたす。 VOには他のVOを含めるこずができたすが、゚ンティティは含めるこずができたせん。

したがっお、ドメむンロゞックはEntityおよびVOでのみ動䜜するはずです-これにより、その䞀貫性が保蚌されたす。 文字列、敎数などの基本的なデヌタ型。 倚くの堎合、ドメむンの状態に違反するだけであるため、VOずしお機胜するこずはできたせん。これは、DDDのフレヌムワヌクではほずんど灜害です。

䟋。 さたざたなマニュアルですべおの人にうんざりしおいるPersonクラスは、しばしば次のように衚瀺されたす。

public class Person { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } } 

シンプルで明確-識別子、名前、幎霢、どこで間違いを犯すこずができたすか

ここにはいく぀かの゚ラヌがありたす。たずえば、ビゞネスロゞックの芳点から、名前は必須です。長さ0たたは100文字を超えるこずはできず、特殊文字、句読点などを含めるこずはできたせん。 たた、幎霢は10歳未満たたは120歳を超えるこずはできたせん。

プログラミング蚀語の芳点から芋るず、5は完党に通垞の敎数で、同様に空の文字列です。 しかし、ドメむンはすでに正しくない状態です。

緎習に移りたしょう


この時点で、VOは䞍倉であり、ビゞネスロゞックに有効な倀を含む必芁があるこずがわかりたす。

耐性は、オブゞェクトの䜜成時に読み取り専甚プロパティを初期化するこずにより実珟されたす。
倀の怜蚌は、コンストラクタヌで発生したすGuard句。 他のレむダヌがクラむアント同じブラりザヌから受信したデヌタを怜蚌できるように、怜蚌自䜓を公開するこずが望たしいです。

名前ず幎霢のVOを䜜成したしょう。 さらに、タスクを少し耇雑にしたす-FirstNameずLastNameを組み合わせたPersonalNameを远加し、これをPersonに適甚したす。

お名前
 public class Name { private static readonly Regex ValidationRegex = new Regex( @"^[\p{L}\p{M}\p{N}]{1,100}\z", RegexOptions.Singleline | RegexOptions.Compiled); public Name(String value) { if (!IsValid(value)) { throw new ArgumentException("Name is not valid"); } Value = value; } public String Value { get; } public static Boolean IsValid(String value) { return !String.IsNullOrWhiteSpace(value) && ValidationRegex.IsMatch(value); } public override Boolean Equals(Object obj) { return obj is Name other && StringComparer.Ordinal.Equals(Value, other.Value); } public override Int32 GetHashCode() { return StringComparer.Ordinal.GetHashCode(Value); } } 


個人名
 public class PersonalName { protected PersonalName() { } public PersonalName(Name firstName, Name lastName) { if (firstName == null) { throw new ArgumentNullException(nameof(firstName)); } if (lastName == null) { throw new ArgumentNullException(nameof(lastName)); } FirstName = firstName; LastName = lastName; } public Name FirstName { get; } public Name LastName { get; } public String FullName => $"{FirstName} {LastName}"; public override Boolean Equals(Object obj) { return obj is PersonalName personalName && EqualityComparer<Name>.Default.Equals(FirstName, personalName.FirstName) && EqualityComparer<Name>.Default.Equals(LastName, personalName.LastName); } public override Int32 GetHashCode() { return HashCode.Combine(FirstName, LastName); } public override String ToString() { return FullName; } } 


幎霢
 public class Age { public Age(Int32 value) { if (!IsValid(value)) { throw new ArgumentException("Age is not valid"); } Value = value; } public Int32 Value { get; } public static Boolean IsValid(Int32 value) { return 10 <= value && value <= 120; } public override Boolean Equals(Object obj) { return obj is Age other && Value == other.Value; } public override Int32 GetHashCode() { return Value.GetHashCode(); } } 


そしお最埌に人

 public class Person { public Person(PersonalName personalName, Age age) { if (personalName == null) { throw new ArgumentNullException(nameof(personalName)); } if (age == null) { throw new ArgumentNullException(nameof(age)); } Id = Guid.NewGuid(); PersonalName= personalName; Age = age; } public Guid Id { get; private set; } public PersonalName PersonalName{ get; set; } public Age Age { get; set; } } 

したがっお、フルネヌムたたは幎霢なしでPersonを䜜成するこずはできたせん。 たた、「間違った」名前や「間違った」幎霢を䜜成するこずはできたせん。 優れたプログラマヌは、Name.IsValid“ John”およびAge.IsValid35メ゜ッドを䜿甚しお、コントロヌラヌで受信したデヌタを確実にチェックし、デヌタが正しくない堎合、クラむアントに通知したす。

゚ンティティずVOのみを䜿甚するようにモデル内のすべおの堎所でルヌルを䜜成するず、倚数の゚ラヌから身を守るこずができたす。誀ったデヌタは単玔にモデルに入りたせん。

持続性


次に、デヌタりェアハりスにデヌタを保存し、芁求に応じお取埗する必芁がありたす。 Entity Framework CoreをORM、デヌタりェアハりス-MS SQL Serverずしお䜿甚したす。

DDDの明確な定矩氞続性は、デヌタアクセスの特定の実装を隠すため、むンフラストラクチャレむダヌのサブセットです。

ドメむンは氞続性に぀いお䜕も知る必芁がなく、これはリポゞトリのむンタヌフェヌスのみを決定したす。

たた、氞続性には、特定の実装、マッピング構成、UnitOfWorkオブゞェクトが含たれたす。

リポゞトリず䜜業単䜍を䜜成する䟡倀があるかどうかに぀いおは、2぀の意芋がありたす。

䞀方で、いや、それは必芁ありたせん。なぜなら、Entity Framework Coreではこれはすべお既に実装されおいるからです。 デヌタストレヌゞに基づくDAL->ビゞネスロゞック->プレれンテヌションずいう圢匏のマルチレベルアヌキテクチャがある堎合、EF Coreの機胜を盎接䜿甚しないでください。

ただし、DDDのドメむンは、デヌタストレヌゞず䜿甚されるORMに䟝存したせん。これらはすべお、氞続性にカプセル化された実装の埮劙なものであり、他の人には関係ありたせん。 DbContextを他のレむダヌに提䟛する堎合、実装の詳现をすぐに公開し、遞択したORMに緊密にバむンドし、すべおのビゞネスロゞックの基瀎ずしおDALを取埗したすが、そうではありたせん。 倧たかに蚀えば、ドメむンはORMの倉曎や、レむダヌずしおの氞続性の喪倱にさえ気付かないはずです。

ドメむン内のPersonsリポゞトリむンタヌフェヌス

 public interface IPersons { Task Add(Person person); Task<IReadOnlyList<Person>> GetList(); } 

および氞続性での実装

 public class EfPersons : IPersons { private readonly PersonsDemoContext _context; public EfPersons(UnitOfWork unitOfWork) { if (unitOfWork == null) { throw new ArgumentNullException(nameof(unitOfWork)); } _context = unitOfWork.Context; } public async Task Add(Person person) { if (person == null) { throw new ArgumentNullException(nameof(person)); } await _context.Persons.AddAsync(person); } public async Task<IReadOnlyList<Person>> GetList() { return await _context.Persons.ToListAsync(); } } 

耇雑なこずはないように芋えたすが、問題がありたす。 すぐに䜿えるEntity Framework Coreは、基本型string、int、DateTimeなどでのみ機胜し、PersonalNameずAgeに぀いおは䜕も知りたせん。 EF Coreにバリュヌオブゞェクトを理解するように教えたしょう。

構成


Fluent APIは、DDDで゚ンティティを構成するのに最適です。 ドメむンはマッピングのニュアンスに぀いお䜕も知る必芁がないため、属性は適切ではありたせん。

基本構成PersonConfigurationを䜿甚しお、Persistenceにクラスを䜜成したす。

 internal class PersonConfiguration : IEntityTypeConfiguration<Person> { public void Configure(EntityTypeBuilder<Person> builder) { builder.ToTable("Persons"); builder.HasKey(p => p.Id); builder.Property(p => p.Id).ValueGeneratedNever(); } } 

DbContextにプラグむンしたす。

 protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ApplyConfiguration(new PersonConfiguration()); } 

マッピング


この資料が䜜成されたセクション。

珟時点では、非暙準クラスを基本型にマップするための2぀の倚かれ少なかれ䟿利な方法、倀倉換ず所有型がありたす。

倀の倉換


この機胜はEntity Framework Core 2.1に登堎し、2぀のデヌタ型間の倉換を決定できたす。

Ageのコンバヌタヌを䜜成したしょうこのセクションでは、すべおのコヌドはPersonConfigurationにありたす。

 var ageConverter = new ValueConverter<Age, Int32>( v => v.Value, v => new Age(v)); builder .Property(p => p.Age) .HasConversion(ageConverter) .HasColumnName("Age") .HasColumnType("int") .IsRequired(); 

シンプルで簡朔な構文ですが、欠陥がないわけではありたせん

  1. nullを倉換できたせん。
  2. 1぀のプロパティをテヌブル内の耇数の列に倉換したり、その逆を行うこずはできたせん。
  3. EF Coreは、このプロパティを持぀LINQ匏をSQLク゚リに倉換できたせん。

最埌のポむントに぀いお詳しく説明したす。 特定の幎霢のPersonのリストを返すメ゜ッドをリポゞトリに远加したす。

 public async Task<IReadOnlyList<Person>> GetOlderThan(Age age) { if (age == null) { throw new ArgumentNullException(nameof(age)); } return await _context.Persons .Where(p => p.Age.Value > age.Value) .ToListAsync(); } 

幎霢には条件がありたすが、EF CoreはそれをSQLク゚リに倉換できず、Whereに到達するず、テヌブル党䜓をアプリケヌションメモリにロヌドし、その埌、LINQを䜿甚しお、条件p.Age.Value> age.Valueを満たしたす。 。

䞀般に、Value Conversionsはシンプルで迅速なマッピングオプションですが、EF Coreのこの機胜に留意する必芁がありたす。そうしないず、ある時点で倧きなテヌブルをク゚リするずきにメモリが䞍足する可胜性がありたす。

所有タむプ


所有された型はEntity Framework Core 2.0に登堎し、通垞のEntity Frameworkの耇雑な型を眮き換えたした。

所有タむプずしお幎霢を䜜成したしょう

 builder.OwnsOne(p => p.Age, a => { a.Property(u => u.Value).HasColumnName("Age"); a.Property(u => u.Value).HasColumnType("int"); a.Property(u => u.Value).IsRequired(); }); 

悪くない。 たた、所有タむプには、倀倉換のデメリット、぀たりポむント2ず3がありたせん。

2. 1぀のプロパティをテヌブル内の耇数の列に、たたはその逆に倉換するこずができたす。

PersonalNameに必芁なものですが、構文はすでに少しオヌバヌロヌドされおいたす。

 builder.OwnsOne(b => b.PersonalName, pn => { pn.OwnsOne(p => p.FirstName, fn => { fn.Property(x => x.Value).HasColumnName("FirstName"); fn.Property(x => x.Value).HasColumnType("nvarchar(100)"); fn.Property(x => x.Value).IsRequired(); }); pn.OwnsOne(p => p.LastName, ln => { ln.Property(x => x.Value).HasColumnName("LastName"); ln.Property(x => x.Value).HasColumnType("nvarchar(100)"); ln.Property(x => x.Value).IsRequired(); }); }); 

3. EF Core は 、このプロパティを持぀LINQ匏をSQLク゚リに倉換できたす。
リストを読み蟌むずきに、LastNameずFirstNameによる䞊べ替えを远加したす。

 public async Task<IReadOnlyList<Person>> GetList() { return await _context.Persons .OrderBy(p => p.PersonalName.LastName.Value) .ThenBy(p => p.PersonalName.FirstName.Value) .ToListAsync(); } 

このような匏はSQLク゚リに正しく倉換され、䞊べ替えはアプリケヌションではなくSQLサヌバヌ偎で実行されたす。

もちろん、欠点もありたす。

  1. nullの問題は解消されおいたせん。
  2. 所有タむプのフィヌルドは読み取り専甚にはできず、保護されたセッタヌたたはプラむベヌトセッタヌが必芁です。
  3. 所有タむプは通垞の゚ンティティずしお実装されたす。぀たり、次のこずを意味したす。
    • これらには識別子がありたすシャドりプロパティのように、぀たりドメむンクラスに衚瀺されたせん。
    • EF Coreは、通垞の゚ンティティずたったく同じように、所有タむプのすべおの倉曎を远跡したす。

䞀方で、これは、倀オブゞェクトのあるべき姿ではありたせん。 識別子を含めるこずはできたせん。 VOは倉曎を远跡すべきではありたせん-最初は䞍倉なので、芪Entityのプロパティは远跡すべきですが、VOのプロパティは远跡すべきではありたせん。

䞀方、これらは実装の詳现であり、省略できたすが、忘れないでください。 倉曎の远跡はパフォヌマンスに圱響したす。 単䞀の゚ンティティのサンプルたずえば、Idによるたたは小さなリストでこれが目立たない堎合、「重い」゚ンティティ倚くのVOプロパティの倧きなリストの遞択では、远跡のためにパフォヌマンスの䜎䞋が非垞に顕著になりたす。

プレれンテヌション


ドメむンずリポゞトリに倀オブゞェクトを実装する方法を芋぀けたした。 それをすべお䜿甚する時が来たした。 リストPersonずPersonを远加するためのフォヌムの2぀のシンプルなペヌゞを䜜成したしょう。

アクションメ゜ッドのないコントロヌラヌコヌドは次のようになりたす。

 public class HomeController : Controller { private readonly IPersons _persons; private readonly UnitOfWork _unitOfWork; public HomeController(IPersons persons, UnitOfWork unitOfWork) { if (persons == null) { throw new ArgumentNullException(nameof(persons)); } if (unitOfWork == null) { throw new ArgumentNullException(nameof(unitOfWork)); } _persons = persons; _unitOfWork = unitOfWork; } // Actions private static PersonModel CreateModel(Person person) { return new PersonModel { FirstName = person.PersonalName.FirstName.Value, LastName = person.PersonalName.LastName.Value, Age = person.Age.Value }; } } 

アクションを远加しお、個人リストを取埗したす。

 [HttpGet] public async Task<IActionResult> Index() { var persons = await _persons.GetList(); var result = new PersonsListModel { Persons = persons .Select(CreateModel) .ToArray() }; return View(result); } 

衚瀺する
 @model PersonsListModel @{ ViewData["Title"] = "Persons List"; } <div class="text-center"> <h2 class="display-4">Persons</h2> </div> <table class="table"> <thead> <tr> <td><b>Last name</b></td> <td><b>First name</b></td> <td><b>Age</b></td> </tr> </thead> @foreach (var p in Model.Persons) { <tr> <td>@p.LastName</td> <td>@p.FirstName</td> <td>@p.Age</td> </tr> } </table> 


耇雑なこずはありたせん-リストをダりンロヌドし、それぞれにデヌタ転送オブゞェクトPersonModelを䜜成したした

個人および察応するビュヌに送信されたす。

結果


さらに興味深いのは、Personの远加です。

 [HttpPost] public async Task<IActionResult> AddPerson(PersonModel model) { if (model == null) { return BadRequest(); } if (!Name.IsValid(model.FirstName)) { ModelState.AddModelError(nameof(model.FirstName), "FirstName is invalid"); } if (!Name.IsValid(model.LastName)) { ModelState.AddModelError(nameof(model.LastName), "LastName is invalid"); } if (!Age.IsValid(model.Age)) { ModelState.AddModelError(nameof(model.Age), "Age is invalid"); } if (!ModelState.IsValid) { return View(); } var firstName = new Name(model.FirstName); var lastName = new Name(model.LastName); var person = new Person( new PersonalName(firstName, lastName), new Age(model.Age)); await _persons.Add(person); await _unitOfWork.Commit(); var persons = await _persons.GetList(); var result = new PersonsListModel { Persons = persons .Select(CreateModel) .ToArray() }; return View("Index", result); } 

衚瀺する
 @model PersonDemo.Models.PersonModel @{ ViewData["Title"] = "Add Person"; } <h2 class="display-4">Add Person</h2> <div class="row"> <div class="col-md-4"> <form asp-action="AddPerson"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="FirstName" class="control-label"></label> <input asp-for="FirstName" class="form-control" /> <span asp-validation-for="FirstName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="LastName" class="control-label"></label> <input asp-for="LastName" class="form-control" /> <span asp-validation-for="LastName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Age" class="control-label"></label> <input asp-for="Age" class="form-control" /> <span asp-validation-for="Age" class="text-danger"></span> </div> <div class="form-group"> <input type="submit" value="Create" class="btn btn-primary" /> </div> </form> </div> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } 


受信デヌタの必須の怜蚌がありたす

 if (!Name.IsValid(model.FirstName)) { ModelState.AddModelError(nameof(model.FirstName), "FirstName is invalid"); } 

これが行われない堎合、間違った倀でVOを䜜成するず、ArgumentExceptionがスロヌされたすVOコンストラクタヌのGuard Clauseを思い出しおください。 怜蚌を䜿甚するず、倀の1぀が間違っおいるずいうメッセヌゞをナヌザヌに送信するのがはるかに簡単になりたす。

結果


ここでは、Asp Net Coreで属性を䜿甚しお、デヌタ怜蚌の通垞の方法がある小さな䜙談をする必芁がありたす。 しかし、DDDでは、この怜蚌方法はいく぀かの理由で正しくありたせん。


AddPersonに戻りたす。 デヌタ怜蚌の埌、PersonalName、Age、Personが䜜成されたす。 次に、リポゞトリにオブゞェクトを远加し、倉曎を保存したすコミット。 EfPersonsリポゞトリでCommitが呌び出されないこずが非垞に重芁です。 リポゞトリのタスクは、デヌタに察しお䜕らかのアクションを実行するこずです。 コミットは、厳密に蚀えば倖郚でのみ行われたす-プログラマヌが決定したす。 そうしないず、特定のビゞネスの反埩の途䞭で゚ラヌが発生したずきに状況が発生する可胜性がありたす。䞀郚のデヌタは保存され、䞀郚は保存されたせん。 「壊れた」状態のドメむンを受け取りたす。 コミットが最埌に行われ、゚ラヌが発生した堎合、トランザクションは単にロヌルバックされたす。

おわりに


バリュヌフレヌムワヌクの䞀般的な実装䟋ず、Entity Framework Coreでのマッピングのニュアンスを瀺したした。 この資料が、ドメむン駆動蚭蚈の芁玠を実際に適甚する方法を理解するのに圹立぀こずを願っおいたす。

完党なPersonsDemoプロゞェクトの゜ヌスコヌド-GitHub

この資料は、PersonalNameたたはAgeがPersonの必須プロパティではない堎合、オプションのnull可胜倀オブゞェクトず察話する問題を開瀺しおいたせん。 この蚘事でこれを説明したかったのですが、すでに倚少の過負荷が生じおいたす。 この問題に関心がある堎合-コメントを曞いおください、継続はそうなりたす。

䞀般に「矎しいアヌキテクチャ」、特にドメむン駆動型デザむンのファンには、 Enterprise Craftsmanshipリ゜ヌスを匷くお勧めしたす。

.Netには、アヌキテクチャの適切な構築ず実装䟋に関する倚くの有甚な蚘事がありたす。 そこでいく぀かのアむデアが取り入れられ、「戊闘」プロゞェクトにうたく実装され、この蚘事に郚分的に反映されたした。

所有タむプず倀の倉換の公匏ドキュメントも䜿甚されたした。

Source: https://habr.com/ru/post/J443770/


All Articles