4

上下文:在 .NET 平台上构建一个智能客户端应用程序,其中您有一个复杂的数据库模型,其中涉及大量列。自然的应用风格是典型的数据驱动的 CRUD。在某些情况下还有相当多的服务器端逻辑,以及有些复杂的验证。您可以完全控制客户端和服务器,因此对互操作性的需求是最低限度的。


这个问题有很多细节,为此道歉,但这是因为我想为答案设置适当的上下文。


其他一些假设
- 在 Microsoft 世界中并不少见,大多数以前的应用程序都是使用 DataSet 编写的,因此它是所涉及的开发人员最知名的技术。但是,假设开发人员也精通 OO 思维。
- 您需要在客户端和服务器上运行验证。
- 您不会以表格形式显示大多数数据。
- 这不是一个内网应用程序,所以你不能对带宽假设太多


。最大的问题:数据集还是对象?


如果你选择数据集,你会有一些积极和消极
的方面 - 就积极方面而言:在从数据库中获取数据、通过网络获取数据以及通过网络返回更改的数据方面,你会获得一些 Microsoft 支持更小的块——因为你可以指定只发送更改。发送较少的数据是好的,因为可能涉及相当多的数据。
- 缺点是:在验证、业务逻辑等方面,你得到了一种程序形式的代码,而你没有得到面向对象代码的好处——行为和数据结合在一起,一种更自然的工作方式和思考方式你在做什么,并且可能与验证逻辑有更密切的联系。您也可以忽略将数据集放在网格中的好处,因为这不是常见的用例。

如果您选择对象,这是相同的练习,但涉及更多选项:
积极因素:行为和数据一起。验证逻辑更接近。更容易看到和理解对象之间的关系。更具可读性的代码。更容易进行单元测试。但是您还需要做很多选择和工作:


OR/Mapping
- 将数据从关系模型获取到对象。OR-mappers 并不复杂,并且能够很好地处理它。但它增加了开发时间。


合同映射
- 将数据从服务器端对象映射到合同对象(可能是 DTO)通常是一种很好的做法。由于这是一个非常适合 CRUD 风格架构的应用程序,因此 DTO 并没有真正为图片增加太多价值,只是映射工作。


共享代码
- 您可以使用共享代码场景,其中包含域数据和逻辑的程序集在客户端和服务器端都可用。这是紧耦合,但当您拥有一个自然紧耦合的客户端-服务器应用程序时,它不一定是坏事。


无论您是否选择添加合同层,您都有必须通过网络发送的大型对象结构由于我们同时控制客户端和服务器,因此传输和编码应该是 TCP 上的二进制编码。这会有所帮助。使用数据集,您可以选择仅将更改发回。来回发送整个对象结构可能是一个性能问题。发送整个对象结构的一个选项是以某种方式识别所涉及的更改(创建、更新、删除),并仅发送有关这些的信息。理论上,将聚合根 ID 以及更改发送到服务器并不难,要求服务器延迟加载聚合根,执行所做的更改,然后再次保存。但所涉及的巨大复杂性是确定所做的更改。你有没有选择过这种方法?为什么?你具体是怎么做的?

演示
对于这个问题,确切的 UI 技术并不是那么重要,WinForms、Silverlight 或 WPF 都是可能的。让我们假设我们正在使用 WPF,因为它是一个新的智能客户端。这意味着我们有两种方式绑定并且可以正确使用 MVVM。

用户界面中绑定的对象将需要实现 INotifyPropertyChanged 并在每次更新属性时引发一个事件。你如何解决这个问题?如果您选择共享代码场景,您可以将其添加到域对象中,但这将涉及在服务器端添加代码和逻辑,而这些代码和逻辑永远不会在那里使用。如果您使用合约对象,这种分离会更自然,但这并没有增加一层映射的价值。

技术
有一些技术可以帮助解决一些问题,但往往会使其他问题复杂化。你是使用它们,还是自己从头开始构建东西?
**
- CSLA 是可能的,但它使单元测试更加困难,并且似乎为数据访问增加了更紧密的耦合。它确实有助于解决许多问题,但我个人对这项技术没有能力,所以它是否非常适合还很难说。
- Silverlight 解决方案可以使用 WCF RIA 服务,但肯定存在一些限制。数据大小为一。
- WCF 数据服务是另一种快速启动的方法,但 REST 没有多大帮助,而且您还缺乏 RIA 服务中的验证支持。

总结
如果您已经走到了这一步,我希望您对我的目标有所了解。我试图缩小范围以避免一次讨论所有内容,但是分布式开发很复杂,因此您必须考虑很多部分。


更新

谢谢你们的回应!我试图问的问题足够开放,可以得到不同的答案,但又足够具体,可以处理一些不常见的要求。

有不同的考虑,有不同的优点和缺点,并且因系统而异。每个通常都会增加寻找解决方案的复杂性。这个问题的要点之一是获得一些额外要求的答案,这些要求不一定直接适合今天通常是正确的答案 - 使用基于任务的 UI。如果你愿意的话,我不是“CRUD 人”。但是由于各种原因(通常是遗留系统),一些系统非常适合 CRUD。

许多商业应用程序都有类似的需求,这些需求朝着不同的方向发展:

业务相关
- 查看:向用户显示数据并更新相同的数据(读取和 CUD - 创建、更新、删除)
- 验证:业务规则

UI 相关
- 验证:UI 规则
- UI 更新:特定于让 UI 更新对象更改的代码 (INotifyPropertyChanged)

网络相关
- 数据大小:您通过网络发送的数据量

数据库相关
- 延迟加载

SRP/重用相关
- 映射:由多层对象/分离关注点引起

维护/更改相关
- 更改:添加新信息(列/字段)
- 代码量
- 重用和“更改原因”

技术限制
- 变更跟踪

但这些只是一些非常具体的。您总是需要知道您认为哪些“-ilities”最重要,因此您需要什么程度的可伸缩性、可用性、可扩展性、互操作性、可用性、可维护性和可测试性。

如果我想对大多数情况进行概括,我会说:

客户端
- 使用 MVVM 进行分离和可测试性
- 在 DTO 之上创建 VM - 在 VM 中
实施 INotifyPropertyChanged。
- 使用 XamlPowerToys、Postsharp 或其他一些帮助解决此问题的方法是值得
的 - 在 UI 中分离读取和 CUD
- 使 CUD 基于任务,并使用命令或类似的命令将这些操作发送到服务器端

服务器
- 为每个屏幕定制一个 dto - 或者使用 Ayende 在http://msdn.microsoft.com/en-us/magazine/ff796225.aspx
中描述的多查询方法 - 使用自动映射来避免繁琐的手动和与您尝试解决的问题步骤完全无关,该映射是 - 让域模型主要关注业务操作,包括与 CUD 相关的操作,而不是读取 - 避免可重用性增加更改原因的数量 -避免封装问题 - (并由此启用 CQRS 样式架构,并可能及时分离读取和 CUD 的缩放) - 尝试找到一种非常适合应该做什么的验证方法(好读:





http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/02/15/validation-in-a-ddd-world.aspx

这是我在这种特殊情况下会采取的方法吗?

好吧,这就是我想开始讨论的内容:) 但似乎比我希望的要难(除了你们两个)。

4

2 回答 2

8

我只能根据我们自己的经验来回答。我们尝试了不同的框架(WCF RIA、Ideblade)并得出结论,框架只会让事情变得更糟。我会在下面进一步解释。

首先,您应该忘记 CRUD。只有演示应用程序有 CRUD - 现实世界的应用程序有行为。

我不建议在客户端模仿整个实体图。它们是两个独立的关注点。

您应该为每个上下文创建量身定制的 Dto。例如,假设您有一个 OrderSearchView,然后您创建一个 OrderSearchDto 并仅映射您需要的字段。在 EditOrderView 中,您将改为使用 EditOrderDto - 它仅包含您需要的字段。

我真的不建议在实体和 dto 之间使用自动映射工具。因为在 dto 和实体之间通常不存在一对一的关系。dto 通常由不同的多个后端实体构建。无论如何,映射是如此简单,所以我没有看到映射框架的意义。而且工作不是映射——它是编写单元测试——无论如何你都必须这样做(有或没有映射框架)。

Dtos 应该与客户端技术无关。在 dto 上实施 INotifyPropertyChanged 违反了单一责任原则。它们被称为数据传输对象。相反,您在客户端创建演示者。您创建一个 EditOrderPresenter,它是 EditOrderDto 的包装器。所以 dto 将只是 EditOrderPresenter 中的一个私有成员字段。Presenter 是为在客户端层进行编辑而量身定制的——因此它通常会实现 INotifyPropertyChanged。EditOrderPresenter 通常具有与 dto 相同的属性名称。

您应该在物理上将客户端验证与服务器端的实体验证分开。小心分享!我认为客户端验证只是 GUI 调整 - 使 gui 体验更好。不要强调在 dto 和实体之间共享验证代码 - 这可能会比有用性更令人头疼。只需确保始终在服务器端进行验证,无论在客户端进行何种验证。有两种验证:简单属性验证和整体实体验证(dto 也是如此)。实体验证只应在状态转换时执行。查看 Jimmy Nilssons 领域驱动设计的背景知识。我不建议使用验证规则引擎——只使用状态模式。

那么更新、插入、删除呢?在我们的实现中,我们使用 WCF,而 WCF API 只有一个方法: IResponse[] Process(params IRequest[] requests); 这到底是什么意思?这意味着客户端正在向服务器发出一批请求。在服务器端,您为系统中定义的每个请求实现一个 RequestHandler。然后你返回一个响应列表。确保 Process() 方法是一个工作单元(~一个事务)。这意味着如果批处理中的一个请求失败 - 所有请求都将失败 - 这将导致事务回滚 - 并且不会对数据库造成伤害。(不要在响应处理程序中使用错误代码 - 而是强制转换异常。)

我建议您查看 Agatha 消息服务器。Davy Brion 有关于消息传递层的精彩博文。在我们公司,我们选择实现自己的消息服务器——因为我们不需要 Agatha 提供的所有东西,所以我们做了一些语法改进。无论如何,实现消息服务器并不难——而且这是一次很好的学习体验。链接http://davybrion.com/blog/

那你怎么处理Dto的。好吧,您永远不会更新它们,但您会在客户端更改它们以获得对 gui 的适当反馈。因此,您可以让演示者以正确的顺序跟踪 dto(请求)发生的每一件事。这将是您的 requestBatch。然后将 requestbatch 发送到 WCF 上的 process-command - 然后请求将在服务器端“重播”并由 requesthandlers 处理。这实际上意味着您永远不会更新 dto。但是演示者可能会在客户端编辑 dto 以提供适当的 gui 反馈。演示者的工作也是跟踪所有已完成的编辑,以便将它们作为请求批次发回服务器(请求与编辑的顺序相同)。考虑以下场景,您检索现有订单,进行编辑,然后您将更改提交回数据库。这将产生两批,一批用于获取订单,另一批用于提交更改。
RequestBatch 1:GetOrderByIdRequest

(..然后用户编辑数据..)

ReqeuestBatch 2:
StartEditOrderRequest ,状态更改以编辑方式,轻松验证
AddConsigneeToOrderRequest
ChangeEarliestETDOnOrderRequest ,无需验证最新的 ETD!
DeleteOrderlineRequest
ChangeNumberOfUnitsOnOrderlineRequest
EndEditOrderRequest ,状态更改为原始状态,在这里进行实体验证!
GetOrderByIdRequest ,以便使用最新更改更新 gui。

在服务端,我们使用 NHibernate。Nhibernate 使用一级缓存来避免繁重的数据库负载。因此,同一工作单元(requestbatch)中的所有请求都将使用缓存。

每个请求应该只包含最少量的数据来完成这项工作。这意味着使用 OrderId + 一些其他属性而不是整个 dto。关于乐观更新,您可以将一些 oldValues 与请求一起发送 - 这称为并发集。请记住,并发集通常不包含很多字段。因为在此期间更新已更改的订单并不一定意味着您将有加薪条件。例如。在此期间添加和订购收货人由另一个用户编辑并不意味着您有加薪条件。

那么,这不会导致大量的工作。你肯定会有更多的课程,但每个课程都会很小并且有一个单一的职责。

顺便说一句,我们在一个中型项目中尝试了 WCF RIA 服务。它并没有那么顺利。我们必须在框架周围找到方法(黑客)来做我们想做的事。而且它还基于代码生成——这对于构建服务器来说是非常糟糕的。此外,您永远不应该通过图层进行可见性。您应该能够在不影响客户端层的情况下更改支持的实体。对于 RIA,这非常困难。我认为 OData 与 WCF RIA 属于同一类别。

如果您需要在客户端构建查询,则使用规范模式 - 不要使用 iqueryable - 那么您将独立于后端实体。

祝你好运。
推特:@lroal

于 2010-09-30T10:55:55.700 回答
2

有趣的问题:)

如果你从几个原则开始:

  • 尝试减少通过网络发送的数据量
  • 尽量减少编写管道代码所花费的时间
  • 尝试提高可测试性

基于此,我会:

  • 使用 POCO 对象传输数据。数据集包含很多您可能不需要的信息
  • 使用 Entity Framework POCO 进行数据库访问,节省您从合约对象到数据对象的映射
  • 将验证放在辅助类中,易于测试,并支持共享代码模型

在我们的项目中,与企业库和数据集相比,我们使用实体框架节省了时间。

在服务器端和客户端对象上,您可以尝试:

  • 客户端对象继承服务器端对象并实现 INotifyPropertyChanged
  • 将客户端和服务器端对象放在单独的 dll 中,这样服务器上就没有未使用的代码
  • 使用 Automapper 在两种类型之间进行映射。(可能是使用接口的更好方法)
于 2010-09-29T16:28:09.720 回答