私の作品のほとんどはJava Spring Bootを使用して記述されています。最近のプロジェクトでは、エンティティにビジネスロジックが含まれていませんでした。リポジトリの処理にSpring Dataが使用され、コントローラが取得するために呼び出すサービスクラスがあります。エンティティに対して特定のデータまたはアクションを実行します。たとえば、ユーザーパスワードのリセットを処理するresetPasswordアクションを持つUserServiceがあるとします。
最近ドメイン主導の設計について多くのことを読みましたが、1冊の本の例がエンティティのように見えるものの動作を示しているため、混乱しています。たとえば、注文に対するchangeStatus()アクション。
MVCパターンを適用する場合、コントローラー、サービス、DTO、DAO、およびエンティティ間の関係を理解するのに苦労しています。
Webサービスを構築しているとしましょう。コントローラーはJSONをクライアントに返します。エンティティはデータベースの構造と一致しますが、データの構造がデータベースと異なる可能性があるため、データをクライアントに返すには別のタイプのオブジェクトが必要です。これはDTOですか?
コントローラはサービスと通信しますか?そして、サービスは何を返しますか?エンティティまたはDTO?エンティティーをコントローラーのマッパーを使用してDTOにマップする場合
行動についてはどうですか?これはサービスクラスで問題ありませんか、それとも、動作を含むDAOにエンティティをマップする必要がありますか?
混乱しています。これを調査すると、エンティティ、リポジトリ、DAOが混乱しているようです。エンティティには動作を含めることができると言う人もいれば、いいえと言う人もいます。
別の例として、ユーザーがシステムで持っているアクセス許可を知りたい場合、getUserPermissions(User user)を呼び出してListを返すことでこれを見つけることができます。コントローラーの内部から何を呼び出しますか?これをPermissionsServiceクラスに入れて呼び出しますか?または、エンティティを表す別のオブジェクトがありますが、これにも動作があり、リポジトリと通信して追加のデータを収集できますか?リポジトリをエンティティに挿入することになるため、エンティティ自体に配置することはできませんでした。
どんなガイダンスもありがたいです。
あなたの主な出発点は
Evansは、ドメインモデルを最初に置くことの意味を明確に紹介しています。 Fowlerは、ドメインモデルパターンを、それに代わるいくつかの方法とともに要約しています。
ここに重要なアイデアがあります-企業はカスタムソフトウェアを(既製品から購入するのではなく)作成します。なぜなら、必要な既製のウェアが存在しないか、詳細を微調整できることが重要だからです。ビジネスの収益。
JVM開発チームを抱える企業の場合、自分のWebサーバー、データアクセスオブジェクト、または他の誰もが使用するのと同じトラックをロールバックしないでください。 Spring、または開発チームが好む他の何かをダウンロードし、開発資本を、違いを生むコードに投資します。
したがって、Evansが「エンティティ」について語るとき、彼はデータベースの行については語っていません-それは単なる配管です。彼はビジネスモデルにとって重要な概念について話している-貨物輸送とはどういう意味ですか?コンテナーが間違ったポートでアンロードされたか、接続が失われたことを通知するメッセージが表示された場合、それに対して何をしますか?
そしてもちろん、エヴァンス/ファウラーの枠組みでは、ドメインエンティティを表すクラスには、自身の状態を変更するメソッドが含まれています。 教えて、聞かないでください 。
リポジトリクラスは、dbからOrderEntityを取得して返すことを担当しますが、ドメインのOrderオブジェクトにマッピングするのはどのクラスですか。
Evansの第6章によると、その作業はリポジトリ内でも行われます。より正確には、(休止状態から)値を読み取り、ファクトリを使用して値を集約ルートに変換してから、ルートをアプリケーションに返します。
(したがって、アプリケーションコードは、休止状態の値をまったく確認できるようになり、集約ルート内に含まれます)。
アーキテクチャの観点からアプリケーションを理解することが重要です。あなたの混乱は、なぜアプリケーションがそのように構成されているのかについての理解の欠如が原因である可能性が高いと思います。従来の「階層化」アーキテクチャである基本から始めて、次のようになります。
View -> Application -> Domain -> Persistence
依存関係のフローが「下向き」になるように、各レイヤーはその上のレイヤーから完全に切り離されているという考えです。依存関係と結合の管理は、どのアーキテクチャでも最も重要な問題です!上記が達成するのは、結果としてアプリケーションの他のどの部分を変更する必要があるかもしれないかを知るように、レイヤーを「スワップアウト」する機能です(レイヤー( s)真上)。この強力なアイデアを念頭に置いて、次のような方法で依存関係を構造化することは理にかなっています。
Volatile Modules -> Stable Modules
Domain
は最も揮発性が低いモジュールである可能性が高いため(ほとんどの場合スワップアウトされません)、DIP(依存関係逆転プリンシパル)を使用して、上記の階層化アーキテクチャを「修正」できます。
View -> Application -> Domain <- Persistence
これで、アプリケーションで記述しているアーキテクチャに到達しました。次に、各レイヤーを調べて、各レイヤーが何を担当すべきかを理解します。
View
レイヤーは、コントローラーが存在する場所です。各Controller
は単にApplication
レイヤーへのデータ(および制御)のフローを仲介します。重要なのは、コントローラーにbusinessロジックが含まれていないことです。これらには、外界からの入力をApplication
レイヤーが理解できる概念に変換し、Application
層からの出力を外界が理解できる概念に変換するために必要なロジックのみが含まれます。 View
レイヤーをWebインターフェースからコマンドラインインターフェースに「スワップアウト」した場合(または別のレイヤーを追加した場合)、新しいコントローラーが必要になります。 View
への入力/出力は必要に応じて異なります。
Application
レイヤーは、ユーザーのユースケースを実行するために使用されます。これは、あなたのDomain
をcoordinatingして行います。繰り返しますが、この層にビジネスロジックが含まれていないことが重要です。たとえば、パスワードを変更する場合:
class ChangePasswordCommand
{
private string newPassword;
public string UserId { get; set; }
public string CurrentPassword { get; set; }
public string NewPassword
{
get { return newPassword; }
set {
if( value.Length > 50 ) {
throw new System.ArgumentException("New password must be less than 50 characters");
}
newPassword = value;
}
}
}
class ChangePasswordCommandHandler
{
private IUserRepository userRepository;
handle( ChangePasswordCommand cmd )
{
userId = cmd.UserId;
newPassword = cmd.NewPassword;
currentPassword = cmd.CurrentPassword;
user = this.userRepository.FindById(userId);
// this may raise a UserChangedPasswordDomainEvent
// or throw a PasswordIsNotDifferentException
user.changePassword(newPassword, currentPassword);
this.userRepository.save(user)
}
}
ここでは、パスワードの実際の変更に関連する「ロジック」が発生していないことに注意してください。 ChangePasswordCommand
が保護している不変条件は、実際にはbusinessロジックとは関係ありません。つまり、あなたのCEOはおそらくUsers
パスワードの長さについて心配していません。パスワードの長さは不変であり、システムの技術の詳細(バッキングデータベースフィールドがVARCHAR(50)などである)であり、「失敗する」速い」。
コマンドハンドラー自体は、理解できる入力(ChangePasswordCommand
)を受け取り、ドメインモデルを調整してコマンドを実行します。トランザクション管理などもApplication
レイヤーで行う必要があります。すべてのユースケースを3ステップの問題と考えてください。
IUserRepository
への依存関係も注目に値します。このインターフェイスはDomain
レイヤーで定義されていますが、Persistence
レイヤーに実装されています(これはDIです)追加されました)。
制御の流れに従って、Persistence
レイヤーに到達します。これはORMが存在する場所です(そしてここだけです!)。たとえば、UserRepository.FindById
は次のようになります。
public User FindById( string userId )
{
// 1. get UserEntity
// 2. map UserEntity to User
// 3. return User
}
特定のフォーマットを必要とするマッピングにライブラリを使用するつもりでない限り、実際には追加のDTOやその他の必要はありません。繰り返しますが、ここでの考え方は、この層は完全に置き換えることができ、アプリケーションの他の部分では違いを知ることさえできないということです。このレイヤーの唯一の変更ベクトルは、ORMまたはDomain
を変更/更新するときです。
最後に、あなたのDomain
を取得します。ここですべてが起こっています。ここで、2つのパスワードが同じにならないようにし、それに応じてドメインモデルを変更します。ここではあまり説明は必要ありません。
最後に、上記のすべては、システムへの書き込みのコンテキストでのみ実際に必要であることをここで書き留めておきます。必要に応じてそこから読み取ることができます(読み取り中に無効な状態になる危険はありません)。ユーザー権限が必要な場合は、取得してください。過度に複雑にしないでください。 View
に必要なデータを取得しますが、適切なチャネル(Domain
)を介してのみデータを書き込むようにしてください。これがCQRSの考え方です。
個別の読み取りモデルプロジェクトを自由に作成できますが、このプロジェクトにはDomain
の概念も必要もないため、非常に冗長になります。必要に応じて、いくつかの読み取りサービスを定義して整理するのが最善です。あるプロジェクトを使用するいくつかのController
メソッド(読み取り)と別のプロジェクトを使用する他のメソッド(書き込み)の混乱を理解できますが、View
には両方が必要です。