DDDのいくつかの概念を実際のコードに適用する方法は?内部の特定の質問


9

私はDDDを研究しており、現在、実際のコードに概念を適用する方法を見つけるのに苦労しています。私はN層で約10年の経験があるので、私が苦労している理由は、私のメンタルモデルがそのデザインにあまりにも結合しているためです。

私はAsp.NET Webアプリケーションを作成し、単純なドメインであるWeb監視アプリケーションから始めています。要件:

  • ユーザーは、監視する新しいWebアプリを登録できる必要があります。Webアプリにはわかりやすい名前が付けられ、URLをポイントします。
  • Webアプリは定期的にステータス(オンライン/オフライン)をポーリングします。
  • Webアプリは定期的に現在のバージョンをポーリングします(Webアプリには、特定のマークアップでシステムバージョンを宣言するファイルである「/version.html」が含まれていることが予想されます)。

私の疑問は主に責任の分割に関係しており、それぞれの適切な場所(検証、ビジネスルールなど)を見つけることです。以下に、コードをいくつか書き、質問と考慮事項を含むコメントを追加しました。

批判し、助言してください。前もって感謝します!


ドメインモデル

すべてのビジネスルールをカプセル化するようにモデル化されています。

// Encapsulates logic for creating and validating Url's.
// Based on "Unbreakable Domain Models", YouTube talk from Mathias Verraes
// See https://youtu.be/ZJ63ltuwMaE
public class Url: ValueObject
{
    private System.Uri _uri;

    public string Url => _uri.ToString();

    public Url(string url)
    {
        _uri = new Uri(url, UriKind.Absolute); // Fails for a malformed URL.
    }
}

// Base class for all Aggregates (root or not).
public abstract class Aggregate
{
    public Guid Id { get; protected set; } = Guid.NewGuid();
    public DateTime CreatedAt { get; protected set; } = DateTime.UtcNow;
}

public class WebApp: Aggregate
{
    public string Name { get; private set; }
    public Url Url { get; private set; }
    public string Version { get; private set; }
    public DateTime? VersionLatestCheck { get; private set; }
    public bool IsAlive { get; private set; }
    public DateTime? IsAliveLatestCheck { get; private set; }

    public WebApp(Guid id, string name, Url url)
    {
        if (/* some business validation fails */)
            throw new InvalidWebAppException(); // Custom exception.

        Id = id;
        Name = name;
        Url = url;
    }

    public void UpdateVersion()
    {
        // Delegates the plumbing of HTTP requests and markup-parsing to infrastructure.
        var versionChecker = Container.Get<IVersionChecker>();
        var version = versionChecker.GetCurrentVersion(this.Url);

        if (version != this.Version)
        {
            var evt = new WebAppVersionUpdated(
                this.Id, 
                this.Name, 
                this.Version /* old version */, 
                version /* new version */);
            this.Version = version;
            this.VersionLatestCheck = DateTime.UtcNow;

            // Now this eems very, very wrong!
            var repository = Container.Get<IWebAppRepository>();
            var updateResult = repository.Update(this);
            if (!updateResult.OK) throw new Exception(updateResult.Errors.ToString());

            _eventDispatcher.Publish(evt);
        }

        /*
         * I feel that the aggregate should be responsible for checking and updating its
         * version, but it seems very wrong to access a Global Container and create the
         * necessary instances this way. Dependency injection should occur via the
         * constructor, and making the aggregate depend on infrastructure also seems wrong.
         * 
         * But if I move such methods to WebAppService, I'm making the aggregate
         * anaemic; It will become just a simple bag of getters and setters.
         *
         * Please advise.
         */
    }

    public void UpdateIsAlive()
    {
        // Code very similar to UpdateVersion().
    }
}

そして、CreatesとDeletesを処理するDomainServiceクラス。これはAggregate自体の問題ではないと私は思います。

public class WebAppService
{
    private readonly IWebAppRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEventDispatcher _eventDispatcher;

    public WebAppService(
        IWebAppRepository repository, 
        IUnitOfWork unitOfWork, 
        IEventDispatcher eventDispatcher
    ) {
        _repository = repository;
        _unitOfWork = unitOfWork;
        _eventDispatcher = eventDispatcher;
    }

    public OperationResult RegisterWebApp(NewWebAppDto newWebApp)
    {
        var webApp = new WebApp(newWebApp);

        var addResult = _repository.Add(webApp);
        if (!addResult.OK) return addResult.Errors;

        var commitResult = _unitOfWork.Commit();
        if (!commitResult.OK) return commitResult.Errors;

        _eventDispatcher.Publish(new WebAppRegistered(webApp.Id, webApp.Name, webApp.Url);
        return OperationResult.Success;
    }

    public OperationResult RemoveWebApp(Guid webAppId)
    {
        var removeResult = _repository.Remove(webAppId);
        if (!removeResult) return removeResult.Errors;

        _eventDispatcher.Publish(new WebAppRemoved(webAppId);
        return OperationResult.Success;
    }
}

アプリケーション層

以下のクラスは、WebMonitoringドメインの外部へのインターフェースを提供します(Webインターフェース、REST APIなど)。これは現時点では単なるシェルであり、適切なサービスへの呼び出しをリダイレクトしますが、将来的にはより多くのロジックを調整するように成長するでしょう(常にドメインモデルを介して実現されます)。

public class WebMonitoringAppService
{
    private readonly IWebAppQueries _webAppQueries;
    private readonly WebAppService _webAppService;

    /*
     * I'm not exactly reaching for CQRS here, but I like the idea of having a
     * separate class for handling queries right from the beginning, since it will
     * help me fine-tune them as needed, and always keep a clean separation between
     * crud-like queries (needed for domain business rules) and the ones for serving
     * the outside-world.
     */

    public WebMonitoringAppService(
        IWebAppQueries webAppQueries, 
        WebAppService webAppService
    ) {
        _webAppQueries = webAppQueries;
        _webAppService = webAppService;
    }

    public WebAppDetailsDto GetDetails(Guid webAppId)
    {
        return _webAppQueries.GetDetails(webAppId);
    }

    public List<WebAppDetailsDto> ListWebApps()
    {
        return _webAppQueries.ListWebApps(webAppId);
    }

    public OperationResult RegisterWebApp(NewWebAppDto newWebApp)
    {
        return _webAppService.RegisterWebApp(newWebApp);
    }

    public OperationResult RemoveWebApp(Guid webAppId)
    {
        return _webAppService.RemoveWebApp(newWebApp);
    }
}

問題を閉じる

ここと別の理由で開いたこの他の質問で回答を集めた後、最終的にこれと同じポイントに到達し、このよりクリーンでより良い解決策を思いつきました:

Github Gistでのソリューションの提案


私はたくさん読んでいますが、CQRSと他の直交パターンと実践を適用するものを除いて、そのような実用的な例は見つかりませんでしたが、今、この単純なものを探しています。
Levidad 2018年

1
この質問はcodereview.stackexchange.comに適しているかもしれません
VoiceOfUnreason

2
私自身、n層アプリで多くの時間を費やしているあなたが好きです。私は本やフォーラムなどからのみDDDについて知っているので、コメントのみを投稿します。検証には、入力検証とビジネスルール検証の2種類があります。入力の検証はアプリケーション層で行われ、ドメインの検証はドメイン層で行われます。WebAppは、アグリゲートではなくエンティティのように見え、WebAppServiceはDomainServiceよりもアプリケーションサービスのように見えます。また、あなたの集合体は、インフラストラクチャの問題であるコンテナを参照します。また、サービスロケータのようにも見えます。
エイドリアンイフトーデ2018年

1
はい、それは関係をモデル化していないからです。集合体は、ドメインオブジェクト間の関係をモデル化しています。WebAppは生データといくつかの動作のみを持ち、たとえば次の不変式を処理する可能性があります。クレイジーのようなバージョンを更新することはできません。つまり、現在のバージョンが1のときにバージョン3にステップします
Adrian Iftode

1
ValueObjectがインスタンス間の等価性を実装するメソッドを持っている限り、私は大丈夫だと思います。シナリオでは、バージョン値オブジェクトを作成できます。セマンティックバージョニングを確認すると、不変条件や動作など、この値オブジェクトをモデル化する方法について多くのアイデアが得られます。WebAppはリポジトリと通信すべきではありません。実際には、直接または間接的に(インターフェースを介して)インフラストラクチャ(リポジトリ、作業単位)に関連する他のものへのドメイン情報を含むプロジェクトからの参照がないようにしてください。
エイドリアンイフトーデ2018年

回答:


1

あなたのWebApp集計に関するアドバイスの長い行ですが、ここに引き込むことはrepositoryここでの正しいアプローチではないことに完全に同意します。私の経験では、Aggregateは自身の状態に基づいて、アクションが大丈夫かどうかを「決定」します。したがって、他のサービスからプルする可能性がある状態ではありません。このようなチェックが必要な場合は、通常、それを集計を呼び出すサービス(例ではWebAppService)に移動します。

さらに、いくつかのアプリケーションが同時にアグリゲートを呼び出したいユースケースに到達する場合があります。これが発生する場合、このような長い時間を要する可能性のあるアウトバウンドコールを実行しているときに、他の使用のために集計をブロックしています。これは結局、集約処理を遅くしますが、これも望ましくないと思います。

そのため、その検証のビットを移動すると、集計が非常に薄くなるように見えるかもしれませんが、それをに移動する方が良いと思いますWebAppService

また、WebAppRegisteredイベントの公開をあなたの集合体に移動することをお勧めします。集合体は作成される人物なので、作成プロセスが成功した場合は、その知識を世界に公開することは理にかなっています。

これが@Levidadに役立つことを願っています!


こんにちはスティーブン、あなたの入力をありがとう。私はここで別の質問を開きましたが、最終的にはこの質問と同じポイントに到達し、最終的にこの問題に対するCleaner Solutionの試みを思いつきました。見てみて、あなたの考えを共有していただけませんか?私はそれが上記の提案の方向に行くと思います。
Levidad

確かにLevidad、私は見てみましょう!
Steven

1
「Voice of Unreason」と「Erik Eidt」の両方の返信を確認しました。両方とも、あなたがそこに持っている質問について私がコメントするものの線に沿っているので、私は本当にそこに価値を追加することはできません。そして、あなたの質問に答える:あなたがWebAppARである方法は、あなたが共有する「クリーナーソリューション」でセットアップされる方法は、確かに私がアグリゲートの良いアプローチと見なすものの線に沿っています。これがLevidadに役立つことを願っています!
Steven
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.