当前位置: 首页 > 科技观察

软件架构编年史:MVC及其变体

时间:2023-03-20 12:01:51 科技观察

转载本文请联系一言公众号。创建可维护的应用程序始终是构建应用程序的真正长期挑战。不久前,我还在一家公司工作,该公司的核心业务应用程序是一个拥有数千家企业客户的SaaS平台。这个重要的应用程序已经开发了三年,代码文件中混合了HTML、CSS、业务逻辑和SQL。果然,在发布两年后,该公司决定彻底重写该应用程序。虽然这些情况时有发生,但今天我们中的许多人都知道这是不对的,也知道如何避免。然而,在20世纪70年代,混合职责是常见的做法,人们仍在寻找更好的解决方案。随着应用复杂度的增加,修改UI必然导致业务逻辑的修改。修改的越复杂,就越费时间,而且可能会引起更多的问题(因为修改的代码较多)。于是,MVC应运而生,它提出了前后端的“关注点分离”来解决上述问题。1979–Model-View-Controller为了解决上述问题,TrygveReenskaug在1979年提出了MVC模式来分离关注点,将UI和业务逻辑隔离开来。这种模式被应用于1973年出现的桌面图形界面的开发。MVC模式将代码拆分为三个概念单元:模型(model)代表业务逻辑;View(视图)代表UI控件:按钮、文本框等;在视图和模型查看器之间进行协调的控制器(controller)),这意味着:它决定显示哪些视图以及显示哪些数据;它将用户操作(例如按钮点击)转换为业务逻辑。模型可以是单个对象(相当无趣),也可以是对象的某种结构。-TrygveReenskaug1979,MVC原始的MVC模式还有其他重要的概念需要理解:View直接使用Model数据对象来显示数据;当Model发生变化时,会触发一个事件来立即更新View(记住,1979年2010年没有HTTP);每个View通常只与一个Controller关联;每个界面可以包含多对视图和控制器;每个Controller可以对应多个View。我现在所知道的HTTP请求-响应范例不使用原始的MVC样式。这是因为,一开始设想,数据是从View流向Controller的,这和我熟悉的一样,但另一方面,数据是直接从Model流向View,没有经过Controller.而且,在目前的请求-响应范式中,当数据库中的数据发生变化时,并不会触发浏览器中显示的View的更新(虽然可以用WebSocket实现)。要看到更新后的数据,用户需要重新发起请求,更新后的数据总会通过Controller返回。1987/2000–PAC/HierarchicalModel-View-ControllerPAC,也称为HMVC,可以在UI片段控制的上下文中实现更好的模块化拆分。比如我们会发现有一部分View被其他一些View以相同的格式使用,甚至直接在同一个View中复用。一个实际的例子是一个网页,它提供了一个可以被其他页面重用的RSS提要片段。如果使用HMVC,处理主请求的Controller会将子请求转发给其他Controller,让这些控件进行渲染,然后将它们合并到主View的渲染中。我自己在HTTP请求/响应范例的上下文中遇到过几次,但我找到了一种更简单的方法让UI对可以呈现控件的控制器进行AJAX调用。在不增加嵌套Controller调用的复杂性的情况下保持模块化优势的同时,另一个优势是这些子请求可以使用像Varnish这样的缓存。1996年——Model-View-PresenterMVC模式给编程范式打了一针强心针。但是,随着应用程序复杂性的增加,需要进一步解耦。1996年,IBM子公司Taligent发布了他们基于MVC的模式MVP。这个想法是更完全地将模型的关注点与UI分开:视图是被动的并且不知道模型;专注于轻量级控制器(Presenters),它不包含任何业务逻辑,只需调用命令和/或查询模型,将原始数据传递给视图;数据的变化不会直接触发View的更新:它总是会经过Presenter,Presenter再更新View。这样,Controller(Presenter)也可以在更新视图之前执行一些与展示相关的附加逻辑。比如同时更新其他数据,这些数据与数据库中变化的数据相关;每个View对应一个Presenter。这更接近于我现在看到的请求/响应范式:数据流总是经过Controller/Presenter。但是,Presenter仍然不会主动更新视图,它总是需要执行新的请求才能使更改可见。MVP中的Presenter也叫SupervisorController。2005–Model-View-ViewModel由于应用程序的复杂度还在不断增加,2005年微软的WPF和Silverlight架构师JohnGossman提出了MVVM模式,目标是进一步将UI设计与代码分离,并提供View给数据数据模型的绑定机制。[MVVM]是[MVC]的变体,专为现代UI开发平台而设计。在现代UI开发中,View是设计师的职责,而不是传统意义上的开发人员。[…]使用它们开发应用程序UI的工具、语言和人员与业务逻辑和数据后端有很大不同。-JohnGossman2005,Model/View/ViewModel简介patternController被ViewModel“取代”:[View]编码键盘快捷键,控件管理自己与输入设备的交互,这应该是MVC中Controller的职责(长关于控制器如何在现代GUI开发中发生变化的故事……我认为它只是从开发人员的实现中淡出。它一直在那里,我们不需要像1979年那样去想它)。——JohnGossman2005,IntroductiontoModel/View/ViewModelpatternMVVM背后的思想是:ViewModel和View一一对应;将View中的逻辑转移到ViewModel中,简化View;View使用的数据与ViewModel中的数据一一对应;将ViewModel中的数据绑定到View中的数据上,这样ViewModel中数据的变化会立即反映到View中。与原始MVC模式的情况一样,这种方法不适用于传统的请求/响应范例,因为ViewModel无法主动更新视图(除非使用Web套接字),而MVVM需要这样做。此外,根据我的经验,控制器将ViewModel的属性与View使用的数据完全匹配并不是一种常见的做法。Model-View-Presenter-ViewModel在构建复杂的云原生企业应用时,我倾向于将应用的UI结构合理设计为M-V-P-VM。这里的ViewModel是MartinFowler在2004年提出的PresentationModel。Model一组包含业务逻辑和用例的类。View是模板引擎用来生成HTML的模板;ViewModel(又名PresentationModel)从查询中接收(或从模型实体中提取)原始数据并保存模板将使用的数据。它还封装了复杂的表示逻辑以简化模板。我发现使用ViewModels非常重要,因为我们永远不想在模板中使用实体。这样我们就可以完全隔离View和Model:Model的变化(比如实体结构的变化)会上升并影响ViewModel,但不会影响模板;复杂的表现逻辑被封装到ViewModel中,不会泄露(比如在业务实体中创建一些只与表现逻辑相关的方法)到域中;模板依赖关系变得清晰,因为它们必须在ViewModel中设置。例如,暴露依赖可以帮助我们决定先从数据库加载哪些内容,避免N+1问题。Presenter接收HTTP请求,触发命令或查询,使用查询、ViewModel、模板和模板引擎返回的数据生成HTML并将其返回给客户端。所有View交互都通过Presenter。下面是我实际遇到的一个非常简单的例子:someRepository=$someRepository;$this->relatedRepository=$relatedRepository;$this->templateEngine=$templateEngine;}/***@returnmixed*/publicfunctionget(int$someEntityId){$mainEntity=$this->someRepository->getById($someEntityId);$relatedEntityList=$this->relatedRepository->getByParentId($someEntityId);返回$this->templateEngine->render('@Some/Controller/Namespace/Detail/details.html.twig',newDetailsViewModel($mainEntity,$relatedEntityList));}}M-V-C-VM_-_Controller_example.phpmainEntity=['name'=>$mainEntity->getName(),'description'=>$mainEntity->getResume(),];foreach($relatedEntityListas$relatedEntity){$this->relatedEntityList[]=['title'=>$relatedEntity->getTitle(),'subtitle'=>$relatedEntity->getSubtitle(),];}$this->shouldDisplayFancyDialog=/*...somecomplexconditionalusingtheentitiesdata...*/;$this->canEditData=/*...另一个使用实体数据的复杂条件...*/;}publicfunctiongetMainEntity():array{return$this->mainEntity;}publicfunctiongetRelatedEntityList():array{return$this->relatedEntityList;}publicfunctionshouldDisplayFancyDialog():bool{return$this->shouldDisplayFancyDialog;}publicfunctioncanEditData():bool{return$this->canEditData;}}M-V-C-VM_-_ViewModel_example.php模板有一对一-与ViewModel一一对应,即View只能被特定的ViewModel使用,反之亦然。这会让我进一步思考,或许我们可以将模板和ViewModel封装成一个View对象,更有效地将Controller与模板和ViewModel解耦,让它只依赖一个通用的View接口;但我还没有机会试验这个想法。总结在互联网上,我们还可以找到MVC的其他变种。但是,这里列出了一些我认为更有意义和/或与我的工作相关的模式。然而,我在本文中提到的模式是为桌面应用程序和/或富客户端的上下文创建的,因此它们并不总是100%匹配请求/响应范例。如果您正在开发云原生企业应用程序并使用MVC,您实际上可能正在使用更接近MVP的模式。但无论如何,我想表达的不是我们应该尊重某个特定的MVC变体或者死板地理解它们的名字,而是我们应该学习所有的模式,根据我们的需要使用和调整它们。俗话说,最终目标是高内聚低耦合:关注点分离。引用来源1979–TrygveReenskaug–MVCXEROXPARC1978-791979–TrygveReenskaug–MVC1987–JoelleCoutaz–PAC,对话设计的面向对象模型1996–MikePotel–MVP:模型-视图-演示者:Taligent编程模型适用于C++和Java2000–JasonCai、RanjitKapila、GauravPal–HMVC:开发强大客户端的分层模式2003–TrygveReenskaug–模型-视图-控制器(MVC):它的过去和现在2004–MartinFowler–演示模型2005–JohnGossman–构建WPF应用程序的模型/视图/视图模型模式简介2006–MartinFowler–监督控制器2006–MartinFowler–GUI架构2011–Mārti??Tere?ko–比MVC更适合Web应用程序的架构?2017*–Tracy-GregoryJ.Gilmore–Neverthetwinshallme等。MV*的故事2017*–技术说明–MVVMvsMVPvsMVC:差异解释2017*–维基百科–模型-视图-控制器2017*–维基百科–演示-抽象-控制2017*–维基百科–模型-视图-presenter2017*–Wikipedia–Hierarchicalmodel–view–controller2017*–Wikipedia–Model–view–viewmodel2018*–Wikipedia–Historyofthegraphicaluserinterface秦宇,A??ndroid开发者/ThoughtWorks技术教练//译者,热心致力于探索所有软件开发的方方面面,从设备到云端,从工具到实践,我喜欢通过翻译来学习和分享知识。译文包括《Kotlin实战》、《领域驱动设计精粹》、《Serverless架构:无服务器应用与AWS Lambda》和《云原生安全与DevOps保障》。