Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9715

DDD の練習してみた(C# 編)

$
0
0

本記事は ドメイン駆動設計#1 Advent Calendar 2019 18 日目の記事です。
元々私が参加登録していたわけではありませんが、空きが出ていたので、僭越ながら記事を書かせていただきます。

はじめに

最近 DDD が盛り上がっていますね。
私も数年前にエヴァンス本読んでちょくちょく勉強してたのですが、今年から色々カンファレンス行くようになって本格的に勉強始めました。
ということで、今回は個人的に DDD を練習してみた結果を書いてみたいと思います。
言語は普段使っている C# で書いています。

テーマは「備品予約」

練習しようと思ったきっかけは、「レガシーをぶっつぶせ。現場でDDD!」に参加した際に購入した「もくもくモデリングの森を旅するチビドラゴンの軌跡」という本です。
サンプルのお題として会議室予約が挙げられており、会社の人と勉強やろうと思って話してたら、会社で使ってる備品の予約とかどう?となって、まずは自分でやってみようと思ったところが始まりです。

要件

まずは要件を以下のような感じでまとめました。
最初なので、そんなに複雑にはしませんでした。

  • 利用者は備品を予約できる。
    • 備品には USB, ポケットWifi, 携帯電話 がある。
    • 備品、予約時間、利用目的を指定して予約する。
    • 予約対象の備品がすでに同一時間帯に予約されている場合は予約できない。
  • 利用者は予約をキャンセルできる。
  • 利用者は予約を変更できる。

ドメインモデル

上記の要件から、ドメインオブジェクトを抽出していき、結果としてこんな感じになりました。

ドメインモデル.png

上記のように予約に備品を紐づけるか、備品に予約をぶら下げるか考えましたけど、予約が中心でしょ!という感じで前者にしました。笑

実装

上記のドメインモデルを実装していきます。

レイヤー分割

まずは DLL を以下のように分けました。
パッケージ.png

アプリケーション層とドメイン層がインフラ層に依存しないように、依存関係を逆転させました。
各層の中身は以下のようになっています。内容を少しづつ紹介していこうと思います。

Architecture.png

ドメイン層

Domain.png

  • Entity、ValueObject、DomainService などのドメインオブジェクトが格納されます。
  • 集約ルートごとに Repository のインターフェースが格納されます。
  • Entity では基本的にプリミティブ型は使わず ValueObject を使うようにしています。

ValueObject のコンストラクタで値のチェックをしています。

ReservationDateTime.cs
// 利用時間の ValueObjectpublicclassReservationDateTime:IValueObject{publicReservationDateTime(DateTimestart,DateTimeend){if(start.CompareTo(end)>=0)thrownewArgumentException("終了日時は開始日時よりも後にしてください。");Start=start;End=end;}publicDateTimeStart{get;}publicDateTimeEnd{get;}// 以下省略}
PurposeOfUse.cs
// 利用目的 の ValueObjectpublicclassPurposeOfUse:IValueObject{publicPurposeOfUse(stringvalue){Assertion.ArgumentRange(value,64,nameof(PurposeOfUse));Value=value;}publicstringValue{get;}// 以下省略}

Entity は各フィールドのプロパティを用意し、private な Setter の中で値のチェックを行います。値を変更するときは SetXXX のような名称ではなく、実際のドメインで使われている名前を付けたメソッドを用意して呼び出せるようにしています。(英語のセンスがなくて微妙かもしれないが。)

Reservation.cs
publicclassReservation:IEntity{publicReservation(ReservationIdid,AccountIdaccountId,EquipmentIdequipmentId,ReservationDateTimereservationDateTime,PurposeOfUsepurposeOfUse,ReservationStatusreservationStatus){Id=id;AccountId=accountId;EquipmentId=equipmentId;ReservationDateTime=reservationDateTime;ReservationStatus=ReservationStatus.Reserved;PurposeOfUse=purposeOfUse;ReservationStatus=reservationStatus;}privateReservationId_id;publicReservationIdId{get{return_id;}privateset{Assertion.ArgumentNotNull(value,nameof(Id));_id=value;}}privateReservationDateTime_reservationDateTime;publicReservationDateTimeReservationDateTime{get{return_reservationDateTime;}privateset{Assertion.ArgumentNotNull(value,nameof(ReservationDateTime));_reservationDateTime=value;}}privatePurposeOfUse_purposeOfUse;publicPurposeOfUsePurposeOfUse{get{return_purposeOfUse;}privateset{Assertion.ArgumentNotNull(value,nameof(PurposeOfUse));_purposeOfUse=value;}}publicvoidChangeReservationDateTime(ReservationDateTimereservationDateTime){ReservationDateTime=reservationDateTime;}publicvoidChangePurposeOfUse(PurposeOfUsepurposeOfUse){PurposeOfUse=purposeOfUse;}publicboolIsDupulicated(Reservationother){Assertion.ArgumentNotNull(other,nameof(other));if(!EquipmentId.Equals(other.EquipmentId))returnfalse;if(!ReservationDateTime.IsRangeOverlapping(other.ReservationDateTime))returnfalse;returntrue;}// 以下省略}

アプリケーション層

Application.png

  • アプリケーションサービスを格納します。
  • CQRS の考え方を利用して、Command と Query のアプリケーションサービスを分けています。ただし、DB は共通としています。(Command は ApplicationService、Query は QueryService という名前にしている。)
  • アプリケーションサービスでは Unit Of Work を利用したトランザクション管理を行っています。このレイヤーには、UnitOfWork のインターフェースの未定義し、実装はインフラ層で行っています。
  • QueryService では、直接 SQL 発行しておらず、インターフェース(IQuery)を定義して、SQL の発行はインフラ層で行っています。(QueryService を挟まずとも、IQueryService の実装をインフラ層で行っても構わないと思います。)
ReservationAppService.cs
publicclassReservationAppService:IReservationAppService{privatereadonlyIUnitOfWork_unitOfWork;publicReservationAppService(IUnitOfWorkunitOfWork){_unitOfWork=unitOfWork??thrownewArgumentNullException(nameof(unitOfWork));}publicvoidRegisterReservation(RegisterReservationRequestrequest){_unitOfWork.Begin();try{_unitOfWork.ReservationRepository.Lock();varreservation=newReservation(newReservationId(),newAccountId(request.AccountId),newEquipmentId(request.EquipmentId),newReservationDateTime(request.StartDateTime,request.EndDateTime),newPurposeOfUse(request.PurposeOfUse),ReservationStatus.Reserved);SaveReservation(reservation);_unitOfWork.Commit();}catch{_unitOfWork.Rollback();throw;}}publicvoidChangeReservationInfo(ChangeReservationInfoRequestrequest){_unitOfWork.Begin();try{_unitOfWork.ReservationRepository.Lock();varreservation=FindReservationWithValidation(request.ReservationId);reservation.ChangeAccountOfUse(newAccountId(request.AccountId));reservation.ChangeEquipment(newEquipmentId(request.EquipmentId));reservation.ChangeReservationDateTime(newReservationDateTime(request.StartDateTime,request.EndDateTime));reservation.ChangePurposeOfUse(newPurposeOfUse(request.PurposeOfUse));SaveReservation(reservation);_unitOfWork.Commit();}catch{_unitOfWork.Rollback();throw;}}publicvoidCancelReservation(CancelReservationRequestrequest){_unitOfWork.Begin();try{varreservation=FindReservationWithValidation(request.ReservationId);reservation.Cancel();_unitOfWork.ReservationRepository.Save(reservation);_unitOfWork.Commit();}catch{_unitOfWork.Rollback();throw;}}privateReservationFindReservationWithValidation(stringreservationId){varreservation=_unitOfWork.ReservationRepository.Find(newReservationId(reservationId));if(reservation==null){thrownewReservationNotFoundException();}returnreservation;}privatevoidSaveReservation(Reservationreservation){varservice=newReservationService(_unitOfWork.ReservationRepository);if(service.IsDupulicatedReservation(reservation)){thrownewReservationDupulicationException();}_unitOfWork.ReservationRepository.Save(reservation);}}

予約リスト画面や予約詳細画面には、予約のアカウント名や備品名を表示したいが、Entity を利用としようとすると、予約Entity、アカウントEntity、備品Entity のデータを取得しなければならず効率が悪いため、QueryService を利用して SQL で結合した結果を取得するようにします。(SQL の実装自体はインフラ層で行う。)

ReservationQueryService.cs
publicclassReservationQueryService:IReservationQueryService{privatereadonlyIQueryFactory_queryFactory;publicReservationQueryService(IQueryFactoryqueryFactory){_queryFactory=queryFactory??thrownewArgumentNullException(nameof(queryFactory));}publicGetReservationDataResponseGetReservationData(GetReservationDataRequestrequest){returnnewGetReservationDataResponse(){ReservationData=_queryFactory.ReservationDataQuery.FindReservationData(newReservationId(request.ReservationId))};}publicGetAllReservationListDataResponseGetAllReservationListData(){returnnewGetAllReservationListDataResponse(){ReservationListDataList=_queryFactory.ReservationDataQuery.FindAllReservationListData()};}}

インフラ層

Infrastructure.png

  • Repository、Query の実装、UnitOfWork の実装を格納しています。
  • DB アクセスは Entity Framework、Dapper を利用しています。
ReservationRepository.cs
publicclassReservationRepository:IReservationRepository{privatereadonlyMyDbContext_dbContext;publicReservationRepository(MyDbContextdbContext){_dbContext=dbContext??thrownewArgumentNullException(nameof(dbContext));}publicReservationFind(ReservationIdreservationId,ReservationStatus?reservationStatus=null){IQueryable<RESERVATIONS>reservations=_dbContext.Reservations.Include(_=>_.reservations_status);if(reservationStatus!=null){reservations=reservations.Where(_=>_.reservations_status.status==(int)reservationStatus.Value);}varreservation=reservations.SingleOrDefault(_=>_.id==reservationId.Value);returnCreate(reservation);}publicIEnumerable<Reservation>FindByEquipmentId(EquipmentIdequipmentId,ReservationStatus?reservationStatus=null){varreservations=_dbContext.Reservations.Include(_=>_.reservations_status).Where(_=>_.equipments_id==equipmentId.Value);if(reservationStatus!=null){reservations=reservations.Where(_=>_.reservations_status.status==(int)reservationStatus.Value);}returnreservations.Select(_=>Create(_)).ToArray();}publicvoidSave(Reservationentity){varreservation=_dbContext.Reservations.Find(entity.Id.Value);if(reservation==null){reservation=newRESERVATIONS();_dbContext.Reservations.Add(reservation);}reservation.id=entity.Id.Value;reservation.accounts_id=entity.AccountId.Value;reservation.equipments_id=entity.EquipmentId.Value;reservation.start_date_time=entity.ReservationDateTime.Start;reservation.end_date_time=entity.ReservationDateTime.End;reservation.purpose_of_use=entity.PurposeOfUse.Value;varreservationStatus=_dbContext.ReservationsStatus.Find(reservation.id);if(reservationStatus==null){reservationStatus=newRESERVATIONS_STATUS();_dbContext.ReservationsStatus.Add(reservationStatus);}reservationStatus.reservations_id=reservation.id;reservationStatus.status=(int)entity.ReservationStatus;_dbContext.SaveChanges();}publicvoidLock(){_dbContext.QueryObjects<RESERVATIONS>("select * from reservations for update;");}privateReservationCreate(RESERVATIONSreservation){if(reservation==null)returnnull;returnnewReservation(newReservationId(reservation.id),newAccountId(reservation.accounts_id),newEquipmentId(reservation.equipments_id),newReservationDateTime(reservation.start_date_time,reservation.end_date_time),newPurposeOfUse(reservation.purpose_of_use),(ReservationStatus)reservation.reservations_status.status);}}
ReservationDataQuery.cs
publicclassReservationDataQuery:IReservationDataQuery{privatereadonlyMyDbContext_dbContext;publicReservationDataQuery(MyDbContextdbContext){_dbContext=dbContext??thrownewArgumentNullException(nameof(dbContext));}publicReservationDataFindReservationData(ReservationIdreservationId){varreservation=_dbContext.Reservations.Find(reservationId.Value);returnCreateReservationData(reservation);}publicIEnumerable<ReservationListData>FindAllReservationListData(){return_dbContext.Reservations.Include(_=>_.accounts).Include(_=>_.equipments).Include(_=>_.reservations_status).Where(_=>_.reservations_status.status==(int)ReservationStatus.Reserved).Select(_=>CreateReservationListData(_)).ToArray();}publicReservationListDataFindReservationListData(ReservationIdreservationId){varreservation=_dbContext.Reservations.Include(_=>_.accounts).Include(_=>_.equipments).Where(_=>_.id==reservationId.Value).SingleOrDefault();returnCreateReservationListData(reservation);}privateReservationDataCreateReservationData(RESERVATIONSreservation){if(reservation==null)returnnull;returnnewReservationData(){Id=reservation.id,AccountId=reservation.accounts.id,EquipmentId=reservation.equipments_id,StartDateTime=reservation.start_date_time,EndDateTime=reservation.end_date_time,PurposeOfUse=reservation.purpose_of_use,};}privateReservationListDataCreateReservationListData(RESERVATIONSreservation){if(reservation==null)returnnull;returnnewReservationListData(){Id=reservation.id,AccountId=reservation.accounts.id,EquipmentId=reservation.equipments_id,StartDateTime=reservation.start_date_time,EndDateTime=reservation.end_date_time,PurposeOfUse=reservation.purpose_of_use,AccountName=reservation.accounts.account_name,EquipmentType=reservation.equipments.equipment_type,EquipmentName=reservation.equipments.equipment_name};}}
UnitOfWork.cs
publicclassUnitOfWork:IUnitOfWork{privatereadonlyMyDbContext_dbContext;publicUnitOfWork(MyDbContextdbContext){_dbContext=dbContext??thrownewArgumentNullException(nameof(dbContext));}privateIReservationRepository_reservationRepository;publicIReservationRepositoryReservationRepository{get{if(_reservationRepository==null)_reservationRepository=newReservationRepository(_dbContext);return_reservationRepository;}}publicvoidBegin(){_dbContext.Database.BeginTransaction();}publicvoidCommit(){_dbContext.Database.CommitTransaction();}publicvoidRollback(){_dbContext.Database.RollbackTransaction();}privatebooldisposed=false;publicvoidDispose(){Dispose(true);GC.SuppressFinalize(this);}protectedvirtualvoidDispose(booldisposing){if(disposed)return;if(disposing){_dbContext.Dispose();}disposed=true;}}

ビュー層

  • ASP .NET Core MVC の Web アプリケーションの入り口。
  • View、Controller を格納する。
  • DI(依存性の注入)を行う。

実装は、画面から入力されたデータを、ApplicationService、QueryService のメソッド引数の型に変換して、サービスのメソッドを呼び出す感じです。(投げやり)

まとめ

ということで、最後の方はだいぶ駆け足になりましたが、自分としてはなんとなく各レイヤーでどんな実装をすればよいか感触をつかめました。
モデリングがまだまだだなので、もう少し複雑なドメインを例にまたやってみようと思います。
あとは、Java、Spring を使ったサンプルも作っていきたいと思います。

ソースコードは以下に公開しています。
https://github.com/TakashiOnawa/EquipmentReservation


Viewing all articles
Browse latest Browse all 9715

Trending Articles