在领域驱动设计 (DDD) 中建模查找表的实用方法是啥?

Posted

技术标签:

【中文标题】在领域驱动设计 (DDD) 中建模查找表的实用方法是啥?【英文标题】:What is a practical way to model lookup tables in Domain Driven Design (DDD)?在领域驱动设计 (DDD) 中建模查找表的实用方法是什么? 【发布时间】:2010-10-22 22:47:41 【问题描述】:

我刚刚学习 DDD(Eric Evans 的书在我面前打开),我遇到了一个我找不到答案的问题。当您只是想获得一个简单的查找记录列表时,您在 DDD 中会做什么?

例如

员工 ID:123 员工姓名:John Doe 州:阿拉斯加(下拉) 县:瓦西拉(下拉列表 -- 将根据州过滤)。

例如,假设您有一个 Employee 域对象、一个 IEmployeeRepository 接口和一个 EmployeeRepository 类。 UI 将使用它来显示员工列表和个人详细信息。在 UI 中,您希望为员工居住的州和县使用下拉菜单。可用县将根据选择的州进行过滤。

不幸的是,数据库表和 UI 看起来非常不同。在 tblEmployees 中,它包含州代码=AK 和县代码=02130,而不是州和县名称。

旧方法(在我开始这个 DDD 任务之前)非常简单,只需创建 2 个查询并使用 DataReader 填充下拉列表。下拉菜单中显示的下方是值,它会自动在表单帖子中使用。

但是,对于 DDD,我不确定您应该如何执行此操作。我首先创建了州和县对象以及存储库和存储库接口。但是,编写 4 个类 + 2 个接口和 hbm.xml 文件中的管道 + Employee 业务对象对于 2 个下拉列表的 2 个查询似乎有点过头了。必须有更好的方法,不是吗?我不会很快更改州或县表中的记录,即使我这样做了,也不会通过此应用程序进行。因此,如果不需要的话,我真的不想为州和县创建业务对象。

我看到的最简单的解决方案是创建一个辅助类,其中包含返回字典的方法,例如 GetStatesAll()、GetState() 和 GetCounties() 以及 GetCounty(),但从 DDD 的角度来看这是错误的。

请帮忙。如何使用 DDD 而不过度设计几个简单的查找?

最终解决方案 我想我终于通过经验找到了答案,就是把GetStates()方法放到自己的Data Access类中,虽然不是repository类。由于我只进行只读访问,因此我将其放入结构 DTO 中。由于数据库很小,我将它们完全放在一个类中,就像下面描述的 Todd。

我的结论:

    查找表绝不是值对象,因为查找表总是有一个标识。如果他们没有身份,你就会有重复,这没有多大意义。 只读查找表可以有存储库,但可能不需要。存储库的目标是通过强制仅通过聚合访问来降低复杂性。通过汇总为您提供了一种确保可以强制执行业务规则的方法,例如如果您没有汽车则不添加轮胎。 如果您允许对查找表进行 CRUD 维护,则查找表拥有自己的存储库是有意义的。 我最终将代码存储为结构这一事实并没有使它们成为“值类型”。 Fowler 在 POEAA 中说结构是一种值类型。没错,结构是不可变的,这就是 Fowler 说它们是“值类型”的原因,但我使用它们的方式不同。我使用结构作为一种轻量级的方式来传递我从未计划在初始创建后更改的 DTO。事实上,我使用的结构确实具有标识,但由于它们是只读的,它们作为结构工作。 我一直在使用的一种我在其他地方看不到的模式是使主键字段不可变。它们由构造函数设置,但它们是只读的(不是私有访问器),一旦创建对象就不能更改。

【问题讨论】:

您找到解决方案了吗?如果是...请在下面发布...谢谢 优秀的总结。这种跟进太少见了。 【参考方案1】:

好吧,我前段时间读了Mathias Verraes 的一篇文章,谈到了这个here。他谈到了将模型中的值对象与服务于 UI 的概念分开。

当被问及是否将国家建模为实体或值对象时引用文章:

将国家建模为 实体并将它们存储在数据库中。但在大多数情况下,那 过于复杂的事情。国家不会经常变化。当一个 国家名称的变化,实际上,出于所有实际目的, 新国家。如果有一天一个国家不存在了,你不能 只需更改所有地址,因为国家可能被分割了 进入两个国家。

他提出了一种不同的方法来引入一个名为AvailableCountry的新概念:

这些可用的国家可以是数据库中的实体,也可以是数据库中的记录 JSON,甚至只是代码中的硬编码列表。 (这取决于 企业是否希望通过 UI 轻松访问它们。)

<?php

final class Country

    private $countryCode;

    public function __construct($countryCode)
    
        $this->countryCode = $countryCode;
    

    public function __toString()
    
        return $this->countryCode;
    


final class AvailableCountry

    private $country;
    private $name;

    public function __construct(Country $country, $name)
    
        $this->country = $country;
        $this->name = $name;
    

    /** @return Country */
    public function getCountry()
    
        return $this->country;
    

    public function getName()
    
        return $this->name;
    



final class AvailableCountryRepository

    /** @return AvailableCountry[] */
    public function findAll()
    
        return [
            'BE' => new AvailableCountry(new Country('BE'), 'Belgium'),
            'FR' => new AvailableCountry(new Country('FR'), 'France'),
            //...
        ];
    

    /** @return AvailableCountry */
    public function findByCountry(Country $country)
    
        return $this->findAll()[(string) $country];
    

所以似乎有第三种解决方案,即将查找表建模为值对象和实体。

顺便说一句,请确保您在 cmets 部分查看 some serious discussions 关于这篇文章的信息。

【讨论】:

【参考方案2】:

您可能想研究Command Query Separation 的概念。我不会担心查找值的类型化存储库,但我仍然可能会在数据集等上使用 DTO 类型类...

您可能想花一些时间阅读 Greg Young 从 this one 到现在的博客。他没有专门谈论填充查找数据,但他经常谈论不通过存储库上的类型化方法处理应用程序的读取/报告功能。

【讨论】:

谢谢。我不确定这是一个确切的答案,但它让我走上了正确的轨道,即“检索只读数据的最佳方法是什么”。【参考方案3】:

使用 DDD 我有以下类似的东西:

interface IAddressService

  IList<Country> GetCountries ();
  IList<State> GetStatesByCountry (string country);
  IList<City> GetCitiesByState (string state);
  // snip

Country、State 和 City 是来自数据库中查找表的值对象。

【讨论】:

如果参数是国家和州而不是字符串会更好。【参考方案4】:

如果您想学习如何进行 DDD 而又不使其过于复杂,那您就读错书了。 :-)

您提出的最简单的解决方案可以满足您的需求。将地址数据封装在业务对象中可以根据您的应用程序需求简单或复杂。例如,State 对象与 County 具有一对多的关系,因此如果您选择以这种方式建模,那么 Employee 真的只需要引用一个 County。如果需要,我只会介绍这种复杂性。

此外,我认为为存储库定义接口不会有什么好处,除非确实有可能为对象创建多个存储库。

【讨论】:

我的理解是,如果你没有为你的存储库使用接口,你并没有真正在做 DDD。为您的存储库创建接口不是更容易让您进行模拟吗?无论如何,在 Visual Studio 中,从您设计的类中生成一个接口需要 1 秒,所以我并没有真正看到使用接口的缺点。谢谢。 如果你只有一个实现接口的类,那么接口的意义何在?具体的类和接口一样可模拟。我确实认为接口对于可能具有多个实现的服务类以及使用依赖注入框架的服务类很有价值。 我还认为从类中提取接口是一种落后的方法——应该在实现之前设计契约。通过这种方式,接口确实有利于单元测试,因为可以在任何具体实现编码之前使用模拟接口创建测试。 只有一个类的接口是为了能够测试它。然后你可以把它存根,嘲笑它......如果你喜欢就吃它......我同意约翰......你做得很好......继续 "具体类与接口一样可模拟。"这不是真的。至少,您必须使属性虚拟化以使用最小起订量来模拟它们,我认为这会掩盖您的意图。我认为您最好将接口作为构造函数依赖项。更坚实。【参考方案5】:

州和县不是实体,而是价值对象。它们不是您系统的主题。你说你以前处理这些的方式是可以的。根据域模型状态的变化,您何时会更改数据库中的州或县记录?不,所以这些不需要存储库。

【讨论】:

好的,这有点道理。让我有些失望的是我对对象的 ID 字段感兴趣,但这并不一定使它们成为“实体”,是吗。你把所有值对象的创建放到哪里去了?进入一个全局 ValueObjectFactory 类?有最佳做法吗? 我同意@John 的观点:像 County 和 State 这样的查找表不是值对象,原因很简单:它们有 ID,它们由 ID 引用。该模型可以有一个地址值对象,该对象将引用县和州。只是为了强调 VO 可以引用实体这一事实。

以上是关于在领域驱动设计 (DDD) 中建模查找表的实用方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

什么是DDD(领域驱动设计)?

领域驱动实战-支付系统

DDD领域驱动设计实践 —— 业务建模小招数

领域驱动设计概览

领域模型驱动设计(DDD)之模型提炼

DDD领域驱动设计实战-聚合(Aggregate)和聚合根(AggregateRoot)