DDD(领域驱动设计)应用层

8

我一直在尝试构建一个基于DDD的应用程序,但是我有一些问题。

我有一些层: - 表现层 - MVC - 应用层 - 领域层 ...

首先,我想知道是否可以在应用层中执行以下操作(获取家庭信息 > 获取默认消息信息 > 发送电子邮件 > 更新数据库):

public ApproveFamilyOutput ApproveFamily(ApproveFamilyInput input)
        {
            Family family = _familyRepository.GetFamily(input.Username);
            family.Approve();

            DefaultMessage defaultMessage = _defaultMessageRepository.GetDefaultMessage(MessageTypes.FamilyApproved);

            _email.Send(family.GetEmail(), defaultMessage.Subject, defaultMessage.Message);

            _familyRepository.Update(family);
            bool isSaved = _familyRepository.Save();

            return new ApproveFamilyOutput()
            {
                Errors = Helper.GetErrorIfNotSaved(isSaved)
            };
        }

我想确认一下,是应用层负责这项工作吗?

第二个问题是:根据用户的权限,我需要向表示层发送一些数据。这些权限在数据库中定义。例如: - 对象“Family”具有姓名、姓氏、电话号码和电子邮件属性,用户可以显示/隐藏其中的每个值。 我该如何处理这个问题?

我能否在应用层中做类似以下的事情:

public GetFamilyOutput GetFamily(GetFamilyInput input)
        {
            Family family = _familyRepository.GetFamily(input.Username);

            FamilyConfiguration familyConfiguration = _familyConfigurationRepository.GetConfigurations(family.Id);

            //ProcessConfiguration will set to null the properties that I cannot show
            family.ProcessConfiguration(familyConfiguration);

            return new GetFamilyOutput
            {
                //Map Family Object to the GetFamilyOutput
            };
        }

注意:家庭、DefaultMessage和FamilyConfiguration是在域层内创建的领域对象。
你有什么看法?
谢谢 :)
编辑: 注意:我喜欢下面所有的答案,并且我使用了所有答案中的一些内容 :) (我无法将所有答案标记为可接受)
4个回答

4
在#1中,您的应用程序服务所做的是完全有效的:它协调了工作流程,几乎没有业务逻辑知识。
然而,肯定可以进行一些改进,例如:
1. 我没有看到任何事务?电子邮件应仅在成功提交事务后发送。 2. 发送电子邮件可以被视为家庭批准的副作用。我想业务专家可能已经说明:“当家庭获得批准时,通过电子邮件通知有兴趣的各方”。因此,发布FamilyApproved域事件并将电子邮件发送逻辑移动到事件处理程序中可能是明智的。 请注意,您希望在将域事件持久化到磁盘后异步调用处理程序,并且您希望在与聚合相同的事务中持久化事件。 3. 您可能还可以将邮件处理进一步抽象成类似于emailService.send(MessageTypes.FamilyApproved,family.getEmail())的内容。应用程序服务无需了解默认消息。 4. 存储库通常仅限于聚合根(AR),如果DefaultMessage不是AR,则应考虑以不同的方式命名DefaultMessageRepository服务。
至于#2,尽管授权检查可以在域中完成,但通常更常见的是使域摆脱此类任务,并在应用程序层强制执行权限。您甚至可以拥有一个专门支持Identity&Access的有界上下文(BC)。
“// ProcessConfiguration将设置我无法显示的属性为空”
这个解决方案并不好(就像实现IFamilyProperty解决方案一样),因为您的域模型会受到技术授权问题的污染。如果您想要应用DDD,则模型应尽可能忠实于普遍语言(UL),我怀疑IFamilyProperty是您的领域专家会提及或甚至理解的内容。允许属性变为空可能也会违反某些不变量。
这种解决方案的另一个问题是,域模型很少适用于查询(它是为命令构建的),因此直接绕过它并倾向于直接访问DB通常更可取。在域中实现授权将使您难以轻松地做到这一点。
至少出于这些原因,我认为在域外实现授权检查更可取。在那里,您可以自由使用任何符合您需要的实现。例如,我相信从DTO中剥离值可能是合法的。

#1 -> 1. 你说得完全正确!;2. 我喜欢这个,我会去做的。;3. 好的,你是对的,我应该这样做;4. DefaultMessage 不是 AR。那么获取 DefaultMessages 的服务最好的名称是什么?我认为我们应该为所有获取数据的类提供 xxxRepositoty,而不仅仅是 AR。#2 我不确定我想要的是授权规则。我想要的是像(例如)Facebook 上的个人信息一样的东西。我们可以向每个人、我的朋友或没有人显示电子邮件。这就是我想要的。这不是业务规则吗? - ASantos
@ASantos,我通常把这些东西称为“提供程序”。例如,如果我在数据库中有电子邮件模板,我会拥有IEmailMessageRepository(如果电子邮件模板实际上是硬编码的,则完全省略存储库),然后我会实现IMessageProvider接口。然后您可以拥有EMailMessageProvider:IMessageProvider、InstantMessageProvider:IMessageProvider、TestMessageProvider:...等等。 - Philip P.
好的,我没有忘记。我只是先做其他事情,但我会回来并将那些帮助了我的答案标记为已接受 :) - ASantos

2
我也曾经怀疑是否可以将一些逻辑放置在应用服务中。但是,当我阅读了弗拉基米尔·霍里科夫(Vladimir Khorikov)的《域服务与应用服务文章》之后,事情变得更加清晰。该文章指出:

领域服务包含领域逻辑,而应用服务不包含。

并通过很好的例子说明了这个想法。因此,在您的情况下,我认为将这些场景放置在应用服务中完全没有问题,因为它不包含领域逻辑。


我阅读了很多不同的方法,以至于我感到困惑,对最佳方法感到“不确定”。:) 谢谢分享。 - ASantos

1

广告 1
我通常将这个逻辑移动到领域层 - 服务中。
因此,应用层只需调用:

public ApproveFamilyOutput ApproveFamily(ApproveFamilyInput input)
{
    var approveService = diContainer.Get<ApproveService>(); // Or correctly injected by constructor
    var result = approveService.ApproveFamily(input);

    // Convert to ouput
}

域服务(AppproveService类)看起来像:

public ApproveResult ApproveFamily(ApproveFamilyInput input)
{
     var family = _familyRepository.GetFamily(input.Username);
     family.Approve();

     _familyRepository.Update(family);
     bool isSaved = _familyRepository.Save();

     if(isSaved)
         _eventPublisher.Publish(family.raisedEvents);
     // return result
}

为了使其发挥作用(并遵循六边形/洋葱架构),领域层定义其依赖项的所有接口(IFamilyRepository,IDefaultMessageRepository等),应用层将具体实现注入到领域层中。
为了明确:
1. 领域层是独立的
2. 领域对象是纯净的——由实体、值对象组成
3. 领域对象不调用存储库,这取决于领域服务
4. 领域对象引发事件
5. 无关逻辑由事件处理程序处理——例如发送电子邮件,它遵循开闭原则
class FamilyApprovedHandler : IHandle<FamilyApprovedEvent>
{
    private readonly IDefaultMessageRepository _defaultMessageRepository;
    private readonly IEmailSender _emailSender;
    private readonly IEmailProvider _emailProvider;

    // ctor

    public Task Handle(FamilyApprovedEvent event)
    {
        var defaultMessage = _defaultMessageRepository.GetDefaultMessage(MessageTypes.FamilyApproved);

        var email = _emailProvider.Generate(event.Family, defaultMessage.Subject, defaultMessage.Message);

        _emailSender.Send(email);
    }
}

正如我在@plalx的帖子中回答的那样,我喜欢创建事件来处理不相关的逻辑的想法。我会实现它! :) - ASantos

0

关于 #1:

理论上,应用层可以实现您所描述的功能。然而,我个人更喜欢进一步分离关注点:应该有一个持久化层。在您的情况下,开发人员需要知道:

  1. 从存储库获取家庭信息。
  2. 调用方法以批准家庭对象。
  3. 将家庭信息更新回存储库。
  4. 持久化存储库。
  5. 处理任何可能的错误,如果存在持久化错误。

我认为2-3-4应该移动到持久化层,使代码看起来像:

Family family = _familyRepository.GetFamily(input.Username);
family.Approve().Notify(_email);

这种方法在处理错误和一些业务逻辑改进方面提供了更多的灵活性。例如,如果遇到持久化错误,您将不会发送电子邮件。

当然,您需要实现一些附加类型和扩展方法(例如,“Notify()”)。

最后,我认为电子邮件服务也应该使用存储库模式实现(因此您有两个存储库),并具有持久化级别的实现。我的观点是:在应用程序之外持久化的任何内容都需要存储库和持久化实现;电子邮件在用户的邮箱中持久化。

至于第二点:

我强烈建议不要使用可空属性并清除它们。这会让事情变得非常混乱,很难进行单元测试,并且有很多“隐藏”的注意事项。相反,为您的属性实现类。例如:

public class UserPriviledge { //... your db-defined privileges  }

public interface IFamilyProperty<T>
{
    public string PropertyName { get; }
    public T PropertyValue { get; }
    public List<UserPriviledge> ReadPriviledges { get; }
    public bool IsReadOnly { get; }
}

public class FamilyName : IFamilyProperty<string>
{
    public static string PropertyName => "Name";
    public string PropertyValue { get; }
    public List<UserPriviledge> ReadPriviledges { get; }
    public bool IsReadOnly { get; private set; }

    public FamilyName(string familyName) {
        this.PropertyValue = familyName;
        this.ReadPriviledges.Add(someUserPrivilege);
        this.IsReadOnly = false;
    }

    public void MakeReadOnly() {
        this.IsReadOnly = true;
    }
}

public class Family
{
     public int Id { get; }
     public List<IFamilyProperty> LimitedProperties { get; }
}

通过这种实现方式,您可以拥有与混淆值不同的方法,而是删除值或应用更复杂的逻辑:

public void ApplyFamilyPermissions(Family family, UserEntity user)
{
    foreach (var property in family.LimitedProperties) {
        if (property.ReadPriviledges.Intersect(user.Priviledges).Any() == false) {
             family.LimitedProperties.Remove(property);
        } else if (property.IsReadOnly == false && HasPropertyWriteAccess(property, user) == false) {
             property.MakeReadOnly();
        }
    }
}

注意:代码未经验证,我相信其中有一些语法错误,但我认为它清楚地传达了思想。


谢谢你的回答 :) 关于第二个答案,我很喜欢,我会按照你说的实现它。 关于第一个答案,理论上我明白了,但在实践中,我有一些疑问:
  • notify方法只是一个扩展方法,我知道它是如何工作的,但Approve方法,我不明白你想说什么。
Approve()方法在Family对象中,对吗?我如何在Approve方法内持久化数据?我需要像这样通过Approve方法传递_familyRepository实例吗:
  • family.Approve(_familyRepository).Notify(_email);?
- ASantos
@ASantos 如果您不使用依赖注入,那么您必须传递存储库。但是,使用依赖注入,您将在具有Family扩展的类中注入存储库。唯一需要避免的情况是当您的“Family”可以驻留在不同的存储库中时(这似乎不是这种情况)。对于Notify()来说情况正好相反 - 我假设可能会有不同的通知方法,每个方法都有自己的存储库(其中每个通知存储库都实现相同的接口),因此传递_email。 - Philip P.
@PhilP。是的,但是IFamilyProperty也是授权代码,它是域的一部分,但根本不是域概念。如果域模型不以任何方式参与授权解决方案,那将会更好。 - plalx
@plalx,我非常不同意IFamilyProperty不是领域的一部分这个观点。领域应该描述实际实体及其属性。我同意,“IsReadOnly”属性应该被删除(我从我的项目中复制了现有代码);“Family”是一个业务/现实实体。家庭有属性,我们只想让它们以某种方式运作。领域应该使行为成为可能。逻辑层(或服务层)应该描述和实现这些行为和规则。归根结底:代码清晰易懂比优雅更重要。 - Philip P.
顺便说一句,我喜欢这个事件的想法,我一定会实现它! :) - ASantos
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接