如何将DDD的某些概念应用于实际代码?里面的具体问题


9

我一直在研究DDD,目前正在努力寻找一种在实际代码中应用这些概念的方法。我在N层有大约10年的经验,所以我苦苦挣扎的原因很可能是我的思维模型与该设计太过相关。

我创建了一个Asp.NET Web应用程序,并从一个简单的域开始:Web监视应用程序。要求:

  • 用户必须能够注册一个新的Web App进行监视。该Web应用程序具有友好的名称并指向URL。
  • 该网络应用将定期轮询状态(在线/离线);
  • 该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().
    }
}

还有一个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 '18年

1
这个问题可能更适合codereview.stackexchange.com
VoiceOfUnreason

2
我本人喜欢您,并花费大量时间在n层应用程序上。我仅从书籍,论坛等中了解DDD,所以我只发表评论。验证有两种类型:输入验证和业务规则验证。输入验证进入应用层,而域验证进入域层。WebApp看起来更像是实体而不是集合,WebAppService看起来更像是应用程序服务,而不是DomainService。另外,您的汇总还引用了涉及基础设施的容器。它看起来也像服务定位器。
Adrian Iftode

1
是的,因为它没有为关系建模。聚合正在建模领域对象之间的关系。Web应用程序只有原始数据和一些行为,可能涉及例如具有下列不变:是不正常更新的版本像疯了似的即steping 3版本时,当前版本为1
阿德里安Iftode

1
只要ValueObject具有实现实例之间相等性的方法,我认为就可以。在您的方案中,您可以创建一个Version值对象。检查语义版本,您将获得有关如何建模此值对象的许多想法,包括不变式和行为。WebApp不应与存储库进行通信,实际上,我相信从您的项目中直接或间接(通过接口)获取包含域内容的与基础架构(存储库,工作单元)相关的任何其他内容的引用都是安全的。
Adrian Iftode

Answers:


1

关于您的WebApp总体意见,长期以来,我完全同意在repository这里采用“引入” 不是正确的方法。以我的经验,聚合将根据操作的自身状态做出“是否可以执行”的“决定”。因此,未处于打开状态时,它可能会从其他服务中退出。如果您需要进行此类检查,通常会将其移至调用聚合的服务(在您的示例中为WebAppService)。

此外,您可能会遇到多个应用程序要同时调用聚合的用例。如果发生这种情况,则在执行这样的出站呼叫(这可能需要很长时间)的同时,就将您的聚合阻止用于其他用途。这最终会减慢聚合处理的速度,我认为这也不可取。

因此,尽管如果您进行一下验证,您的汇总似乎会变得很稀疏,但我确实认为将其移至会更好WebAppService

我还建议将WebAppRegistered事件的发布移到您的汇总中。聚合是要创建的人,因此,如果创建过程成功,则可以将其发布给世界。

希望这可以帮助您@Levidad!


嗨,史蒂文,谢谢您的输入。我在这里提出了另一个问题,最终达到了与该问题相同的目的,最后我针对此问题提出了“更清洁的解决方案”的尝试。您可以看看并分享您的想法吗?我认为这符合您上面建议的方向。
莱维达德

好的,莱维达,我来看一下!
史蒂文

1
我刚刚检查了“不通之声”和“埃里克·埃德”的两个答复。两者都符合我对您所提出的问题的看法,因此我无法真正在此处增加价值。而且,要回答您的问题:在您所WebApp共享的“更干净的解决方案”中设置AR 的方式确实符合我认为是聚合的一种好方法。希望这可以帮助您解决Levidad!
史蒂文
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.