在Rich与Anemic领域模型的争论中,互联网充满了哲学上的建议,但缺乏权威性的例子。这个问题的目的是找到适当的领域驱动设计模型的明确指南和具体示例。 (理想情况下,在C#中。)对于一个实际示例,DDD的这种实现似乎是错误的:

下面的WorkItem域模型不过是属性包,由Entity Framework提供的代码优先数据库。根据福勒,这是贫血的。

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

因此,如果以下错误,该如何纠正?
该行为(即AddStatusUpdate或Checkout)应该正确属于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处理。)

Update

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

更新-2年后

我认为这是DDD刚刚成熟的标志,即使研究了2年,我仍然不能保证我知道这样做的“正确方法”。无处不在的语言,聚合的根源以及其行为驱动设计的方法是DDD对行业的宝贵贡献。持续性的无知和事件源导致混乱,我认为这样的哲学使它无法被广泛采用。但是,如果我不得不根据自己的经验重新编写这段代码,我想它看起来应该像这样:这篇(非常活跃的)帖子提供了有效域模型的所有最佳实践代码。

评论

当您告诉他们“我不想将我的所有实体复制到DTO中只是因为我不需要它并且它违反了DRY,并且我也不希望我的客户应用程序执行所有操作时,所有哲学理论都落在了地上。依赖EntityFramework.dll”。实体框架术语中的“实体”与“域模型”中的“实体”不同

如果可以的话,我可以使用自动工具(例如Automapper)将域实体复制到DTO中。我只是不确定一天结束后的样子。

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

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

我也要求爱Java:/

#1 楼

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

他的回答:


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

http://vimeo.com/43598193


我记了一些笔记来总结视频,以使我的团队受益,并提供一些即时细节在这篇文章中。 (该视频长达一个小时,但是如果您有时间的话,确实值得每一分钟。Jimmy Bogard的解释值得赞扬。)


“对于大多数应用程序...我们不知道当我们开始时它们会变得很复杂。它们会变成那样。”


随着代码和需求的添加,复杂度自然增长。应用程序可以像CRUD一样非常简单地开始,但是行为/规则可以融入其中。包,只需使用标准的重构技术,我们就可以朝着真正的领域模型发展。”


域模型=业务对象。域行为=业务规则。
行为通常隐藏在应用程序中-可以在PageLoad,Button1_Click中,也可以在助手类(如“ FooManager”或“ FooService”)中。从域对象“要求我们记住”这些规则。在我上面的个人示例中,一个业务规则是WorkItem.StatusHistory.Add()。我们不仅在更改状态,还在将其存档以进行审核。


域行为“比编写大量测试要容易得多,消除了应用程序中的错误”。测试要求您知道要编写那些测试。域行为为您提供了正确的测试路径。
域服务是“帮助程序类,用于协调不同域模型实体之间的活动。”


域服务!=域行为。实体具有行为,域服务只是实体之间的中介。


域对象不应该拥有它们所需的基础结构(即IOfferCalculatorService)。基础结构服务应该传递给使用它的域模型。
域模型应该提供告诉您它们可以做什么,并且它们只能执行那些事情。
域的属性模型应使用私有设置器来保护,以便只有模型可以通过自己的行为来设置自己的属性。否则,它是“混杂的”。
作为ORM的属性包的贫血域模型对象仅是“薄薄单板-数据库上的强类型版本。”


“将数据库行放入对象非常容易,这就是我们所拥有的。”
'大多数持久性对象模型就是这样。贫血领域模型与没有真正行为的应用程序的区别在于,对象是否具有业务规则,但在领域模型中找不到这些规则。'


”对于许多应用程序,实际上并不需要构建任何类型的真实业务应用程序逻辑层,它只是可以与数据库进行通信的对象,并且可能是表示其中数据的简单方法。”


,换句话说,如果您所要做的只是没有特殊业务对象或行为规则的CRUD,则不需要DDD。 >请随意评论您认为应该包括的其他要点,或者如果您认为其中任何注释不合时宜。尝试直接引用或尽可能多地解释。

评论


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

–负责人
16年12月30日18:00

像往常一样晚到聚会。我的看法是“如果您只是在做CRUD”,那就是太多的人提出了这种观点并将所有内容强加给它,即使它不再是真的。

–托马斯·爱德(Thomas Eyde)
20-2-10在15:37



“基础结构服务应传递到使用它的域模型中。”我不确定它是正确的,如果是这样,那么您的模型不是纯@rjb

– Alireza Rahmani
20/12/04在16:29

#2 楼

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

例如,如果要求特定工作项只能具有特定状态,或者可以仅具有N个状态,则这是域逻辑,应作为方法之一作为WorkItemStatusHistory的一部分。不需要它。仅当您具有许多复杂的域逻辑时,域模型才有意义。例如。对实体本身起作用并源于需求的逻辑。如果代码是关于从外部数据操作实体的,那么这很可能不是域逻辑。但是当您根据要使用的数据和实体获得大量if时,这就是领域逻辑。

真正的领域建模的问题之一就是要管理复杂的需求。因此,它的真正力量和好处无法在简单的代码上展示出来。您需要数十个具有大量需求的实体,才能真正看到收益。再次说明,您的示例太简单了,以至于领域模型无法真正发挥作用。 。尽管将ORM设计为将真正的OOP结构映射到关系OM,但是仍然存在许多问题,并且关系模型经常会泄漏到OOP模型中。即使使用我认为比EF功能强大得多的nHibernate,这也可能是一个问题。

评论


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

– RJB
13年10月7日在17:20

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

– up
13年10月7日在20:36

#3 楼

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

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

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

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

评论


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

– Stefan Billiet
13年11月27日在13:04

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

– Maple_shaft♦
13年11月27日在14:41

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

– Stefan Billiet
13年11月27日在15:17

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

– Maple_shaft♦
13年11月27日在16:18

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

– Stefan Billiet
13年11月27日17:00

#4 楼

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

将“更改工作项状态”封装在WorkItem类上,如下所示: />
现在,您的WorkItem类负责维护自己的合法状态。但是,该实现非常薄弱。产品负责人希望获得对WorkItem所做的所有状态更新的历史记录。

我们将其更改为以下形式:

public SomeStatusUpdateType Status { get; private set; }

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


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

这是富域模型实体IMHO的示例。