富域模型-行为如何准确地适应?


84

在Rich与Anemic领域模型的辩论中,互联网充满了哲学上的建议,但缺乏权威的例子。这个问题的目的是找到适当的领域驱动设计模型的明确指南和具体示例。(理想情况下为C#。)

对于一个实际示例,这种DDD实现似乎是错误的:

下面的WorkItem域模型不过是属性包,由Entity Framework用于代码优先数据库。按照福勒的说法,这是贫血的

WorkItemService层显然是对域服务的常见误解。它包含WorkItem的所有行为/业务逻辑。Per Yemelyanov等人认为,这是程序性的。(第6页)

因此,如果以下内容是错误的,我该怎么做呢?
该行为,即AddStatusUpdateCheckout,应该属于WorkItem类吗?
WorkItem模型应具有哪些依赖关系?

在此处输入图片说明

public class WorkItemService : IWorkItemService {
    private IUnitOfWorkFactory _unitOfWorkFactory;

    //using Unity for dependency injection
    public WorkItemService(IUnitOfWorkFactory unitOfWorkFactory) {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void AddStatusUpdate(int workItemId, int statusId) {

        using (var unitOfWork = _unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()) {
            var workItemRepo = unitOfWork.WorkItemRepository;
            var workItemStatusRepo = unitOfWork.WorkItemStatusRepository;

            var workItem = workItemRepo.Read(wi => wi.Id == workItemId).FirstOrDefault();
            if (workItem == null)
                throw new ArgumentException(string.Format(@"The provided WorkItem Id '{0}' is not recognized", workItemId), "workItemId");

            var status = workItemStatusRepo.Read(s => s.Id == statusId).FirstOrDefault();
            if (status == null)
                throw new ArgumentException(string.Format(@"The provided Status Id '{0}' is not recognized", statusId), "statusId");

            workItem.StatusHistory.Add(status);

            workItemRepo.Update(workItem);
            unitOfWork.Save();
        }
    }
}

(此示例已简化为更易于阅读。代码肯定仍然很笨拙,因为这是一个令人困惑的尝试,但是域行为是:通过将新状态添加到存档历史记录来更新状态。最终我同意其他答案,这只能由CRUD处理。)

更新资料

@AlexeyZimarev给出了最佳答案,这是Jimmy Bogard用C#编写的关于C#的完美视频,但由于在链接之外没有提供足够的信息,因此显然在下面进入了注释。我在下面的回答中对笔记进行了粗略的草稿,总结了视频。请随时对答案进行任何更正,以发表评论。该视频长达一个小时,但非常值得观看。

更新-2年后

我认为这是DDD刚刚成熟的标志,即使研究了2年,我仍然不能保证我知道这样做的“正确方法”。无处不在的语言,聚合的根源以及其行为驱动设计的方法是DDD对行业的宝贵贡献。持续性的无知和事件源导致混乱,我认为这样的哲学使它无法被广泛采用。但是,如果我不得不以我所学到的方式再次编写此代码,我认为它将看起来像这样:

在此处输入图片说明

我仍然欢迎这篇(非常活跃的)帖子提供任何答案,这些答案提供了有效域模型的所有最佳实践代码。


6
当你告诉他们时,所有的哲学理论都落在了地上"I don't want to duplicate all my entities into DTOs simply because I don't need it and it violates DRY, and I also don't want my client application to take a dependency on EntityFramework.dll"。实体框架术语中的“实体”与“域模型”中的“实体”不同
Federico Berasategui 2013年

如果可以的话,我可以使用自动工具(例如Automapper)将域实体复制到DTO中。我只是不确定到底该怎么办。
RJB

16
我建议您在Vimeo上观看Jimmy Bogard的NDC 2012会议“制作邪恶域模型” 。他解释了富域应该是什么,以及如何通过实体中的行为在现实生活中实现它们。示例非常实用,所有示例均使用C#。
Alexey Zimarev

谢谢,我正在观看视频,目前为止还很完美。我知道,如果这是错误的话,那么某个地方肯定会有一个“正确”的答案
。...– RJB

2
我也要求爱Java:/
uylmz '16

Answers:


59

最有帮助的答案是阿列克谢·齐马列夫(Alexey Zimarev)给出的,至少得到7票赞成,之后主持人将其移至我的原始问题下方的评论中。

他的回答:

我建议您在Vimeo上观看Jimmy Bogard的NDC 2012会议“制作邪恶域模型”。他解释了富域应该是什么,以及如何通过实体中的行为在现实生活中实现它们。示例非常实用,所有示例均使用C#。

http://vimeo.com/43598193

我记了一些笔记来总结视频,这对我的团队既有好处,又在本文中提供了一些即时细节。(该视频长达一个小时,但是如果您有时间的话,确实值得每一分钟。JimmyBogard的解释值得称赞。)

  • “对于大多数应用程序……我们不知道启动时它们会变得复杂。它们只是以这种方式出现。”
    • 随着代码和需求的增加,复杂性自然增长。应用程序可以像CRUD一样非常简单地开始,但是行为/规则可以融入其中。
    • “令人高兴的是我们不必从复杂开始。我们可以从贫瘠的领域模型开始,那就是财产包,而只要有了标准的重构技术,我们就可以迈向真正的领域模型。”
  • 域模型=业务对象。域行为=业务规则。
  • 行为通常隐藏在应用程序中-可以存在于PageLoad,Button1_Click或“ FooManager”或“ FooService”之类的帮助器类中。
  • 与域对象分开的业务规则“要求我们记住”那些规则。
    • 在上面的我的个人示例中,一项业务规则是WorkItem.StatusHistory.Add()。我们不仅在更改状态,还在将其存档以进行审核。
  • 域行为“比编写大量测试要容易得多,可以消除应用程序中的错误”。测试要求您知道编写这些测试。域行为为您提供了正确的测试路径
  • 域服务是“帮助程序类,用于协调不同域模型实体之间的活动”。
    • 域服务!=域行为。实体具有行为,域服务只是实体之间的中介。
  • 域对象不应拥有所需的基础结构(即IOfferCalculatorService)。基础结构服务应传递到使用它的域模型中。
  • 领域模型应该告诉您他们可以做什么,而他们只能做到这些事情。
  • 域模型的属性应由私有设置器保护,以便只有模型可以通过自身的行为来设置其自身的属性。否则,它是“混杂的”。
  • 贫血领域模型对象,仅仅是ORM的属性包,仅是“薄薄板-数据库上的强类型版本”。
    • “将数据库行放入对象非常容易,这就是我们所拥有的。”
    • '大多数持久对象模型就是这样。贫血领域模型与没有真正行为的应用程序的区别在于,对象是否具有业务规则,但在领域模型中找不到这些规则。'
  • “对于许多应用程序,并不需要真正构建任何类型的真实业务应用程序逻辑层,它只是可以与数据库对话的对象,并且可能是表示其中数据的简单方法。”
    • 因此,换句话说,如果您所要做的只是没有特殊业务对象或行为规则的CRUD,则不需要DDD。

请随意评论您认为应该包含的任何其他要点,或者如果您认为其中任何注释不合时宜。尝试尽可能直接引用或解释。


很棒的视频,特别是了解重构在工具中的工作方式。关于域对象的正确封装(以确保它们是一致的)有很多。他在告知要约,成员等方面的业务规则方面做得很出色。他多次提到“不变式”一词(这是基于合同的域建模)。我希望.net代码能更好地传达什么是正式的业务规则,因为那些更改需要维护。
Fuhrmanator '16

6

您的问题无法回答,因为您的示例是错误的。具体来说,因为没有行为。至少不在您域的范围内。AddStatusUpdate方法的示例不是域逻辑,而是使用该域的逻辑。在某种内部处理外部请求的服务中,这种逻辑确实有意义。

例如,如果要求一个特定的工作项只能具有特定的状态,或者只能具有N个状态,则这是域逻辑,并且应属于WorkItemStatusHistory作为方法的一部分。

造成混淆的原因是,您正在尝试将不需要的准则应用到代码中。仅当您具有许多复杂的域逻辑时,域模型才有意义。例如。对实体本身起作用并源于需求的逻辑。如果代码是关于从外部数据操作实体的,那么这很可能不是域逻辑。但是,当您if根据要使用的数据和实体获得大量s时,这就是域逻辑。

真正的领域建模的问题之一是它与管理复杂的需求有关。因此,它的真正力量和好处无法在简单的代码上展示出来。您需要数十个具有大量需求的实体,才能真正看到收益。同样,您的示例过于简单,以至于领域模型无法真正发挥作用。

最后,我要提到的一些OT问题是,使用实体框架很难真正拥有真正的OOP设计的领域模型。尽管将ORM设计为将真正的OOP结构映射到关系OM,但是仍然存在许多问题,并且关系模型经常会泄漏到OOP模型中。即使使用我认为比EF功能强大得多的nHibernate,这也可能是一个问题。


好点。那么,数据或基础架构中的另一个项目中的AddStatusUpdate方法将属于什么位置?什么是理论上可能属于WorkItem的行为的示例?任何伪代码或模型将不胜感激。我的示例实际上已简化为更具可读性。还有其他实体,例如AddStatusUpdate有一些额外的行为-它实际上采用状态类别名称,如果该类别不存在,则创建该类别。
RJB

@RJB就像我说的那样,AddStatusUpdate是使用域的代码。因此,无论是某种Web服务还是使用域类的应用程序。就像我说的那样,您不能指望任何形式的模型或伪代码,因为您将需要使整个项目的复杂性足够大,以展示OOP域模型的真正优势。
欣快的

5

您假设将与WorkItem相关联的业务逻辑封装到“胖服务”中是一种固有的反模式,我认为这不一定。

不管您对贫乏的域模型有何想法,Business Line .NET应用程序的典型标准模式和实践都会鼓励由各种组件组成的事务分层方法。他们鼓励将业务逻辑与域模型分开,以专门促进其他.NET组件以及不同技术堆栈或物理层上的组件之间的通用域模型通信。

一个示例是基于.NET的SOAP Web服务,该服务与Silverlight客户端应用程序通信,该应用程序恰好具有包含简单数据类型的DLL。该域实体项目可以内置到.NET程序集或Silverlight程序集中,在该程序集中,具有此DLL的感兴趣的Silverlight组件将不会暴露于仅依赖于该服务可用组件的对象行为。

不管您对这场辩论的立场如何,这都是Microsoft提出的采用和接受的模式,而且根据我的专业意见,这并不是错误的方法,但是定义自己行为的对象模型也不一定是反模式。如果继续进行此设计,则最好是认识并理解如果需要与需要查看域模型的其他组件集成时可能遇到的一些局限性和痛点。在这种特定情况下,也许您可​​能希望让Translator将面向对象的样式域模型转换为不公开某些行为方法的简单数据对象。


1
1)如何将业务逻辑与域模型分开?它是此业务逻辑所在的领域;该域中的实体正在执行与该业务逻辑关联的行为。现实世界没有服务,也不存在于领域专家的脑海中。2)希望在整合与您需要建立自己的域模型,因为它需要的任何成分有所不同,这就会对你的域模型有不同的看法。长期以来,您可以创建一个可以共享的域模型。
Stefan Billiet

1
@StefanBilliet这些关于通用域模型的谬论是很不错的,但是像我之前所做的那样,可以在更简单的组件和组件交互中实现。我的观点是,领域模型之间的转换逻辑会产生很多繁琐的样板代码,如果可以安全地避免使用,那么这将是一个不错的设计选择。
maple_shaft

1
坦率地说,我认为唯一好的设计选择是业务专家可以推理的模型。您正在建立一个领域模型,供企业用来解决该领域内的某些问题。将行为从域实体拆分为服务会使每个参与人员感到困难,因为您必须不断地将域专家所说的内容映射到与当前对话几乎没有任何相似之处的服务代码。以我的经验,与输入样板文件相比,您浪费的时间更多。这并不是说没有办法解决锅炉课程的守则。
Stefan Billiet 2013年

@StefanBilliet在一个完美的世界中,我同意您的看法,在这里,业务专家有时间与开发人员坐下来。软件行业的现实情况是,业务专家没有时间或兴趣参与此级别或更糟的事情,但是开发人员只能在模糊的指导下解决它。
maple_shaft

是的,但这不是接受这一现实的理由。继续这样的追求就是浪费开发商的时间(可能还有声誉)和客户的钱。我描述的过程是一个需要随着时间而建立的关系。这需要付出很多努力,但会产生更好的结果。有一个原因通常认为“无处不在的语言”是DDD的最重要方面。
Stefan Billiet 2013年

5

我知道这个问题已经很老了,所以这个答案是给后代的。我想用一个具体的例子代替基于理论的例子。

将“更改工作项状态”封装在WorkItem类上,如下所示:

public SomeStatusUpdateType Status { get; private set; }

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Maybe we designed this badly at first ;-)
    Status = status;       
}

现在,您的WorkItem班级有责任保持自己的合法状态。但是,该实现非常薄弱。产品负责人希望获得对的所有状态更新的历史记录WorkItem

我们将其更改为以下内容:

private ICollection<SomeStatusUpdateType> StatusUpdates { get; private set; }
public SomeStatusUpdateType Status => StatusUpdates.OrderByDescending(s => s.CreatedOn).FirstOrDefault();

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Better...
    StatusUpdates.Add(status);       
}

实现已发生了巨大的变化,但是ChangeStatus方法的调用者没有意识到底层的实现细节,也没有理由自行更改。

这是富域模型实体恕我直言的示例。

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.