2

我已经在 Yii 框架上开发了一段时间(4 个月),到目前为止,我遇到了一些关于 MVC 的问题,我想与经验丰富的开发人员分享。我将通过列出它们的复杂程度来呈现这些问题。

[Level 1] CR(创建更新)表格。首先,我们有很多表格。每个表单本身都是一个模型,因此每个表单都有一些验证规则、一些属性和一些要对属性执行的操作。在很多情况下,这些表单中的每一个都使用单个活动记录对象在数据库中更新创建记录。 -> 所以在这种复杂程度下,表单必须

  • 打开时,

    • 能够以人性化的方式显示来自数据库的数据库友好数据

    • 能够显示所有具有活动记录对象属性的表单字段。从 db 表中添加、删除、更改列必须影响表单的显示。

  • 保存时,能够在获取数据之前将对人类友好的数据格式化为对数据库友好的数据

  • 验证时,能够执行活动记录对象强制执行的基本验证,它还必须执行其他验证来满足某些业务规则

  • 当验证失败时,能够回滚对属性所做的更改以及对数据库所做的更改,并向用户显示他们最初输入的数据。

[级别 2] 扩展 CR 表格。一种可以同时从不同表创建/更新记录的表单。不仅如此,表单是否会创建/更新其记录有时可能取决于其他条件(更多业务规则),因此表单有时可以更新表 A、B 但不能更新 D 中的记录,有时会更新 A 中的记录,D 但不是 B -> 所以在这种复杂程度下,我们看到一个表单必须:

  • 能够满足[1级]

  • 能够有条件地创建/更新某些记录,有条件地创建/更新某些记录的某些列。

[Level 3] 模型树。表单在应用程序中的角色,在许多方面,是一个让用户与您的应用程序交互的端口。为了满足请求,此端口将与许多其他对象交互,而这些对象又与更多对象交互。其中一些对象可以被视为模型。Active Record 是一个模型,但 Mailer 也可以是一个模型,RobotArm 也是如此。这些模型相互使用以满足用户的要求。每个模型都可以执行自己的操作,并且整个树必须能够回滚在错误/失败情况下所做的任何更改。

有没有人遇到或能够解决这些问题?

我想出了很多东西,比如将模型属性封装在 ModelAttribute 对象中,以解决它们在客户端、服务器和数据库层中的存在问题。

我还认为我们应该给模型树一个 Observer 来观察并通知观察到的模型在发生错误时回滚更改。但是如果可以存在多个观察者,如果一个节点使用其父的观察者但给它的孩子另一个观察者怎么办。

工程师、开发人员、Rails、Yii、Zend、ASP、JavaEE,任何 MVC 人,为了科学,请加入这个讨论。

--更新 teresko 的回复: ---
@teresko 我实际上打算将服务合并到工作单元内的执行中,并且让工作单元不用担心新/更新/删除。工作单元内的每个对象都将对其状态负责,并需要实现自己的 commit() 和 rollback()。一旦发生错误,工作单元将从最新注册的对象回滚到最旧的注册对象的所有更改,因为我们不仅处理数据库,还可以有邮件程序、发布者等。否则,树执行成功,我们从最旧的注册对象调用 commit() 到最新的注册对象。这样,邮件程序可以保存邮件并在提交时发送。

使用数据映射器是个好主意,但我们仍然必须确保 db 中的列与数据映射器和域对象匹配。此外,扩展的 CR 表单或具有依赖于其他模型的属性的模型必须在验证和数据类型方面匹配它们的属性。那么也许一个属性可以是一个对象并从一个模型运送到另一个模型?一个属性还可以判断它是否被修改,应该对其执行什么验证,以及它如何对人类友好、对应用程序友好和对数据库友好。对 db 模式的任何更新都会影响此属性,从而引发异常,需要开发人员对系统进行更改以满足此更改。

4

1 回答 1

16

原因

您的问题的根源是滥用活动记录模式。AR 适用于仅具有基本 CRUD 操作的简单域实体。当您开始在多个表之间添加大量验证逻辑和关系时,该模式开始分崩离析。

为简单起见,最好的活动记录是轻微的SRP违规。当你开始承担责任时,你开始受到严厉的惩罚。

解决方案

1级:

最好的选择是将业务和存储逻辑分开。大多数情况下,它是通过使用域对象数据映射器来完成的:

  • 领域对象(在其他材料中也称为业务对象或领域模型对象)处理验证和特定业务规则,并且完全不知道它们中的数据是如何(或什至“如果”)存储和检索的。它们还让您拥有不直接绑定到存储结构(如数据库表)的对象。

    例如:您可能有一个LiveReport表示当前销售数据的域对象。但它可能在 DB 中没有特定的表。相反,它可以由多个映射器提供服务,这些映射器从 Memcache、SQL 数据库和一些外部 SOAP 中汇集数据。并且LiveReport实例的逻辑与存储完全无关。

  • 数据映射器知道将来自域对象的信息放在哪里,但它们不进行任何验证或数据完整性检查。认为他们能够处理来自低级存储抽象的异常,例如违反UNIQUE约束。

    数据映射器也可以执行事务,但是,如果需要为多个域对象执行单个事务,您应该考虑添加工作单元(更多关于它的内容较低)。

    在更高级/复杂的情况下,数据映射器可以交互和利用DAO和查询构建器。但这更多的是针对情况,当您旨在创建类似 ORM 的功能时。

    每个域对象可以有多个映射器,但每个映射器应该只与特定类的域对象(或一个的子类,如果你的代码遵守LSP)一起工作。您还应该认识到域对象和域对象集合是两个独立的东西,应该有独立的映射器。

    此外,每个域对象可以包含其他域对象,就像每个数据映射器可以包含其他映射器一样。但在映射器的情况下,这更像是一个偏好问题(我非常不喜欢它)。

另一个可以减轻您当前混乱的改进是防止应用程序逻辑在表示层(通常是控制器)中泄漏。相反,您将从使用包含映射器和域对象之间交互的服务中受益匪浅,从而为您的模型层创建一个公开的API。

基本上,您封装模型的完整部分的服务,可以(在现实世界中 - 只需少量努力和调整)在不同的应用程序中重用。例如:RecognitionMailerDocumentLibrary将所有服务。

另外,我认为我不应该,并非所有服务都必须包含域对象和映射器。一个很好的例子是前面提到的Mailer,它可以由控制器直接使用,或者(更有可能)由另一个服务使用。

2级:

如果您停止使用活动记录模式,这将成为一个非常简单的问题:您需要确保只保存那些域对象中的数据,这些数据自上次保存以来实际上已经发生了变化。

在我看来,有两种方法可以解决这个问题:

  1. Quick'n'Dirty

    如果有什么变化,只需更新它...

    我更喜欢的方式是在域对象中引入一个checksum变量,该变量包含来自所有域对象变量的哈希(当然,除了checksum它自己)。

    每次要求映射器保存域对象时,它都会调用isDirty()该域对象上的一个方法,该方法检查数据是否已更改。然后映射器可以采取相应的行动。这也可以通过一些调整用于对象图(如果它们不是很广泛,在这种情况下你可能需要重构)。

    此外,如果您的域对象实际上被映射到多个表(甚至是不同形式的存储),则为每组变量设置多个校验和可能是合理的。由于映射器已经为特定类的领域对象编写,它不会加强现有的耦合。

    对于 PHP,您将在此答案中找到一些代码示例。

    注意:如果您的实现使用 DAO 将域对象与数据映射器隔离,那么基于校验和的验证逻辑将移至 DAO。

  2. 工作单元

    这是您问题的“行业标准”,在PoEAA书中有一整章(第 11 章)处理它。

    基本思想是,您创建一个实例,在域对象和数据映射器之间充当控制器(在经典中,而不是 MVC 意义上的)。

    每次更改或删除域对象时,都会通知工作单元。每次在域对象中加载数据时,您都要求工作单元执行该任务。

    有两种方法可以告诉Unit of Work所做的更改:

    • 调用者注册:执行更改的对象也通知工作单元
    • 对象注册:更改的对象(通常来自 setter)通知工作单元,它已被更改

    当与域对象的所有交互都完成后,您调用commit()工作单元上的方法。然后它找到必要的映射器并存储所有更改的域对象。

3级:

在这个复杂的阶段,唯一可行的实现是使用工作单元。它还将负责使用适当的回滚子句启动和提交 SQL 事务(如果您使用的是 SQL 数据库)。

附言

阅读《企业应用架构模式》一书。这是你迫切需要的。它还将纠正您通过使用类似 Rails 的框架获得的关于 MVC 和受 MVC 启发的设计模式的误解。

于 2012-11-22T04:32:13.670 回答