DDD-实体不能直接访问存储库的规则


183

在领域驱动设计,似乎有很多协议,任何单位不应该访问存储库直接。

这是来自Eric Evans的“ 域驱动设计”书,还是来自其他地方?

背后的原因哪里有很好的解释?

编辑:澄清:我不是在谈论将数据访问从业务逻辑分离到单独层的经典OO做法-我是在谈论在DDD中实体不应该与数据交谈的特定安排根本没有访问层(即,它们不应保存对存储库对象的引用)

更新:我给了BacceSR赏金,因为他的回答似乎最接近,但是我对此仍然很不了解。如果它是如此重要的原理,那么肯定在某处在线有关于它的好文章吗?

更新:2013年3月,对该问题的投票暗示对此有很多兴趣,尽管有很多答案,但我仍然认为,如果人们对此有想法,还有更多的余地。


看看我的问题stackoverflow.com/q/8269784/235715,它显示了一种情况,即在没有实体访问存储库的情况下很难捕获逻辑。虽然我认为实体不应该访问存储库,但是可以解决这种情况,即无需存储库引用就可以重写代码,但是目前我还没有想到。
Alex Burtsev 2011年

不知道它来自哪里。我的想法:我认为这种误解来自于人们如何不理解DDD的全部含义。此方法不是用于实现软件,而是用于设计软件(领域..设计)。过去,我们有架构师和实施者,但现在只有软件开发人员。DDD是为建筑师设计的。当架构师设计软件时,他需要一些工具或模式来代表将实施准备好的设计的开发人员的内存或数据库。但是,设计本身(从业务角度而言)没有或需要存储库。
berhalak

Answers:


47

这里有些混乱。存储库访问聚合根。聚合根是实体。这样做的原因是关注点分离和良好的分层。在小型项目中这没有意义,但是如果您在大型团队中,您会说:“您可以通过Product Repository访问产品。Product是实体集合(包括ProductCatalog对象)的集合根。如果要更新ProductCatalog,则必须遍历ProductRepository。”

这样,您就可以非常清楚地区分业务逻辑和更新内容。您没有一个孩子会自己一个人写完整的程序,将所有这些复杂的事情写到产品目录中,而要把它集成到上游项目中,您就坐在那里看着它并意识到它一切都必须放弃。这也意味着当人们加入团队,添加新功能时,他们知道去哪里以及如何构建程序。

可是等等!像“存储库模式”一样,存储库也指持久层。在一个更好的世界中,埃里克·埃文斯(Eric Evans)的存储库和存储库模式将具有单独的名称,因为它们往往会重叠很多。要获得存储库模式,您需要使用服务总线或事件模型系统来与其他访问数据的方式进行对比。通常,当您达到这一级别时,Eric Evans的存储库定义就会绕开,您便开始谈论有限的上下文。每个有界上下文本质上都是其自己的应用程序。您可能具有完善的审批系统,可以将产品放入产品目录。在您的原始设计中,产品是核心,但在这种有限的上下文中,产品目录是。您仍然可以通过服务总线访问产品信息并更新产品,

回到您的原始问题。如果您要从实体内部访问存储库,则意味着该实体实际上不是业务实体,而是可能应在服务层中存在的实体。这是因为实体是业务对象,因此应尽可能使自己像DSL(特定于域的语言)一样。在这一层中只有业务信息。如果您要对性能问题进行故障排除,那么您将知道其他地方,因为此处仅应包含业务信息。如果突然之间,您在这里遇到应用程序问题,那么您将很难扩展和维护应用程序,这的确是DDD的核心:开发可维护的软件。

对评论1的回应:是的,很好的问题。因此,并非所有验证都发生在域层中。夏普有一个“ DomainSignature”属性可以满足您的需求。它具有持久性,但是作为属性可以使域层保持干净。它可以确保您没有名称相同的重复实体。

但是,让我们谈谈更复杂的验证规则。假设您是Amazon.com。您是否曾经使用过期的信用卡订购过商品?我有,但我还没有更新卡和买东西。它接受订单,并且UI通知我所有东西都是桃红色的。大约15分钟后,我会收到一封电子邮件,说我的订单有问题,我的信用卡无效。理想情况是,在域层中进行了一些正则表达式验证。这是正确的信用卡号吗?如果是,则继续执行订单。但是,在应用程序任务层还需要进行其他验证,在该层中,将查询外部服务以查看是否可以在信用卡上付款。如果没有,则实际上不运送任何东西,暂停订单并等待客户。

不要害怕在可以访问存储库的服务层创建验证对象。只需将其放在域层之外即可。


15
谢谢。但是我应该努力将尽可能多的业务逻辑引入实体(及其关联的工厂和规范等)中,对吗?但是,如果不允许它们通过存储库获取数据,我应该如何编写任何(合理复杂的)业务逻辑?例如:聊天室用户不允许将其名称更改为其他人已经使用的名称。我希望该规则由ChatUser实体内置,但是如果您不能从那里访问存储库,则执行起来并不容易。所以我该怎么做?
codeulike 2011年

我的回复超出了评论框的允许范围,请参见编辑。
kertosis 2011年

6
您的实体应该知道如何保护自己免受伤害。这包括确保它不会进入无效状态。与聊天室用户一起描述的是业务逻辑,该业务逻辑是实体必须保持自身有效的逻辑的补充。诸如您想要的东西之类的业务逻辑真正属于Chatroom服务,而不是ChatUser实体。
亚历克

9
谢谢亚历克。那是表达它的明确方法。但是在我看来,Evans的以领域为中心的“所有业务逻辑都应进入领域层”的黄金法则与“实体不应该访问存储库”的原则相冲突。如果我理解为什么会这样,我可以接受,但是我找不到关于实体为什么不应该访问存储库的任何很好的解释。Evans似乎没有明确提及。它从哪里来的?如果您可以发布指向一些好的文学作品的答案,则可以为自己赢得50分的赏金
:)

4
“他在小事上没有道理”这是团队犯的一个大错误……这是一个小项目,因此我可以做到这一点……别再这样思考了。由于业务需求,我们合作的许多小型项目最终都会变得庞大。如果您做的事情大小不一,那就正确吧。
MeTitus

35

最初,我是有说服力的,允许我的某些实体访问存储库(即没有ORM的延迟加载)。后来我得出结论,我不应该这样做,并且可以找到其他方法:

  1. 我们应该知道请求中的意图以及从域中获得什么,因此我们可以在构造或调用聚合行为之前进行存储库调用。这也有助于避免内存状态不一致和延迟加载的需求(请参阅本文)。气味是您不能再创建实体的内存实例而不用担心数据访问。
  2. CQS(命令查询分离)可以帮助减少为实体中的事物调用存储库的需求。
  3. 我们可以使用规范来封装和传达域逻辑需求,并将其传递给存储库(服务可以为我们编排这些东西)。规范可以来自负责维护该不变性的实体。存储库将把规范的某些部分解释为它自己的查询实现,并将规范中的规则应用于查询结果。这旨在将域逻辑保留在域层中。它还为无所不在的语言和交流提供了更好的服务。想象一下说“过期订单说明”与说“从tbl_order过滤器订单,其中place_at在sysdate前不到30分钟”(请参阅​​此答案)。
  4. 由于违反了单一责任原则,这使得对实体行为的推理变得更加困难。如果您需要解决存储/持久性问题,则知道该去哪里,不去哪里。
  5. 它避免了授予实体双向访问全局状态(通过存储库和域服务)的危险。您也不想打破交易界限。

我所知道的红色书《实现域驱动设计》中的弗农·沃恩在两个地方都提到了这个问题(请注意:这本书得到了埃文斯的完全认可,您可以在前言中进行阅读)。在关于服务的第7章中,他使用域服务和规范来解决需要使用聚合存储库的聚合和使用另一个聚合来确定用户是否已通过身份验证的需求。引用他的话说:

根据经验,如果可能的话,我们应该尽量避免在聚合内使用存储库(12)。

弗农·沃恩(2013年2月6日)。实施域驱动设计(Kindle Location 6089)。培生教育。Kindle版。

他在第10章“聚合”的标题为“模型导航”部分中说道(就在他建议使用全局唯一ID引用其他聚合根之后):

通过身份引用并不能完全阻止在模型中导航。有些人将使用Aggregate内部的存储库(12)进行查找。这种技术称为“断开域模型”,实际上是一种延迟加载的形式。但是,有一种不同的建议方法:使用存储库或域服务(7)在调用聚合行为之前查找依赖对象。客户端应用程序服务可以控制它,然后分派给聚合:

他继续在代码中显示此示例:

public class ProductBacklogItemService ... { 

   ... 
   @Transactional 
   public void assignTeamMemberToTask( 
        String aTenantId, 
        String aBacklogItemId, 
        String aTaskId, 
        String aTeamMemberId) { 

        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( 
                                        new TenantId( aTenantId), 
                                        new BacklogItemId( aBacklogItemId)); 

        Team ofTeam = teamRepository.teamOfId( 
                                  backlogItem.tenantId(), 
                                  backlogItem.teamId());

        backlogItem.assignTeamMemberToTask( 
                  new TeamMemberId( aTeamMemberId), 
                  ofTeam,
                  new TaskId( aTaskId));
   } 
   ...
}     

他接着还提到了如何将域服务与double-dispatch一起用于Aggregate命令方法中的另一种解决方案。(我不能推荐足够多的方法来阅读他的书。在您厌倦了无休止地在互联网上翻阅后,请花钱买本书。)

然后,我与总是客气的Marco Pivetta @Ocramius进行了讨论,向我展示了一些有关从域中提取规范并使用该规范的代码:

1)不建议这样做:

$user->mountFriends(); // <-- has a repository call inside that loads friends? 

2)在域服务中,这很好:

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ 
    $user = $this->users->get($mount->userId()); 
    $friends = $this->users->findBySpecification($user->getFriendsSpecification()); 
    array_map([$user, 'mount'], $friends); 
}

1
问题:我们总是被教导不要在无效或不一致的状态下创建对象。当您从存储库加载用户时,然后getFriends()在执行任何其他操作之前先调用它,则它将为空或延迟加载。如果为空,则此对象位于并且处于无效状态。有什么想法吗?
Jimbo'9

存储库调用域以新建实例。如果不通过域,就不会获得User的实例。这个答案解决的问题是相反的。域引用存储库的位置,应避免这种情况。
prograhammer '19

27

这是一个很好的问题。我期待对此进行一些讨论。但是我认为在几本DDD书以及Jimmy nilssons和Eric Evans中都提到了它。我想通过示例也可以看到如何使用存储库模式。

但是让我们讨论。我认为一个非常有效的想法是,为什么一个实体应该知道如何持久另一个实体?对于DDD而言,重要的是每个实体都有责任管理自己的“知识范围​​”,并且不应该对如何读写其他实体一无所知。当然,您可以只向实体A添加一个存储库接口以读取实体B。但是风险是您公开了有关如何持久化B的知识。实体A在将B持久化为db之前也会对B进行验证吗?

如您所见,实体A可以更多地参与实体B的生命周期,并且可以为模型增加更多的复杂性。

我猜(没有任何示例)单元测试将更加复杂。

但是我敢肯定,在某些情况下,您很想通过实体使用存储库。您必须查看每种情况才能做出有效的判断。优点和缺点。但是我认为存储库实体解决方案始于很多缺点。专业人员必须平衡的缺点是非常特殊的情况。


1
好点子。我猜想,老式的领域模型可能会让实体B在让自己持久之前进行验证。您确定Evans提到实体未使用存储库吗?我已经
读完

好吧,我几年前读过这本书(3年级...),我的记忆使我失望。我不记得他是否说了这句话,但我相信他通过例子说明了这一点。您还可以在dddsamplenet.codeplex.com上找到有关他的Cargo示例的社区解释(来自他的书)。下载代码项目(查看Vanilla项目-书中的示例)。您会发现存储库仅在应用程序层中用于访问域实体。
Magnus Backeus 2011年

1
从下载书的DDD SmartCA例如 p2p.wrox.com/...你会看到另一种方法(虽然这是一个RIA Windows客户端),其中仓库在服务中使用(任何异常),但服务是内部的entites用途。这是我不会做的事情,但我是webb应用程序专家。给定SmartCA应用程序的场景,您必须能够脱机工作,因此ddd设计的外观可能会有所不同。
Magnus Backeus 2011年

SmartCA示例听起来很有趣,它在哪一章中?(代码下载按章安排)
codeulike 2011年

1
@codeulike我目前正在使用ddd概念设计和实现框架。有时进行验证需要访问数据库并进行查询(例如:查询多列唯一索引检查)。关于此以及查询应在存储库层中编写的事实表明,域实体需要引用它们的存储库在域模型层中进行接口,以便将验证完全放在域模型层中。那么,域实体最终可以访问存储库吗?
Karamafrooz

13

为什么要分开数据访问?

从这本书中,我认为模型驱动设计一章的前两页说明了为什么要从域模型的实现中抽象出技术实现细节的理由。

  • 您想在域模型和代码之间保持紧密联系
  • 分离技术问题有助于证明该模型对于实施是可行的
  • 您希望无所不在的语言渗透到系统的设计中

这似乎都是为了避免使用一个单独的“分析模型”,而该模型与系统的实际实现脱离了。

从我对这本书的了解中可以看出,这种“分析模型”最终可以在不考虑软件实现的情况下进行设计。一旦开发人员尝试实现业务方面理解的模型,他们就会根据需要形成自己的抽象,从而造成沟通和理解上的障碍。

另一方面,开发人员在领域模型中引入太多技术问题也可能导致这种分歧。

因此,您可以考虑将关注点(如持久性)分离开来,可以帮助防止这些设计与分析模型之间的差异。如果有必要在模型中引入诸如持久性之类的东西,那么这是一个危险信号。也许该模型不适合实施。

报价:

“单一模型减少了出错的机会,因为设计现在是经过深思熟虑的模型的直接产物。设计甚至代码本身都具有模型的可通信性。”

我的解释方式是,如果最终有更多的代码行处理诸如数据库访问之类的事情,那么您将失去沟通能力。

如果访问数据库的需求是诸如检查唯一性之类的事情,请查看:

Udi Dahan:团队在应用DDD时犯的最大错误

http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/

在“并非所有规则都平等”下

采用领域模型模式

http://msdn.microsoft.com/zh-CN/magazine/ee236415.aspx#id0400119

在“不使用域模型的方案”下,涉及同一主题。

如何区分数据访问

通过接口加载数据

“数据访问层”已经通过一个接口进行了抽象,您可以调用该接口来检索所需的数据:

var orderLines = OrderRepository.GetOrderLines(orderId);

foreach (var line in orderLines)
{
     total += line.Price;
}

优点:该接口分离出“数据访问”管道代码,使您仍然可以编写测试。可以根据具体情况处理数据访问,从而比常规策略具有更好的性能。

缺点:调用代码必须假定已加载和未加载。

假设出于性能原因,GetOrderLines返回具有空ProductInfo属性的OrderLine对象。开发人员必须对接口背后的代码有深入的了解。

我已经在实际系统上尝试过这种方法。您最终一直在更改加载内容的范围,以尝试解决性能问题。您最终会在界面后面偷看,以查看数据访问代码以查看正在加载和正在加载的内容。

现在,关注点分离应该使开发人员可以尽可能一次地专注于代码的一个方面。接口技术删除了如何加载此数据,但未加载如何,何时加载以及何时加载。

结论:分离度很低!

延迟加载

数据按需加载。调用加载数据的操作隐藏在对象图本身中,在其中访问属性可以导致在返回结果之前执行sql查询。

foreach (var line in order.OrderLines)
{
    total += line.Price;
}

优点:专注于域逻辑的开发人员对数据访问的“时间,地点和方式”是隐藏的。聚合中没有用于处理加载数据的代码。加载的数据量可以是代码所需的确切量。

缺点:当遇到性能问题时,如果拥有通用的“一刀切”解决方案,则很难修复。延迟加载可能会导致整体性能变差,并且实现延迟加载可能会比较棘手。

角色界面/渴望获取

通过聚合类实现的角色接口使每个用例都明确,从而允许按每个用例处理数据加载策略。

提取策略可能如下所示:

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order>
{
    Order Load(string aggregateId)
    {
        var order = new Order();

        order.Data = GetOrderLinesWithPrice(aggregateId);
    
        return order;
    }

}
   

然后,您的集合可能看起来像:

public class Order : IBillOrder
{
    void BillOrder(BillOrderCommand command)
    {
        foreach (var line in this.Data.OrderLines)
        {
            total += line.Price;
        }

        etc...
    }
}

BillOrderFetchingStrategy用于构建聚合,然后聚合执行其工作。

优点:允许每个用例使用自定义代码,以实现最佳性能。符合接口隔离原则。没有复杂的代码要求。聚合单元测试不必模仿加载策略。通用加载策略可用于大多数情况(例如“全部加载”策略),并在必要时实施特殊的加载策略。

缺点:更改域代码后,开发人员仍然必须调整/查看获取策略。

使用获取策略方法,您仍然可能会发现自己正在更改自定义获取代码以更改业务规则。这不是一个完美的关注点分离,但是最终将更易于维护,并且比第一种选择更好。提取策略确实封装了HOW,WHEN和WHERE数据的加载方式。它具有更好的关注点分离,而又不会失去灵活性,就像一种尺寸适合所有延迟加载方法一样。


谢谢,我将检查链接。但是在您的回答中,您是否将“关注点分离”与“根本无法访问”混淆了?当然,大多数人都同意将持久层与实体所在的层分开。但这与说“即使通过非常一般的实现不可知的实体,实体甚至也不能看到持久层”不同。接口'。
codeulike 2011年

是否通过接口加载数据,您仍然需要在实现业务规则时加载数据。我同意,尽管很多人仍然将这种关注点分离称为“责任分离”,但最好还是使用“单一责任原则”。
ttg

1
不太确定如何解析您的最后一条评论,但是我认为您建议在处理业务规则时不应加载数据?我认为这会使规则“更纯净”。但是,许多类型的业务规则将需要引用其他数据-您是否建议应事先通过单独的对象加载它?
Codeulike

@codeulike:我已经更新了答案。如果确实有必要,您仍然可以在业务规则期间加载数据,但这不需要在域模型中添加行数据访问代码(例如,延迟加载)。在我设计的域模型中,通常像您所说的那样预先加载数据。我发现运行业务规则通常不需要过多的数据。
ttg


12

多么好的问题。我走的是同一条路,互联网上的大多数答案似乎都带来了解决方案所带来的许多问题。

因此(冒着一年以后可能会写出我不同意的事情的风险),这是我到目前为止的发现。

首先,我们喜欢一个丰富的域模型,它为我们提供了很高的可发现性(对于聚合我们可以做到的)和可读性(表达性方法调用)。

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

我们希望在不将任何服务注入实体的构造函数中的情况下实现此目标,因为:

  • 引入新行为(使用新服务)可能会导致构造函数更改,这意味着更改会影响实例化实体的每一行
  • 这些服务不是模型的一部分,但是构造函数注入将表明它们是模型的一部分
  • 通常,服务(甚至是其接口)只是实现细节,而不是域的一部分。域模型将具有一个向外依赖
  • 可能令人困惑的是,如果没有这些依赖项,实体就无法存在。(您说信用证服务吗?我什至不会对信用证做任何事情...)
  • 它将很难实例化,因此难以测试
  • 这个问题很容易传播,因为其他包含此实体的实体将获得相同的依赖关系-在它们上看起来像是非常不自然的依赖关系

那么,我们该怎么做呢?到目前为止,我的结论是方法依赖双重调度提供了不错的解决方案。

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

CreateCreditNote()现在需要一个负责创建贷方通知单的服务。它采用双调度,完全卸载工作,以负责任的服务,同时保持曝光率Invoice实体。

SetStatus()现在,它对记录器的依赖简单,它显然会完成部分工作

对于后者,为了使客户端代码更容易处理,我们可以改为通过登录IInvoiceService。毕竟,发票记录似乎是发票所固有的。这样的单一服务IInvoiceService有助于避免为各种操作而需要各种微型服务。缺点是,它变得模糊,究竟该服务将。它甚至可能看起来像是双重调度,而大多数工作实际上仍是SetStatus()自己完成的。

我们仍然可以将参数命名为“ logger”,以期揭示我们的意图。不过似乎有些虚弱。

相反,我会选择要求一个IInvoiceLogger(就像我们在代码示例中所做的那样)并IInvoiceService实现了该接口。客户代码可以简单地将其单一代码IInvoiceService用于Invoice要求任何此类非常特殊的,发票固有的“微型服务”的所有方法,而方法签名仍可以很清楚地表明其要求。

我注意到我没有明确地访问存储库。记录器是或使用存储库,但让我也提供一个更明确的示例。如果只需一两个方法就需要存储库,我们可以使用相同的方法。

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

实际上,这为麻烦的惰性负载提供了替代方案。

更新:我出于历史目的保留了以下文字,但我建议避免100%的延迟加载。

对于真正的,基于属性的延迟加载,我目前确实使用构造函数注入,但是以一种对持久性无知的方式。

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

一方面,Invoice从数据库加载的资料库可以自由访问将加载相应贷方通知单的功能,并将该功能注入Invoice

另一方面,创建实际 代码的代码Invoice将仅传递返回空列表的函数:

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(一个习惯ILazy<out T>可以使我们摆脱对的丑陋表法IEnumerable,但这会使讨论变得复杂。)

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

我很高兴听到您的意见,偏好和改进!


3

在我看来,这似乎是与OOD相关的一般良好实践,而不是DDD特有的。

我能想到的原因有:

  • 关注点分离(实体应与持久化方式分开。因为根据使用场景,可能存在多个策略可以持久化同一实体)
  • 从逻辑上讲,可以在低于存储库运行级别的级别查看实体。较低级别的组件不应了解较高级别的组件。因此,条目不应该具有存储库知识。

2

弗农·沃恩(Vernon Vaughn)简单地给出了一个解决方案:

使用存储库或域服务在调用聚合行为之前先查找依赖对象。客户端应用程序服务可以控制它。


但不是来自实体。
ssmith

从弗农·沃恩IDDD来源:公共类Calendar扩展EventSourcedRootEntity {... public CalendarEntry scheduleCalendarEntry(CalendarIdentityService aCalendarIdentityService,
Teimuraz

检查他的论文@Teimuraz
Alireza Rahmani Khalili

1

在所有这些单独的层级嗡嗡声出现之前,我学会了编码面向对象的程序,我的第一个对象/类DID直接映射到数据库。

最终,我添加了一个中间层,因为我不得不迁移到另一台数据库服务器。我已经多次看到/听说过相同的情况。

我认为将数据访问(也称为“存储库”)与业务逻辑分开是其中之一,这些东西已经被重新发明了数次,并加入了“域驱动设计”书,使其变得“杂乱无章”。

我现在像许多开发人员一样使用3层(GUI,逻辑,数据访问),因为它是一种很好的技术。

将数据分成一个Repository层(又称为Data Access层),可能会被视为一种好的编程技术,而不仅仅是一条规则。

像许多方法一样,您可能希望通过未实现的方法开始,并在了解它们之后最终更新程序。

Quote:Iliad并不是荷马发明的,Carmina Burana也不是卡尔·奥尔夫发明的,​​在两种情况下,让其他人工作的人,所有人,都得到了荣誉;-)


1
谢谢,但是我并不是想将数据访问与业务逻辑分开-这是非常明确的事情,已经达成了广泛的共识。我问的是为什么在DDD架构(例如S#arp)中,实体甚至不允许与数据访问层“对话”。这是一个有趣的安排,我没有能找到很多讨论的地方。
codeulike 2011年

0

这是来自Eric Evans的“域驱动设计”书,还是来自其他地方?

这是旧东西。埃里克(Eric)的书使它变得更加流行。

背后的原因哪里有很好的解释?

道理很简单-当面对模糊相关的多种环境时,人的思维就会变得虚弱。它们导致模棱两可(美国在南美/南美的意思是南美/北美洲),模棱两可导致不断地映射信息,只要头脑“触碰它”,这总会带来不良的生产力和错误。

业务逻辑应尽可能清晰地反映出来。外键,规范化,对象关系映射来自完全不同的领域-这些东西与技术,计算机有关。

打个比方:如果您正在学习如何手写,那么您就不应该了解制作笔的位置,为什么墨水会滞留在纸上,什么时候发明了纸以及中国还有哪些其他著名的发明。

编辑:澄清:我不是在谈论将数据访问从业务逻辑分离到单独层的经典OO做法-我是在谈论在DDD中实体不应该与数据交谈的特定安排根本没有访问层(即,它们不应保存对存储库对象的引用)

原因仍然与我上面提到的相同。在这里,仅一步之遥。如果实体可以全部(至少接近),为什么实体应该部分不了解?我们的模型所涉及的与领域无关的问题更少-当需要重新解释它时,我们的思想将获得更多的喘息空间。


对。那么,如果完全不允许持久性的实体甚至不允许与持久层进行通信,那么如何实现完全持久性无知的实体呢?当需要查看任意其他实体中的值时,该怎么办?
codeulike

如果您的实体需要查看任意其他实体中的值,则可能存在一些设计问题。也许考虑分班,使他们更有凝聚力。
cdaq

0

引用Carolina Lilientahl的话,“模式应该防止循环” https://www.youtube.com/watch?v=eJjadzMRQAk,其中提到了类之间的循环依赖性。对于聚合内的存储库,出于唯一的原因,出于对象导航的便利性,倾向于创建循环依赖关系。Vernon Vaughn推荐了prograhammer上面提到的模式,在该模式中,其他集合由id而不是根实例引用(此模式有名称吗?),它建议了一种替代方法,可以指导其他解决方案。

类之间的循环依赖(自白)示例:

(Time0):两个类Sample和Well互相引用(循环依赖)。为方便起见,“孔”指的是“样品”,而“样品”指的是“孔”,有时是循环样品,有时是将板中的所有孔循环。我无法想象Sample不会引用回放置它的Well的情况。

(Time1):一年后,实施了许多用例..现在有些情况下Sample不应该引用其所放置的孔。在一个工作步骤中有一些临时标牌。这里的孔是指样品,而样品又是指另一块板上的孔。因此,当有人尝试实现新功能时,有时会发生奇怪的行为。需要时间来渗透。

上面提到的有关延迟加载的负面影响的这篇文章也对我有所帮助。


-1

在理想情况下,DDD建议实体不应该引用数据层。但是我们并不生活在理想世界中。域可能需要引用其他域对象以获取与之不相关的业务逻辑。实体出于只读目的引用存储库层以获取值是合乎逻辑的。


不,这会给实体带来不必要的耦合,违反了SRP和关注点分离,并且很难从持久性上反序列化实体(因为反序列化过程现在还必须注入实体所需的服务/存储库)。
ssmith
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.