49

我刚刚学习 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 类中,尽管不是存储库类。由于我只进行只读访问,因此我将其放入结构 DTO 中。由于数据库很小,我将它们完全放在一个类中,就像下面描述的 Todd。

我的结论:

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

5 回答 5

9

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

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

于 2009-04-30T21:20:57.670 回答
7

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

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

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

于 2009-04-30T21:12:41.573 回答
4

好吧,我前段时间读了一篇Mathias Verraes的文章,在这里谈到了这一点。他谈到了将模型中的值对象与服务于 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];
    }
}

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

顺便说一句,请确保您查看评论部分以了解有关该文章的一些严肃讨论。

于 2014-02-26T12:47:16.777 回答
3

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

于 2009-04-30T20:14:42.667 回答
3

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

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

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

于 2009-04-30T20:16:18.013 回答