DOP 和 DVMH






4.93/5 (9投票s)
本文讨论了可分发可观察 POCO (DOP) 以及为充分利用它们而构建的类似 MVVM 的设计模式。
背景
当我撰写文章《在 WPF 中编写多人游戏》时,我使用了一种远程处理架构(也称为多层架构),我将其视为可远程处理的 MVVM。事实上,当我发表文章时,我并没有使用真正的 MVVM,只使用了一些它的概念,即使我在最新版本的游戏中使用 MVVM,我也从未发布过。
无论游戏或以前的架构运行得如何,我都不认为它是理想的。真正的问题是我将游戏组件绑定到游戏框架,所以如果我想要不同类型的应用程序,我就会得到很多未使用的类,并且需要对某些特定部分进行变通。
因此,我决定创建一个新的架构,我认为它是 MVVM 的真正演变,使其以“可分发”的方式工作(即,如果需要,能够让不同的层在不同的计算机上工作),能够创建普通应用程序和游戏。
DOP - 可分发可观察 POCO
DOP 意为 Distributable Observable POCO(可分发可观察 POCO),而 DOP 的使用是新架构的核心。我们可以说,一个 DOP 是任何只公开公共属性、正确实现 INotifyPropertyChanged
(这是使其可观察的原因),并且如果通过使用默认构造函数和设置相同属性在两个不同的应用程序中创建,则它们是等效的类。这意味着它不能在其构造函数中使用客户端特定数据(例如用当前日期和时间设置属性),不能在属性获取时使用奇怪的代码,不能有方法或事件(除了属性更改),也不能在属性设置时验证值。方法、事件和属性设置验证必须由另一个类完成。
正确实现 INotifyPropertyChanged
的意思:它们应该只在属性确实发生更改时才通知。将属性设置为其已有的值不应触发事件。这看起来很傻,但对于避免最终导致堆栈溢出的“循环更新”至关重要。
为什么叫 DOP?
我最初不确定是使用 Distributable Data-Transfer Object(因此是 DDTO)还是 DOP。然而,DTO 不能在其属性集中有任何特殊之处。它们只是充满了可以直接获取和设置的属性的对象,提供通知将意味着改变 DTO 的含义。但是使 POCO 可分发和可观察不会损害其基本定义,只会为其添加一些额外的约束。
实际上,POCO 是一个非常麻烦的定义。POCO 意为 Plain Old CLR Object(很多时候也称 Plain Old C# Object)。那个“plain old”的目的是说它不包含任何框架特定的特性,因此它不应该有任何框架特定的数据类型、属性,也不应该从框架特定的类继承或实现框架特定的接口。然而,POCO 的定义非常麻烦,因为显然它最初是用来描述“持久化无关”的数据库对象,但后来它们被限制在数据库中,而实际上它们不应该被限制。此外,有些框架(如 Entity Framework)可能要求属性声明为 virtual,因为框架会继承该类以添加一些行为,但是,嗯,这个术语是 Plain Old CLR Object,而不是 Plain Old CLR Class。我们应该能够创建不继承任何东西的实例,然后将它们交给框架。
所以,总而言之,DTO 限制太多。POCO 过于开放。所以我决定创建一个更受限制的 POCO,它不像 DTO 那样有限。事实上,DOP 的精神是,负责更改它的代码将始终能够做到,并且任何对其当前状态感兴趣的人都会收到有关更改的通知。
使用 DOP
DOP 本身不依赖于任何框架(我不认为实现 INotifyPropertyChanged
算是依赖于框架)。如果我们要可视化一个 DOP,我们只需为其编写一个 Data-Template,一切都会正常工作,毕竟 DOP 必须包含所有需要可视化呈现的信息。
然而,我们通常希望“操作”这些 DOP,更改其内容、执行方法和应用验证,这些都必须在其他地方完成。因此,在没有任何架构的情况下,我们可以在另一个类中简单地创建接收 DOP 作为输入的方法,可能还有其他参数,并执行所需的操作,例如进行验证、将数据保存到数据库、生成异常和更改 DOP。然后我们遇到以下问题:我们如何保证这个其他类将被用于操作 DOP,并且 DOP 的用户不会直接更改它们,将无效值放入其中并绕过所需的验证?
嗯,这就是我们拥有新模式和支持它的框架的地方。
DVMH - DOP、视图、消息和处理程序
MVVM 与 DVMH 最大的区别在于,Model 被拆分为 DOP(属性)和消息(既代表事件也代表方法)。消息实际上是调用方法或生成事件的“请求”。要执行这些消息,我们需要处理程序。
每个字母的详细信息
- D - DOP:表示对象的 所有公共属性。视图可以很容易地在 DOP 的基础上构建,以在屏幕上显示它,即使 DOP 从未知道视图;
- V - 视图:目的是在屏幕上呈现 DOP。如果视图想要更改 DOP,它需要向“远程端”发送消息请求更改;
- M - 消息:任何可以发送到“远程端”的东西。考虑到在真实的远程环境中需要数据序列化,并且我们应该使用对象而不是
string
消息,因此所有消息都被期望是可序列化的 DTO(实际上任何 DTO 都是可序列化的,但许多序列化框架需要一些特定的东西,例如[Serializable]
属性才能知道); - H - 处理程序:是处理给定消息的代码。考虑到不同的“上下文”,对于同一条消息,它可以是不同的代码。我们可以说
Click
是一个很好的消息示例,可以根据点击了哪个控件进行不同的处理。
DVMH 支持架构
为了使 DVMH 模式工作,我们至少需要一种发送消息和处理这些消息的方式。我决定将这个最小架构放入一个框架中。
嗯,我首先构建了该框架的接口,因为我不想将用户绑定到我的框架实现,只提供“架构”以便能够正确使用 DOP 和 DVMH 模式。
即使我们使用本地框架,其理念也是在数据呈现和数据操作之间实现良好的隔离,这通常非常适合客户端和服务器的远程架构。因此,客户端呈现 DOP,服务器操作它们。
考虑到客户端和服务器,我们有
客户端
客户端“参与”服务器“房间”。作为参与者,客户端接收所有添加、删除和修改对象的通知,并在必要时更新这些 DOP 的本地副本,以便它们可以在屏幕上正确呈现。
服务器
服务器必须至少有一个房间,并且必须接受参与者连接,选择将这些参与者放置在哪个房间。重要的是要注意,服务器不会接收真正的参与者对象,只会接收一个与该参与者通信的对象。
服务器可以随时向房间添加和移除组件,并将参与者从一个房间更改到另一个房间。一旦参与者“链接到房间”,他们将收到所有现有组件的信息,并能够观察这些组件发生的更改。
当然,服务器可以向客户端发送消息,客户端也可以向服务器发送消息。
更准确地说,参与者可以向他们连接的房间发送消息,而服务器可以查看绑定到房间的所有活动“通信对象”,并使用这些对象向参与者发送消息。
为了给涉及的对象命名,在客户端我们有
- DopParticipant:接收服务器端发生的更改通知(例如何时添加、移除对象以及参与者何时被放入另一个房间)并具有 PostMessage 方法,能够向服务器发布消息的对象。
在服务器中,我们有
- DopRoom:负责管理组件和参与者并接收来自参与者消息的对象;
- DopCommunicationToParticipant:已连接参与者的服务器表示,可用于向参与者发送私有消息。
框架
正如我已经说过的,我首先通过接口构建了框架。这些接口呈现了 IDopParticipant
、IDopRoom
和 IDopCommunicationToParticipant
。
事实上,还有一些其他接口,因为我决定将处理房间组件的方法放在另一个专用接口中(一个用于服务器端,能够添加和移除组件;一个用于客户端,仅限于枚举和获取这些组件的信息)。
然后,我实现了这些接口的两种版本。一种是本地的,作为松散耦合 DVMH 的支持,没有序列化和远程处理开销;另一种是远程的,允许在一个计算机上创建房间,并从另一台计算机访问这些房间和组件。
远程框架需要消息端口来发送消息,并在客户端创建服务器 DOP 的副本。这种副本实际上有助于强制执行 DVMH 模式,因为对客户端 DOP 的更改不会影响原始 DOP,这将自然地强制开发人员使用消息而不是直接更改 DOP,并避免客户端在不经过所需验证的情况下陷入无效状态。考虑到这一好处,我决定创建一个本地消息端口,以便可以强制执行真正的 DVMH 架构,但其中不涉及 TCP/IP(或类似)通信。
对消息端口的需求再次是编写“框架”的情况。远程库不附带任何消息端口,只附带消息端口的接口,但我还提供了另一个库(最小实现),它实现了本地消息端口和 TCP/IP 消息端口,使用默认的 .NET 二进制序列化来发送消息。对于专业的分布式应用程序,我强烈建议实现另一个消息端口。
库
那么,为了清楚起见,让我们看看有哪些库以及它们包含什么。
- Pfz.DisposableObservablePoco:这个库只由接口和委托组成。它说明了 DOP 框架应该做什么,包括“服务器”端(房间和与参与者的通信)和客户端(参与者);
- Pfz.DisposableObservablePoco.Local:这个库是本地 DOP 框架的实现。它实际上不将对象从房间复制到参与者,它只是将现有对象通知参与者。这几乎没有开销,如果您想快速查看多层应用程序的本地视图,这很有用。强烈建议首先开发一个远程解决方案,以确保您使用正确的客户端/服务器隔离(因为此版本允许违反它),然后使用此本地框架以获得最小的本地开销;
- Pfz.DisposableObservablePoco.Remote:我期望用户实际尝试使用的库。它具有使用分布式 DOP 的架构,具有创建作为服务器组件副本的客户端组件所需的复制逻辑。然而,它不附带默认的消息端口实现,只附带支持消息端口的额外接口。但别担心,请看下一个库;
- Pfz.DisposableObservablePoco.Remote.MinimalImplementation:这是消息端口的最小实现,因此您可以尝试 Remote 库。它附带一个本地消息端口(它比使用真正的本地 DOP 框架开销更大,但它包括所有隔离和复制步骤,以确保您编写一个真正解耦的应用程序)和一个 TCP/IP + 普通 .NET 序列化消息端口。我真的相信,对于专业应用程序(特别是如果它们对世界开放),使用更好的序列化器或更好的消息端口是必须的,但这一个很好的起点,并允许我们测试 DOP 框架和创建 LAN 应用程序;
- Pfz.DisposableObservablePoco.Common:这个库被本地和远程实现使用,它包含我认为大多数用户会想要的类,这些类将在下一节中介绍。
“额外内容”
我认为许多开发人员会想要两样东西:动态 DOP 和某种 DOP 生成器,以避免手动编写 DOP 类。毕竟,即使这些类很简单,但编写带有正确验证和通知的属性集可能会很无聊且容易出错。
因此,在 Pfz.DistributableObservablePoco.Common 库中,您将找到 DynamicDop
和 AbstractDopCreator
类。
DynamicDop
是一个与ExpandoObject
非常相似的类,在使用dynamic
关键字或通过GetValue
和SetValue
方法时可访问,有效地“根据需要创建属性”。如果您不想与客户端共享服务器组件,这很理想,因此客户端使用DynamicDop
并支持来自服务器的所有属性就足够了。当然,如果需要,您也可以在服务器上使用它。AbstractDopCreator
能够在运行时实现 DOP 抽象类或接口,使用正确的模式,其中包括在继续之前验证值是否真正更改,并将所有EventArgs
缓存到静态字段中,以便在每次属性更改时不会创建新实例。考虑到 DOP 不应该有任何特定的构造函数逻辑、方法或事件(除了PropertyChanged
),我能想到避免使用此运行时生成器的唯一原因是不支持运行时代码生成的受限环境或需要动态 DOP。
INotifyPropertyChanged 不是强制性的
我一开始说 DOP 必须是可观察的,这通过实现 INotifyPropertyChanged
接口来实现。实际上,DOP 框架只依赖于 DOP 是可观察的这一事实,它们不依赖于 INotifyPropertyChanged
本身。框架为每个 DOP 类型使用一个 DOP Manipulator,该操作器通过委托找到,因此您可以提供不同的操作器,并且如果对象在不实现 INotifyPropertyChanged
的情况下是可观察的,那么只要该操作器知道如何观察属性更改就足够了。
我个人很希望我的 DOP 不实现 INotifyPropertyChanged
,但考虑到 WPF 使用该接口,我仍然在我的 DOP 中实现它。但我不知道……也许您使用了一个不依赖 INotifyPropertyChanged
的不同框架,因此只要您创建了一个能够观察那些不同 DOP 的 DOP Manipulator,您就可以自由地忽略该接口。
示例
这个示例是一个小游戏(我们可以说它是 Shoot'em Up .NET 的一个非常简化版本),它使用了 DVMH 模式。它不是很完整,即使它遵循 DVMH,也不是良好编程实践的最佳示例,因为我使用了一些静态变量。但是,嗯,它只是一个小样本,而不是一个用该模式构建的杀手级应用程序。
我将所有重要的代码都放在客户端和服务器的库中。客户端部分有效地简化为将 DOP 放到屏幕上,拥有数据模板并处理箭头键 + 空格(并设置“信号”对象)。游戏本身在服务器中运行。
我为客户端和服务器使用了库,这样我就可以重用相同的代码来构建独立应用程序和客户端/服务器应用程序。即使独立应用程序同时包含客户端和服务器,客户端代码对服务器代码一无所知,服务器代码对客户端代码也一无所知。
我知道我创建了许多程序集,但我认为尝试遵循程序集的模式极其重要。将所有消息对象、DOP 和可能的一些资源放在一个公共程序集中(我将图像放在公共程序集中,因为客户端呈现它们,服务器使用它们进行碰撞检测)。然后,将所有与“视图”相关的代码放在客户端库中。将所有实际工作的代码放在服务器库中。而且,如果您不想要客户端/服务器应用程序,只想要 DVMH 模式,请创建一个使用所有这些库的可执行文件。即使最终所有内容都在同一个可执行文件和应用程序域中,代码也将是松散耦合的,因为客户端类不了解服务器类(反之亦然)。
小细节
到目前为止,我一直尽量不关注细节。我提供了一个具有最小实现的框架,它已经有局限性,即使其中一些可以通过不同的实现轻松解决,我仍然选择保留它们,以确保使用一个框架编写的应用程序可以轻松地被另一个框架使用。
那么,让我们看看其中一些细节
仅简单数据
DOP 属性的类型必须是简单数据,而不是任何可能包含其自身可修改属性的对象类型。
此外,考虑到可能的序列化限制(我们可以说这是另一个实现细节),我们应该尝试在 DOP 属性中仅使用原始类型、字符串或不可变的、可序列化的类型。因此,DOP 是可修改的,但放入 DOP 属性中的对象的内容不能是可修改的。
引用其他 DOP
考虑到 DOP 具有可修改的属性,我们如何在 DOP 中拥有一个引用另一个 DOP 的属性?
这个问题的答案是使用 ID。添加到房间的每个组件都被赋予一个 ID,该 ID 可用于在房间内再次找到该组件。不期望 ID 跨不同房间工作,并且 ID 本身的类型是实现相关的,因此它可以是 string
、int
、GUID
等。
由于本地实现不使用序列化,本地房间生成的 ID 不是可序列化对象(至少不被 .NET 二进制格式化程序序列化),但房间和参与者对象能够通过这样的键找到正确的组件。
因此,当一个 DOP 引用另一个 DOP 或发送应引用 DOP 的消息时,请使用 DOP 的 ID,而不是 DOP 本身。
组件定向消息
DOP 框架本身只能从参与者发送消息到房间,或者从房间发送消息到参与者。它们不能向特定组件发送消息。
这样做是故意的,因为它简化了 DOP 框架,但考虑到消息可以包含组件的 ID,我们总是可以向房间发送消息,告知它应该“分派”给哪个组件。
实际上,可以构建另一个专门用于处理消息分派到组件的框架。我还没有这样做,但我认为对于大型应用程序来说,拥有一个可配置的处理程序非常重要。幸运的是,任何 DOP 框架都可以通过将 MessageReceived
方法委托给这样的框架来使用消息分派框架。
刷新
通过 PostMessage
方法发送的消息和属性更改不保证立即发送到远程端。必须调用 Flush()
。
由于本地实现中不需要这样的调用,因此如果本地应用程序稍后编译为远程应用程序,则可能无法按预期工作。这就是为什么我认为始终从使用远程框架开始,如果需要或想要,稍后使用本地框架来加快速度非常重要。
出于性能原因,我决定不自动刷新。
对象的处置
在我看来,对象的处置总体上仍然是一个棘手的话题。.NET 拥有一个 垃圾回收器,它主要解决了与内存泄漏和访问已删除对象相关的问题(或者换句话说,它解决了当我们不确定一个对象还有多少引用时,与删除顺序相关的问题,这通常会导致内存泄漏或在仍有引用时删除对象)。然而,有无数种情况 垃圾回收器 是不够的。这通常表现为文件和网络连接之类的事情,但它走得更远,因为放入房间的任何对象在房间存在期间都无法被回收,即使它不再需要或使用了。
对于房间中的对象,没什么好说的。开发人员有责任在不再需要对象时立即将其删除。DOP 不应有析构函数或任何特定的处置逻辑,因此只需使它们可供回收(将它们从活动列表中删除,例如房间的组件),.NET 就会处理它们。
但我们仍然有 房间、参与者 和 消息端口。这些对象的处置支持应该存在、可观察且线程安全。我知道,许多人认为 Dispose()
方法不应该被多个线程调用,并且如果 Dispose()
必须是线程安全的,那么架构中就存在错误,但这在双工通信场景中非常常见。连接可能随时被远程端关闭……甚至可能由于物理电缆断开而关闭。这种连接丢失,在需要活动连接的消息端口上,意味着消息端口可能随时被处置(因此需要线程安全)。此外,使用已处置 消息端口 的参与者是无用的,因此它应该与消息端口一起处置,这意味着它应该被告知这种处置,也可能在随机时刻被处置。
为了支持这一点,我创建了 IDopDisposable
接口。它是一个 IDisposable
,具有以下额外成员
- IsDisposed - 一个返回布尔值的属性,指示对象(房间、参与者等)是否已处置;
- DisposeException - 连接丢失实际上会抛出异常,但我们不允许在应用程序中抛出此类异常,我们只是处置对象并存储异常,可以通过此属性访问。使用已处置对象的方法应抛出 ObjectDisposedException,并将 DisposeException 作为其内部异常;
- Disposed - 当对象被处置时调用的事件。这就是参与者可以在 消息端口 被处置后立即被处置的方式。
并且,为了使事情完全线程安全,重要的是要注意,如果对象已处置,注册 Disposed
事件应立即调用委托,因为用户无法保证在检查对象仍然存在后对象不会立即处置。
小问答
为什么服务器是唯一可以创建新 DOP 的一方?
安全。服务器可能会为其创建的对象在内存中保留数据,因此服务器应用程序的开发人员必须决定何时创建对象。如果客户端可以简单地创建驻留在服务器上的对象,那么攻击就会变得非常容易。
为什么只有服务器才能更改 DOP 属性?
再次为了安全,并强制使用消息。一个客户端不应该能够更改另一个客户端可能正在使用的数据。如果两个或更多客户端认为他们可以同时更改同一个 DOP,情况可能会更糟。
然而,客户端可以更改“私有 DOP”的属性。私有 DOP 是在服务器上创建的,只能被单个参与者可见,因此该参与者可以自由更改它们。
为什么 DOP 没有方法?
因为如果支持这一点,客户端实现应重定向到服务器,而服务器实现应执行操作。将 abstract
方法放入 DOP 类并在客户端和服务器上以不同方式实现它们是可能的,但这最终会创建不同的 DOP 来表示客户端和服务器版本,而我真的希望在两侧都使用相同的 DOP 类,至少对于框架来说。
此外,普通方法通常具有同步签名,而对于分布式编程,异步处理会更好。然而,如果您真的想要拥有方法,可以在远程框架之上实现这种支持,因为客户端和服务器都可以自由提供其 DOP 创建器委托。
为什么消息没有结果?
因为同步/异步问题,也因为其理念是提供简单的接口来实现 DOP 框架。可以构建一个消息框架,能够发送消息并 await
结果,而这些结果仅依赖于 DOP 框架的接口,从而有效地将这种能力赋予任何可能的框架实现。
为什么 DOP 不能有事件?
因为事件有两面:生产者和消费者。当我们在一个类中声明一个事件时,其他类只能消费事件,而不能生产事件(尝试调用在另一个类中声明的事件,你不能,你必须使用 +=
或 -=
来注册你的事件处理程序)。考虑到 DOP 没有方法,它们将永远不会产生事件。因此,我们可以创建一个方法来允许其他类产生事件,这将回到有方法的问题,或者我们可以通过使用消息来避免这种复杂性。
客户端可以同时连接多个房间吗?
可以,但需要多个 Participant/MessagePort 对象,每个房间一个。实际上,并非每个 MessagePort 都必须拥有自己的连接,因此可以有许多不同的消息端口使用单个连接来完成它们的工作。然而,最小实现不处理这种情况,我们可以认为这是所涉及的 消息端口 的关注点,而不是 DOP 框架本身的关注点。
我可以将同一个组件添加到两个或更多房间吗?
可以,但所提供的框架不会知道该 DOP 是共享的。因此,应使用任何关于共享资源的良好实践。如果房间有自己的专用线程来处理对象,那么在访问共享组件时将需要锁。此外,房间不应期望对组件应用不同的规则,否则它们可能会看到来自另一个房间的“无效状态”,而该房间实际上认为这是有效状态。
消息和 WPF 中的命令不是一回事吗?
它们的功能非常相似,但 WPF 创建 Command 时并未考虑远程支持,并且 WPF 控件可以同时使用事件和命令。我认为将事物限制为仅消息有助于避免“混合”API,并减少创建新 DOP 框架所需的代码量。
ViewModel
通过查看 DVMH 模式,您可能会发现缺少 ViewModel。
那么,是我忘记了它,它缺失了还是被隐藏了?
我的回答是:我没有将 ViewModel 作为模式中的一项要求。仍然可以使用它,但我认为实际上有一个更好的解决方案。
当 Model 已经可观察时,ViewModel 很多时候都是多余的。它仍然可以用于“转换”某些属性数据类型,甚至可以添加 View 特定属性,但在大多数情况下,它最终会填充一些简单地重定向到 Model 的属性(或者用户违反 MVVM 设计模式并直接从 View 访问 Model)。
嗯,我不喜欢代码重复,所以我更倾向于说我们不应该有 ViewModel。那么,我们如何转换值呢?我们如何在不将 View 特定属性放入“Model”(DOP)的情况下添加 View 特定属性呢?
值转换是一个真正的问题。我个人不喜欢在 Binding
中放置转换器,因为这会将“代码内容”放入 View 中。然而,当我们有一个 View 想要以不同方式呈现 Model(例如将厘米值呈现为英寸)时,我们可以说进行转换仍然是 View 的关注点(但有些人会在 ViewModel 中放置一个已经进行英寸转换的属性)。
另一个解决方案是使用远程框架,它实际上会在客户端创建服务器组件的副本,以便在客户端和服务器上创建不同的对象(即使远程框架实际上使用本地消息端口)。常见的 DOP 仍然应该存在,但客户端可以继承常见的 DOP 并添加客户端特定属性,而服务器可以继承这些组件以放置服务器特定信息。这之所以可能,是因为服务器委派获取“类型 ID”的工作,而客户端委派通过此类 ID 创建对象的工作。因此,可以发送类型名称(例如),但在不同的命名空间中找到它们以拥有不同的客户端和服务器对象。这与我在《在 WPF 中编写多人游戏》文章中所做的非常相似,因为服务器具有控制动画的特定属性,而客户端根本不知道这些属性。
最后,我们获得了 ViewModel 的好处,例如拥有 View 特定属性,而无需编写代码来重定向所有我们根本不想更改的属性。但是,如果您真的想保留 ViewModel 并且不想为客户端和服务器拥有不同的对象,请随意使用 ViewModel,因为 DVMH 不禁止使用它。
层数、性能、可伸缩性、可用性等
DOP 中的 D 表示分布式,当我开始开发该框架时,我更关注分布式部分(它允许 N 层应用程序),而不是可以本地使用的 DVMH 模式。那么,DOP 在多层环境中实际效果如何?它们的可伸缩性如何?性能又如何?
答案再模糊不过了。它取决于很多因素。我在这篇文章中提供的框架实现将对象保存在内存中,因此我们可以说这些框架的可伸缩性不高。然而,即使 HTTP 协议是无状态的,ASP.NET 允许我们通过使用会话将状态保存在内存中。在这种情况下,问题是我们将会话中保留多少个对象。
因此,与 DOP 相比,我们在内存中保留了多少个 DOP?实际上,我们不需要观察一个房间才能进入“登录页面”。该登录页面可以完全存在于客户端,登录消息可以发送到一个共享房间。
此外,DOP 极其简单。它们可以很容易地存储在数据库中,因此不同的框架实现可以做到这一点。当然,如果这样的框架也能缓存最近使用的 DOP 以避免过多的数据库访问,那会更好,但这是一个编写可伸缩网站的每个人都必须处理的常见问题。
我们绝不能忘记预期的用途。我们是想创建完全由客户端控制,仅在需要“数据”时才与服务器通信的客户端应用程序,还是想创建只是在其他地方(可能在同一个进程中加载的另一个库中)存在的应用程序的“可视化工具”的客户端应用程序?DVMH 实际上属于第二类。这是一件好事,因为我们正在创建一个更松散耦合的类似 MVVM 的架构。因此,DVMH 架构预计不会完全可伸缩,但 DOP 是可伸缩的。它们可以在 DVMH 架构中使用,也可以在更可伸缩的不同架构中使用。
因此,我只能说 DOP 确实具有可伸缩性,而 DVMH 模式预计在 局域网 (LAN) 中或完全本地使用。所以,可以将 DVMH 与其他架构结合使用,并在该其他架构中使用 DOP 来实现真正的多层和可伸缩应用程序和游戏。