Angular 的模型-视图-演示器
复杂应用
关注点分离
模型-视图-演示者模式
Angular 的 Model-View-Presenter 概念
容器组件
混合组件
主持人
模型-视图-呈现器三角关系
改进的 Angular 应用程序
案例研究:英雄之旅
资源
相关文章
致谢
同行评审员
一个工程过程。封面照片由energepic.com在 Pexels 上发布。
原始发布日期:2018-10-17。
🇪🇸 西班牙语版本,作者:Estefania Garcia Gallardo
随着应用程序的增长,维护变得越来越困难。复杂性不断增加,但可复用模块的价值却不断提升。我们知道,在面临失败的风险之前,我们必须采取措施。
设计模式来救援!
复杂应用
复杂应用程序至少具有以下特征之一:
- 组件树中的多个组件显示同一部分应用程序状态
- 应用程序状态的几个更新源,例如:
- 多个用户同时互动
- 将更新状态实时推送到浏览器的后端系统
- 计划的后台任务
- 接近传感器或其他设备传感器
- 应用程序状态更新非常频繁
- 大量组件
- 用多行代码构建的组件,让人想起过去的Big Ball of Mud AngularJS 控制器
- 组件的圈复杂度较高——逻辑分支或异步控制流高度集中
同时,我们希望应用程序具有可维护、可测试、可扩展和高性能。
复杂的应用程序很少具备所有有价值的特性。我们无法在满足高级项目需求的同时避免所有复杂性,但我们可以设计应用程序,最大限度地发挥其宝贵的特性。
关注点分离
我们可以将关注点分离(SoC) 视为应用程序的划分。我们根据系统关注点对逻辑进行分组,以便能够一次专注于单个关注点。从最顶层来看,关注点分离是一门架构规范。在日常开发中,它几乎需要我们熟记在心,清楚地知道哪些部分应该放在何处。
我们可以垂直、水平或两者兼而有之地对应用程序进行切片。垂直切片时,我们按功能对软件工件进行分组。水平切片时,我们按软件层进行分组。在我们的应用程序中,我们可以将软件工件分类到以下水平层或系统关注点中:
Horizontal layer | Examples | |
---|---|---|
Business logic | Application-specific logic, domain logic, validation rules | |
Persistence | WebStorage, IndexedDB, File System Access API, HTTP, WebSocket, GraphQL, Firebase, Meteor | |
Messaging | WebRTC, WebSocket, Push API, Server-Sent Events | |
I/O | Web Bluetooth, WebUSB, NFC, camera, microphone, proximity sensor, ambient light sensor | |
Presentation | DOM manipulation, event listeners, formatting | |
User interaction | UI behaviour, form validation | |
State management | Application state management, application-specific events |
Horizontal layer | Examples | |
---|---|---|
Business logic | Application-specific logic, domain logic, validation rules | |
Persistence | WebStorage, IndexedDB, File System Access API, HTTP, WebSocket, GraphQL, Firebase, Meteor | |
Messaging | WebRTC, WebSocket, Push API, Server-Sent Events | |
I/O | Web Bluetooth, WebUSB, NFC, camera, microphone, proximity sensor, ambient light sensor | |
Presentation | DOM manipulation, event listeners, formatting | |
User interaction | UI behaviour, form validation | |
State management | Application state management, application-specific events |
同样的规则也适用于我们的 Angular 组件。它们应该只关注展现层和用户交互层。这样做的好处是,我们能够降低系统各个组件之间的耦合度。
当然,这个过程需要很多的纪律,因为我们会添加额外的抽象层,但最终结果的宝贵特性弥补了这一点。请记住,我们只是创建了本来就应该存在的抽象。
模型-视图-演示者模式
模型-视图-呈现器(通常缩写为 MVP)是一种用于实现应用程序用户界面 (UI) 的架构式软件设计模式。我们使用它来最小化类、函数和模块(软件构件)中难以测试的复杂逻辑。特别是,我们避免了特定于 UI 的软件构件(例如 Angular 组件)的复杂性。
与其衍生的模型-视图-控制器模式类似,模型-视图-演示器模式将表示层与领域模型分离。表示层通过应用观察者模式来响应领域模型的变化,正如 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(也称为“四人帮”)在其经典著作《设计模式:可复用面向对象软件的基础》中所描述的那样。
在观察者模式中,主体维护一个观察者列表,当状态发生变化时,它会通知这些观察者。这听起来熟悉吗?你猜对了,RxJS 就是基于观察者模式的。
视图除了数据绑定和组件组合的形式外,不包含任何逻辑或行为。当用户交互发生时,它将控制权委托给演示者。
呈现器批量处理状态更改,以便用户填写表单时只产生一次大的状态更改,而不是多次小的更改。例如,每个表单更新一次应用程序状态,而不是每个字段更新一次。这使得状态更改的撤消或重做变得非常容易。呈现器通过向模型发出命令来更新状态。由于观察者同步机制,状态更改会反映在视图中。
角度变化
受到原始模型-视图-演示器模式和变体的启发,我们将创建非常适合 Angular 平台及其关键 UI 构建块(组件)的软件工件。
理想情况下,Angular 组件只关注呈现和用户交互。实际上,我们必须严格遵循原则,确保组件只关注向用户呈现应用程序状态的一部分,并允许用户影响该状态。
本文介绍的模型-视图-呈现器 (Model-View-Presenter) 变体是对封装呈现器 (Encapsulated Presenter) 样式的一种演绎。然而,我们的呈现器不会引用其视图。相反,我们将使用可观察对象将呈现器连接到模型和视图,从而使呈现器可以与其视图隔离进行测试。
在应用模型-视图-呈现器 (MV-Presenter) 模式时,我们倾向于使用监督控制器 (Supervising Controller) 的方式。我们的视图(Angular 组件)仅依赖其呈现器 (Presenter) 进行用户交互。由于呈现器被其视图封装,因此数据和事件都会在某个时刻流经组件模型。
借助组件模型,我们的 Presenter 将用户交互转换为特定于组件的事件。该事件随后被转换为命令并发送给模型。最终的转换由稍后介绍的容器组件处理。
我们的演示器将具备演示模型的一些特征,即包含演示逻辑,例如布尔值或可观察属性,用于指示是否应禁用 DOM 元素。另一个示例是指示应使用哪种颜色渲染 DOM 元素的属性。
我们的视图绑定到演示器的属性上,只需将其所代表的状态投射出来,无需任何额外的逻辑。最终结果是一个包含非常简单的组件模板的瘦组件模型。
Angular 的 Model-View-Presenter 概念
为了将 MVC 模式应用于 Angular 应用,我们将引入一些深受 React 社区启发的概念。为了方便本文讨论,我们的组件将属于以下三个类别之一:
React 开发者多年来一直在从混合组件中提取展示组件和容器组件。我们可以在 Angular 应用程序中使用相同的概念。此外,我们还将介绍 Presenter 的概念。
展示组件
展示组件是纯粹的展示和交互视图。它们向用户呈现部分应用程序状态,并允许用户影响其状态。
除了演示者之外,演示组件根本不了解应用程序的任何其他部分。它们拥有一个数据绑定 API,该 API 描述了它们处理的用户交互以及它们所需的数据。
为了消除对 UI 进行单元测试的大部分原因,我们将展示组件的复杂性保持在绝对最小值,包括组件模型和组件模板。
容器组件
容器组件将应用程序状态片段暴露给展示层组件。它们通过将组件特定的事件转换为非展示层的命令和查询,将展示层与应用程序的其余部分集成在一起。
通常,容器组件和展示组件之间存在一对一的关系。容器组件具有与其展示组件的输入属性相匹配的类属性,以及响应通过展示组件的输出属性发出的事件的方法。
混合组件
如果一个组件既不是容器组件也不是展示组件,那么它就是混合组件。对于现有的应用程序,它很有可能由混合组件组成。我们之所以称之为混合组件,是因为它们具有混合的系统关注点——它们包含属于多个水平层的逻辑。
如果您偶然发现一个组件,除了包含用于演示的域对象数组之外,还直接访问设备摄像头、发送 HTTP 请求并使用 WebStorage 缓存应用程序状态,请不要感到惊讶。
虽然这种逻辑在应用程序中是预期的,但将其全部集中在一个地方会使其难以测试、难以推理、难以重用且紧密耦合。
主持人
行为逻辑和复杂的展示逻辑被提取到Presenter中,从而得到一个简单的展示组件。Presenter 没有 UI,通常没有或只有少量的依赖注入,因此易于测试和推理。
演示器很少了解应用程序的其余部分。通常只有一个演示组件引用一个演示器。
模型-视图-呈现器三角关系
这三个软件组件组合在一起,构成了我们所说的“模型-视图-呈现器”三角。模型(由容器组件表示)是应用程序的状态,显示给用户,供用户浏览和修改。
视图由展示组件表示,是一个薄的用户界面,它呈现应用程序状态并将用户交互转换为特定于组件的事件,通常将控制流重定向到展示器。
演示者通常只是一个类的实例,它完全不知道应用程序的其余部分。
数据流
让我们直观地了解数据和事件如何通过模型-视图-演示器三角关系流动。
数据沿组件树向下流动
在图 2 中,服务中发生了应用程序状态变化。容器组件会收到通知,因为它订阅了服务上的一个可观察属性。
容器组件将发出的值转换为最适合展示组件的形状。Angular 为展示组件绑定的输入属性分配新的值和引用。
展示组件将更新的数据传递给演示器,演示器重新计算展示组件模板中使用的附加属性。
数据现在已经完成了沿组件树的流动,并且 Angular 将更新的状态渲染到 DOM,并以列表的形式显示给用户。
事件沿组件树向上流动
图 3 中,用户点击了一个按钮。由于模板中存在事件绑定,Angular 将控制权交给了展示组件模型中的事件处理程序。
用户交互被呈现器拦截,呈现器将其转换为数据结构,并通过可观察属性将其发出。呈现组件模型观察变化,并通过输出属性发出值。
由于模板中存在事件绑定,Angular 会将组件特定事件中发出的值通知给容器组件。
现在事件已完成沿组件树的向上流动,容器组件将数据结构转换为传递给服务方法的参数。
在执行更改应用程序状态的命令后,服务通常会在其可观察属性中发出状态变化,并且数据再次沿组件树流动,如图 2 所示。
改进的 Angular 应用程序
有些人会认为我们新的 UI 架构是过度工程的过度复杂结果,而实际上我们剩下的只是许多简单的模块化软件。模块化软件架构使我们能够实现敏捷。这种敏捷并非指敏捷流程和仪式,而是指变更成本方面的敏捷。在处理客户需求变化时,我们采取主动而非被动的方式,而不是不断增加技术债务。对于一个紧密耦合、难以测试、需要数月时间才能重构的系统来说,要达到这种敏捷程度非常困难。
模块化软件架构使我们变得敏捷。
我们不会让技术债务不断累积,而是积极主动地应对客户需求的变化。如果系统紧密耦合、难以测试,需要耗费数月时间进行重构,那么实现这种敏捷性将非常困难。
可维护
尽管最终的系统由许多活动部件组成,但每个部件都非常简单,并且只解决一个系统问题。此外,我们有一个清晰的系统,明确了每个部件的分配。
可测试
我们尽量减少 Angular 特定软件组件中的逻辑,因为它们通常测试起来既困难又缓慢。由于每个软件组件只关注一个系统问题,因此它们很容易推理。这些假设我们可以轻松地在自动化测试中验证。
UI 测试极其困难且缓慢,Angular 也不例外。使用模型-视图-演示器 (MVP) 架构,我们可以将演示组件中的逻辑量最小化到几乎不值得测试的程度。相反,我们可以选择完全跳过单元测试,依靠开发工具、集成测试和端到端测试来捕获简单的错误,例如拼写错误、语法错误以及未初始化的属性。
可扩展
功能可以彼此独立地开发。即使是位于不同水平层的软件构件,也可以独立开发和测试。我们清楚地知道每部分逻辑的归属。
现在我们可以独立地开发各个层,从而区分技术前端开发和可视化前端开发。有的开发人员擅长使用 RxJS 实现行为,有的则热衷于后端集成,还有的开发人员则热衷于完善设计,并用 CSS 和 HTML 解决可访问性问题。
由于我们可以独立开发功能,因此任务可以在团队之间分离。一个团队负责产品目录,另一个团队负责处理电商系统中购物车的问题和新功能。
高性能
适当的关注点分离通常能带来高性能,尤其是在表现层。性能瓶颈很容易被追踪和隔离。
通过OnPush
变化检测策略,我们最大限度地减少了 Angular 变化检测周期对应用程序性能的影响。
案例研究:英雄之旅
我们从 Angular.io “英雄之旅” 教程的结尾开始。我们选择它作为起点,是因为它是 Angular 开发者们普遍熟悉的教程。
《英雄之旅》教程最终代码中的所有组件都是混合组件。这一点显而易见,因为它们都没有输出属性,但其中一些会改变应用程序的状态。
在相关文章中,我们将逐步讲解如何在这些组件中应用 M-V-Presenter 模式,并提供大量实际操作的代码示例。我们还会讨论在 M-V-Presenter 三元组中需要测试哪些行为。
您会注意到,我们没有改变应用程序的任何功能或行为,而只是将其 Angular 组件重构为更专业的软件工件。
虽然这些文章只讨论了《英雄之旅》的一些组件,但我已经将模型-视图-演示器模式应用于整个应用程序,并在此 GitHub 存储库中添加了容器组件和演示器的测试套件。
先决条件
除了本文介绍的概念之外,我希望您熟悉一些关键的 Angular 概念。“Model-View-Presenter”概念在相关文章中有详细解释。
我希望你对 Angular 组件有深入的理解,例如数据绑定语法以及输入输出属性。我还假设你具备RxJS 的基本知识,例如对可观察对象、主题、操作符和订阅有所了解。
我们将构建独立的单元测试,并使用 Jasmine 间谍程序对服务依赖项进行存根。存根和其他测试替身并非理解测试的关键。请专注于测试用例,并尝试理解我们测试测试所执行行为的原因。
资源
在 StackBlitz 上浏览《英雄之旅》教程的最终代码。
下载最终的《英雄之旅》教程代码(zip 压缩包,30 KB)
浏览 GitHub 上的 Tour of Heroes—Model-View-Presenter 样式存储库。
观看我在 Angular Online Meetup #8 上的演讲“使用 Angular 的 Model-View-Presenter”:
查看我的演讲“带有 Angular 的模型-视图-演示器”的幻灯片:
相关文章
了解“模型-视图-呈现器 (Model-View-Presenter)”模式的历史,以及它的姊妹模式“模型-视图-控制器 (Model-View-Controller)”是如何被引入 Web 客户端 UI 框架的。阅读“模型-视图-呈现器 (Model-View-Presenter) 的历史”。
你是否厌倦了为 Angular 组件的状态管理和后端事务操心?那就把所有烦人的非展示性逻辑提取到容器组件中吧。阅读“ Angular 容器组件”一文。
在“测试 Angular 容器组件”中了解如何使用极快的单元测试来测试容器组件逻辑。
“使用 Angular 的展示组件”讨论了纯粹的、确定性的、可能可重用的组件,这些组件仅依赖于输入属性和用户交互触发的事件来确定其内部状态。
在“带有 Angular 的演示器”中了解如何从演示组件中提取演示器。
在“精益 Angular 组件”一文中,我们讨论了健壮组件架构的重要性。“模型-视图-呈现器”封装了几种帮助我们实现这一点的模式。
致谢
动画流程图是由我的好朋友兼软件开发人员Martin Kayser创建的。
实现高度的关注点分离是受到罗伯特“鲍勃叔叔”马丁的作品启发的努力,特别是他的书“清洁架构:工匠的软件结构和设计指南”。
将模型-视图-演示器模式应用于 Angular 应用程序的灵感来自Dave M. Bush的文章“模型视图演示器、Angular 和测试” 。
在我最初的研究中,我研究了Roy Peled在文章“ JavaScript 的 MVP 指南 —模型-视图-演示器”中描述的原始 JavaScript 的模型-视图-演示器模式。
编辑
我要感谢Max Koretskyi,感谢你帮助我把这篇文章打磨得尽可能完美。非常感谢您抽出时间分享您在软件开发社区写作的经验。
同行评审员
感谢各位审阅者,帮助我完成了这篇文章。你们的反馈非常宝贵!
鏂囩珷鏉ユ簮锛�https://dev.to/this-is-angular/model-view-presenter-with-angular-533h