1

对于我想在下一个(Laravel PHP)项目中实现的存储库服务/用例模式(DDD设计的一部分),我需要一些帮助。

一切似乎都清楚了。DDD 中只有一个令人困惑的部分是来自存储库的数据结构。人们似乎选择了存储库应该返回的数据结构(数组或实体),但这都有缺点。其中之一是回顾我过去经历的表现。一种是您没有简单数据结构(数组或简单对象属性)的接口。

我将首先解释我在以前的项目中的经验。这个项目有缺陷,但我从新项目中学到并希望看到一些好的优势,但解决了一些设计错误。

以往的经验

过去,我使用 Kohana 框架和 Doctrine 2 ORM(数据映射器模式)构建了一个以 API 为中心的网站。流程如下所示:

网站控制器 → API 客户端(HMVC 调用) → API 控制器 → 自定义存储库 → Doctrine 2 ORM 原生存储库/实体管理器

我的自定义存储库使用 Doctrine2 DQL 返回纯数组。Doctrine2 建议将数组结果数据用于只读操作。是的,它使我的网站美观而轻松。API 控制器只是将数组数据转换为 JSON。就那么简单。

过去,我的公司创建的项目完全依赖加载的 Doctrine2 实体,这让我们因性能而感到遗憾。

我的 REST API 支持
/api/users?include_latest_adverts=2&include_location=true
对用户资源的查询。API 控制器传递include_location到直接包含位置关系的存储库。控制器读取latest_adverts=2并调用广告存储库以获取每个用户的最新 2 个广告。数组被返回。

例如第一个用户数组:

[
    name
    avatar
    adverts [
        advert 1 [
            name
            price 
        ]
        advert 2 [
            …. 
        ]
    ]
]

事实证明这是非常成功的。我的整个网站都在使用 API。添加新客户端将非常容易,因为该 API 已经在使用 oauth 完美地投入生产。整个网站都在上面运行。

但这种设计也有缺陷。我的控制器仍然包含很多用于验证、邮件、参数或过滤器的逻辑,比如has_adverts=true只为用户提供广告。这意味着如果我创建一个新端口,比如一个全新的 CLI 接口,由于所有验证等原因,我将不得不复制很多这些控制器。但如果我要创建一个新客户端,则不需要重复。所以至少解决了一个问题:-)

我的管理面板完全耦合到了学说 2 存储库/实体管理器,以加快开发速度(有点)。为什么?因为我的 API 具有仅针对网站具有特殊功能的胖控制器(特殊验证、邮寄注册等)。我将不得不重做工作或重构很多。因此决定直接使用实体以仍然有某种清晰的方式来编写代码,而不是重写我所有的 API 控制器并将它们移动到服务(例如站点和管理员)。时间是修复我的设计错误的一个问题。

对于我的下一个项目,我希望所有代码都通过我自己的自定义存储库和服务。一个流动的好分离。

新项目(使用 DDD 思想)和数据结构的困境

虽然我喜欢以 API 为中心的想法,但我不希望我的下一个项目以 API 为中心,因为我认为相同的功能应该在没有 HTTP 协议的情况下可用。我想使用 DDD 思想设计核心。

但我喜欢这个想法,它使用一个仅作为 API 说话并返回简单数组的层。任何新端口的完美基础,包括我自己的前端。我的想法是将我的服务类视为 API 接口(返回数组数据),进行验证等。我可以拥有专门用于网站的服务(注册)和管理员或后台进程使用的普通服务。在某些管理情况下,简单的 CRUD 编辑无论如何都不需要服务,我可以直接使用存储库。控制器会很薄。有了这个,创建一个真正的 REST API 只需使用与我的前端控制器类相同的服务来创建新控制器。

对于像业务规则这样的内部逻辑,拥有实体(清晰的接口)而不是来自存储库的数组会很有用。通过这种方式,我可以从定义一些基于属性执行一些逻辑的方法中受益。但是如果我使用 Doctrine2 并且我的存储库总是返回实体,我的应用程序将遭受很大的性能打击!

当使用像 Doctrine 2 这样的数据模式模式(现在或将来)时,一种数据结构确保性能但没有清晰的接口,另一种确保清晰的接口但性能不佳。此外,我最终可能会得到两种令人困惑的数据类型。

我在想类似于这个流程的东西:

Controller(瘦)→ UserService(包括验证)→ UserRepository(只是存储)→ Eloquent ORM

为什么用 Eloquent 而不是 Doctrine2?因为我想坚持一下 Laravel 框架和社区中的共同点。所以我可以从第三方模块中受益,例如生成管理界面或基于模型的类似模块(绕过我的 DDD 规则)。除了使用第三方模块之外,我会设计我的核心内容,因此切换应该总是很容易,并且不会影响数据结构的选择或性能。

Eloquent 是一个 activerecord 模式。所以我很想将这些数据转换为像 Doctrine2 实体一样的 POPO。但是不...如上所述,使用doctrine2 真实模型会使系统变得非常胖。所以我再次回到简单的数组。知道这一点将适用于未来的任何其他实现。

但是感觉不好总是依赖数组。尤其是在创建内部业务规则时。开发人员必须猜测数组上的值,在他的 IDE 中没有自动完成功能,不能像实体类那样有特殊方法。但是用两种方式处理数据也让人感觉很糟糕。或者我太完美了;)我想要一个清晰的数据结构!

构建接口和 POPO 将意味着大量重复工作。我需要将 Eloquent 模型(只是一个表映射器,而不是实体)转换为实现此接口的实体对象。都是额外的工作。最终我的最后一层就像一个 API,从而再次将其转换为数组。这也是额外的工作。数组似乎又是一笔交易。

读入 DDD 和 Hexagonal 似乎很容易。好像很合乎逻辑!但实际上,我在试图坚持 OOP 原则的过程中遇到了一个简单的问题。我想使用数组,因为它是 100% 确定我不依赖于任何模型选择和从我的 ORM 中查询关于性能等的选择的唯一方法,并且在转换为视图或 API 的数组时没有重复的工作。但是对于用户数组的外观并没有明确的约定。我想使用这些模式加速我的项目,而不是减慢它们的速度:-) 所以不是有很多转换器的选项。

现在我阅读了很多主题。一个使符合像 Doctrine2 这样的适当实体的 POPO 和接口可以返回,但要为 Eloquent 做所有额外的工作。切换到 Doctrine2 应该相当容易,但会影响性能如此糟糕,或者需要将 Doctrine2 数组数据转换为这些自己的实体接口。其他人选择返回简单的数组。

有人说服人们使用 Doctrine2 而不是 Eloquent,但他们忽略了 Doctrine2 很重并且您确实需要将数组结果用于只读操作的事实。

我们将存储库设计为可更改的,对吗?不是因为它只是设计上的“好”。那么,如果它对性能或重复工作有如此大的影响,我们怎么能依赖完整的实体呢?即使仅使用 Doctrine2(耦合),由于其性能也会出现同样的问题!

所有 ORM 实现都将能够返回数组,因此那里没有重复的工作。很棒的表演。但我们错过了明确的合同。而且我们没有数组或类属性的接口(作为一种解决方法)......呃;)

我只是错过了我们的编程语言中缺少的部分吗?简单数据结构上的接口??

制作所有数组并让高级业务逻辑与这些数组对话是否明智?因此没有具有清晰接口的类。任何预先计算的数据(通常由 Entity 方法返回)都将位于定义 Service 类的数组键中。如果不明智,考虑到上述所有因素,还有什么选择?

如果有人在考虑性能、不同的 ORM 实现等方面在这个“领域”有丰富经验的人能告诉我他/她是如何处理这个问题的,我将不胜感激?

提前致谢!

4

1 回答 1

1

我认为您正在处理的事情与我正在努力解决的事情类似。我认为最有效的解决方案是:

  1. 实体/存储库
    • 在执行写入操作(创建事物、更新事物、删除事物及其复杂组合)时始终使用和传递实体。
    • 有时您可能会在执行读取操作时使用实体(当您预计读取可能需要在不久之后用于写入时......即->findById很快就会跟随->save)。
    • 每当您使用实体(无论是写入还是读取)时,都需要使用存储库。您应该能够告诉新开发人员,他们只能通过实体和存储库持久化到数据库。
    • 实体将具有表示某些域对象的属性(很多时候它们表示具有表字段的数据库表,但并非总是如此)。它们还将包含域逻辑/规则(即验证、计算),因此它们不会乏力。如果您的实体需要帮助与其他实体交互(需要触发其他事件),或者您只需要一个额外的地方来处理一些额外的域逻辑(执行存储库调用以检查某些独特条件),您可能还需要一些域服务。
    • 您的存储库将仅用于与实体合作。存储库可以接受实体并对其进行一些持久性工作。或者他们可以只接受一些参数,并对完整的实体进行一些读取/获取。
    • 一些存储库将知道如何保存一些比其他更复杂的域对象。也许一个实体的属性包含需要与主实体一起保存的其他实体列表(如果需要,您可以深入了解聚合根)。
    • 存储库的接口位于您的域层中,而不是这些存储库的实际实现。这样你就可以拥有一个 Eloquent 版本或其他版本。
  2. 其他查询(表数据网关)
    • 这些查询不适用于实体。他们只会接受参数并返回诸如数组或 POPO(普通旧 PHP 对象)之类的东西。
    • 很多时候,您需要执行不能很好地返回到单个实体的读取。这些读取通常更多用于报告(不适用于类似 CRUD 的操作,例如将用户读取到最终提交并保存的编辑表单中)。例如,您可能有一个包含 200 行 JOINed 数据的报表。如果您使用存储库并尝试返回大型深层对象(填充所有关系,甚至延迟加载),那么您将遇到性能问题。相反,使用表数据网关模式。您只是在显示数据,并不真正需要 OOP 功能。然而,输出的数据可能包含 ID,通过 UI 可用于启动对 Repository 持久性方法的调用。
    • 在开发应用程序时,当您遇到需要新的读取/报告查询时,请在 Table Data Gatway 文件夹中的某个类中创建一个新方法。您可能会发现您已经创建了一个类似的查询,因此请查看如何合并其他查询。如有必要,请使用一些参数以使网关方法的查询在特定方式(即要选择的列、排序顺序、分页等)上更加灵活。不要让你的查询过于灵活,这就是查询构建器/ORM 出错的地方!您需要将查询限制在一定程度上,如果您需要替换它们(可能是不同的数据库引擎),那么您可以轻松地了解允许的变化是什么,不是什么。您可以在灵活性(因此您有更多 DRY 代码)和约束(以便您以后可以优化/替换查询)之间找到适当的平衡。
    • 您可以在您的域中创建服务来处理接收参数,然后将它们传递给表数据网关,然后接收返回的数组以进行更多的变异。这将使您的域逻辑保持在域中(并且在存储库和表数据网关的基础架构/持久层之外)。
    • 同样,就像存储库一样,在您的域服务中使用接口,以便实现细节远离您的域层,并驻留在实际的表数据网关文件夹中。
于 2015-03-31T01:02:17.117 回答