美文网首页自动化程序员
GUI应用程序架构的十年变迁(二)

GUI应用程序架构的十年变迁(二)

作者: 51reboot | 来源:发表于2018-04-23 09:53 被阅读82次

    Features of Good Architectural Pattern:何为好的架构模式

    Balanced Distribution of Responsibilities:合理的职责划分

    合理的职责划分即是保证系统中的不同组件能够被分配合理的职责,也就是在复杂度之间达成一个平衡,职责划分最权威的原则就是所谓Single Responsibility Principle,单一职责原则。

    Testability:可测试性

    可测试性是保证软件工程质量的重要手段之一,也是保证产品可用性的重要途径。在传统的GUI程序开发中,特别是对于界面的测试常常设置于状态或者运行环境,并且很多与用户交互相关的测试很难进行场景重现,或者需要大量的人工操作去模拟真实环境。

    Ease of Use:易用性

    代码的易用性保证了程序架构的简洁与可维护性,所谓最好的代码就是永远不需要重写的代码,而程序开发中尽量避免的代码复用方法就是复制粘贴。

    Fractal:碎片化,易于封装与分发

    In fractal architectures, the whole can be naively packaged as a component to be used in some larger application.In non-fractal architectures, the non-repeatable parts are said to be orchestrators over the parts that have hierarchical composition.

    • By André Staltz

    所谓的Fractal Architectures,即你的应用整体都可以像单个组件一样可以方便地进行打包然后应用到其他项目中。而在Non-Fractal Architectures中,不可以被重复使用的部分被称为层次化组合中的Orchestrators。譬如你在Web中编写了一个登录表单,其中的布局、样式等部分可以被直接复用,而提交表单这个操作,因为具有应用特定性,因此需要在不同的应用中具有不同的实现。譬如下面有一个简单的表单:

    <form action="form_action.asp" method="get">
      <p>First name: <input type="text" name="fname" /></p>
      <p>Last name: <input type="text" name="lname" /></p>
      <input type="submit" value="Submit" />
    </form>
    

    因为不同的应用中,form的提交地址可能不一致,那么整个form组件是不可直接重用的,即Non-Fractal Architectures。而form中的input组件是可以进行直接复用的,如果将input看做一个单独的GUI架构,即是所谓的Fractal Architectures,form就是所谓的Orchestrators,将可重用的组件编排组合,并且设置应用特定的一些信息。

    Reference

    Overview

    MV*

    MVC

    MVP

    • presentation-model-and-passive-view-in-mvp-the-android-way

    • Repository that showcases 3 Android app architectures(https://github.com/ivacf/archi

    MVVM

    • approaching-android-with-mvvm

    Unidirectional Architecture

    Viper/Clean Architecture

    MV*:Fragmentary State 碎片化的状态与双向数据流

    MVC模式将有关于渲染、控制与数据存储的概念有机分割,是GUI应用架构模式的一个巨大成就。但是,MVC模式在构建能够长期运行、维护、有效扩展的应用程序时遇到了极大的问题。MVC模式在一些小型项目或者简单的界面上仍旧有极大的可用性,但是在现代富客户端开发中导致职责分割不明确、功能模块重用性、View的组合性较差。作为继任者MVP模式分割了View与Model之间的直接关联,MVP模式中也将更多的ViewLogic转移到Presenter中进行实现,从而保证了View的可测试性。而最年轻的MVVM将ViewLogic与View剥离开来,保证了View的无状态性、可重用性、可组合性以及可测试性。总结而言,MV*模型都包含了以下几个方面:

    • Models:负责存储领域/业务逻辑相关的数据与构建数据访问层,典型的就是譬如PersonPersonDataProvider

    • Views:负责将数据渲染展示给用户,并且响应用户输入

    • Controller/Presenter/ViewModel:往往作为Model与View之间的中间人出现,接收View传来的用户事件并且传递给Model,同时利用从Model传来的最新模型控制更新View

    MVC:Monolithic Controller

    相信每一个程序猿都会宣称自己掌握MVC,这个概念浅显易懂,并且贯穿了从GUI应用到服务端应用程序。MVC的概念源自Gamma, Helm, Johnson 以及Vlissidis这四人帮在讨论设计模式中的Observer模式时的想法,不过在那本经典的设计模式中并没有显式地提出这个概念。我们通常认为的MVC名词的正式提出是在1979年5月Trygve Reenskaug发表的Thing-Model-View-Editor这篇论文,这篇论文虽然并没有提及Controller,但是Editor已经是一个很接近的概念。大概7个月之后,Trygve Reenskaug在他的文章Models-Views-Controllers中正式提出了MVC这个三元组。上面两篇论文中对于Model的定义都非常清晰,Model代表着an abstraction in the form of data in a computing system.,即为计算系统中数据的抽象表述,而View代表着capable of showing one or more pictorial representations of the Model on screen and on hardcopy.,即能够将模型中的数据以某种方式表现在屏幕上的组件。而Editor被定义为某个用户与多个View之间的交互接口,在后一篇文章中Controller则被定义为了a special controller ... that permits the user to modify the information that is presented by the view.,即主要负责对模型进行修改并且最终呈现在界面上。从我的个人理解来看,Controller负责控制整个界面,而Editor只负责界面中的某个部分。Controller协调菜单、面板以及像鼠标点击、移动、手势等等很多的不同功能的模块,而Editor更多的只是负责某个特定的任务。后来,Martin Fowler在2003开始编写的著作Patterns of Enterprise Application Architecture中重申了MVC的意义:Model View Controller (MVC) is one of the most quoted (and most misquoted) patterns around.,将Controller的功能正式定义为:响应用户操作,控制模型进行相应更新,并且操作页面进行合适的重渲染。这是非常经典、狭义的MVC定义,后来在iOS以及其他很多领域实际上运用的MVC都已经被扩展或者赋予了新的功能,不过笔者为了区分架构演化之间的区别,在本文中仅会以这种最朴素的定义方式来描述MVC。

    根据上述定义,我们可以看到MVC模式中典型的用户场景为:

    • 用户交互输入了某些内容

    • Controller将用户输入转化为Model所需要进行的更改

    • Model中的更改结束之后,Controller通知View进行更新以表现出当前Model的状态

    根据上述流程,我们可知经典的MVC模式的特性为:

    • View、Controller、Model中皆有ViewLogic的部分实现

    • Controller负责控制View与Model,需要了解View与Model的细节。

    • View需要了解Controller与Model的细节,需要在侦测用户行为之后调用Controller,并且在收到通知后调用Model以获取最新数据

    • Model并不需要了解Controller与View的细节,相对独立的模块

    Observer Pattern:自带观察者模式的MVC

    上文中也已提及,MVC滥觞于Observer模式,经典的MVC模式也可以与Observer模式相结合,其典型的用户流程为:

    • 用户交互输入了某些内容

    • Controller将用户输入转化为Model所需要进行的更改

    • View作为Observer会监听Model中的任意更新,一旦有更新事件发出,View会自动触发更新以展示最新的Model状态

    可知其与经典的MVC模式区别在于不需要Controller通知View进行更新,而是由Model主动调用View进行更新。这种改变提升了整体效率,简化了Controller的功能,不过也导致了View与Model之间的紧耦合。

    MVP:Decoupling View and Model 将视图与模型解耦, View<->Presenter

    维基百科将MVP称为MVC的一个推导扩展,观其渊源而知其所以然。对于MVP概念的定义,Microsoft较为明晰,而Martin Fowler的定义最为广泛接受。MVP模式在WinForm系列以Visual-XXX命名的编程语言与Java Swing等系列应用中最早流传开来,不过后来ASP.NET以及JFaces也广泛地使用了该模式。在MVP中用户不再与Presenter进行直接交互,而是由View完全接管了用户交互,譬如窗口上的每个控件都知道如何响应用户输入并且合适地渲染来自于Model的数据。而所有的事件会被传输给Presenter,Presenter在这里就是View与Model之间的中间人,负责控制Model进行修改以及将最新的Model状态传递给View。这里描述的就是典型的所谓Passive View版本的MVP,其典型的用户场景为:

    • 用户交互输入了某些内容

    • View将用户输入转化为发送给Presenter

    • Presenter控制Model接收需要改变的点

    • Model将更新之后的值返回给Presenter

    • Presenter将更新之后的模型返回给View

    根据上述流程,我们可知Passive View版本的MVP模式的特性为:

    • View、Presenter、Model中皆有ViewLogic的部分实现

    • Presenter负责连接View与Model,需要了解View与Model的细节。

    • View需要了解Presenter的细节,将用户输入转化为事件传递给Presenter

    • Model需要了解Presenter的细节,在完成更新之后将最新的模型传递给Presenter

    • View与Model之间相互解耦合

    Supervising Controller MVP

    简化Presenter的部分功能,使得Presenter只起到需要复杂控制或者调解的操作,而简单的Model展示转化直接由View与Model进行交互:

    MVVM:Data Binding & Stateless View 数据绑定与无状态的View,View<->ViewModels

    Model View View-Model模型是MV*家族中最年轻的一位,也是由Microsoft提出,并经由Martin Fowler布道传播。MVVM源于Martin Fowler的Presentation Model,Presentation Model的核心在于接管了View所有的行为响应,View的所有响应与状态都定义在了Presentation Model中。也就是说,View不会包含任意的状态。举个典型的使用场景,当用户点击某个按钮之后,状态信息是从Presentation Model传递给Model,而不是从View传递给Presentation Model。任何控制组件间的逻辑操作,即上文所述的ViewLogic,都应该放置在Presentation Model中进行处理,而不是在View层,这一点也是MVP模式与Presentation Model最大的区别。

    MVVM模式进一步深化了Presentation Model的思想,利用Data Binding等技术保证了View中不会存储任何的状态或者逻辑操作。在WPF中,UI主要是利用XAML或者XML创建,而这些标记类型的语言是无法存储任何状态的,就像HTML一样(因此JSX语法其实是将View又有状态化了),只是允许UI与某个ViewModel中的类建立映射关系。渲染引擎根据XAML中的声明以及来自于ViewModel的数据最终生成呈现的页面。因为数据绑定的特性,有时候MVVM也会被称作MVB:Model View Binder。总结一下,MVVM利用数据绑定彻底完成了从命令式编程到声明式编程的转化,使得View逐步无状态化。一个典型的MVVM的使用场景为:

    • 用户交互输入

    • View将数据直接传送给ViewModel,ViewModel保存这些状态数据

    • 在有需要的情况下,ViewModel会将数据传送给Model

    • Model在更新完成之后通知ViewModel

    • ViewModel从Model中获取最新的模型,并且更新自己的数据状态

    • View根据最新的ViewModel的数据进行重新渲染


    根据上述流程,我们可知MVVM模式的特性为:

    • ViewModel、Model中存在ViewLogic实现,View则不保存任何状态信息

    • View不需要了解ViewModel的实现细节,但是会声明自己所需要的数据类型,并且能够知道如何重新渲染

    • ViewModel不需要了解View的实现细节(非命令式编程),但是需要根据View声明的数据类型传入对应的数据。ViewModel需要了解Model的实现细节。

    • Model不需要了解View的实现细节,需要了解ViewModel的实现细节

    MV* in iOS

    MVC

    Cocoa MVC中往往会将大量的逻辑代码放入ViewController中,这就导致了所谓的Massive ViewController,而且很多的逻辑操作都嵌入到了View的生命周期中,很难剥离开来。或许你可以将一些业务逻辑或者数据转换之类的事情放到Model中完成,不过对于View而言绝大部分时间仅起到发送Action给Controller的作用。ViewController逐渐变成了几乎所有其他组件的Delegate与DataSource,还经常会负责派发或者取消网络请求等等职责。你的代码大概是这样的:

    var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
    
    userCell.configureWithUser(user)
    

    上面这种写法直接将View于Model关联起来,其实算是打破了Cocoa MVC的规范的,不过这样也是能够减少些Controller中的中转代码呢。这样一个架构模式在进行单元测试的时候就显得麻烦了,因为你的ViewController与View紧密关联,使得其很难去进行测试,因为你必须为每一个View创建Mock对象并且管理其生命周期。另外因为整个代码都混杂在一起,即破坏了职责分离原则,导致了系统的可变性与可维护性也很差。经典的MVC的示例程序如下:

    import UIKitstruct Person { // Model
        let firstName: String
    
        let lastName: String
    }
    
        class GreetingViewController : UIViewController { // View + Controller
        var person: Person!
    
        let showGreetingButton = UIButton()
        let greetingLabel = UILabel()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
        }
    
        func didTapButton(button: UIButton) {
            let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
            self.greetingLabel.text = greeting
    
        }
        // layout code goes here
    }   // Assembling of MVC   let model = Person(firstName: "David", lastName: "Blaine")
    
    let view = GreetingViewController()view.person = model;
    

    上面这种代码一看就很难测试,我们可以将生成greeting的代码移到GreetingModel这个单独的类中,从而进行单独的测试。不过我们还是很难去在GreetingViewController中测试显示逻辑而不调用UIView相关的譬如viewDidLoaddidTapButton等等较为费时的操作。再按照我们上文提及的优秀的架构的几个方面来看:

    • Distribution:View与Model是分割开来了,不过View与Controller是紧耦合的

    • Testability:因为较差的职责分割导致貌似只有Model部分方便测试

    • 易用性:因为程序比较直观,可能容易理解。

    MVP

    Cocoa中MVP模式是将ViewController当做纯粹的View进行处理,而将很多的ViewLogic与模型操作移动到Presenter中进行,代码如下:

    import UIKit
    
    struct Person { // Model
    
        let firstName: String
    
        let lastName: String
    
    }protocol GreetingView: class {
    
        func setGreeting(greeting: String)
    
    }
    
    protocol GreetingViewPresenter {
    
        init(view: GreetingView, person: Person)
    
        func showGreeting()
    
    }class GreetingPresenter : GreetingViewPresenter {
    
        unowned let view: GreetingView
    
        let person: Person
    
        required init(view: GreetingView, person: Person) {
    
            self.view = view
    
            self.person = person
    
        }
    
        func showGreeting() {
    
            let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
    
            self.view.setGreeting(greeting)
    
        }
    
    }class GreetingViewController : UIViewController, GreetingView {
    
        var presenter: GreetingViewPresenter!
    
        let showGreetingButton = UIButton()
    
        let greetingLabel = UILabel()
    
        override func viewDidLoad() {
    
            super.viewDidLoad()
    
            self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    
        }
    
        func didTapButton(button: UIButton) {
    
            self.presenter.showGreeting()
    
        }
    
        func setGreeting(greeting: String) {
    
            self.greetingLabel.text = greeting
    
        }
    
        // layout code goes here
    
    }
    
    // Assembling of MVP
    
    let model = Person(firstName: "David", lastName: "Blaine")
    
    let view = GreetingViewController()
    
    let presenter = GreetingPresenter(view: view, person: model)
    
    view.presenter = presenter
    
    • Distribution:主要的业务逻辑分割在了Presenter与Model中,View相对呆板一点

    • Testability:较为方便地测试

    • 易用性:代码职责分割的更为明显,不过不像MVC那样直观易懂了

    MVVM

    import UIKit
    
    struct Person { // Model
    
        let firstName: String
    
        let lastName: String
    
    }
    
    protocol GreetingViewModelProtocol: class {
    
        var greeting: String? { get }
    
        var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
    
        init(person: Person)
    
        func showGreeting()
    
    }
    
    class GreetingViewModel : GreetingViewModelProtocol {
    
        let person: Person
    
        var greeting: String? {
    
            didSet {
    
                self.greetingDidChange?(self)
    
            }
    
        }
    
        var greetingDidChange: ((GreetingViewModelProtocol) -> ())?
    
        required init(person: Person) {
    
            self.person = person
    
        }
    
        func showGreeting() {
    
            self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
    
        }
    
    }
    
    class GreetingViewController : UIViewController {
    
        var viewModel: GreetingViewModelProtocol! {
    
            didSet {
    
                self.viewModel.greetingDidChange = { [unowned self] viewModel in
    
                    self.greetingLabel.text = viewModel.greeting
    
                }
    
            }
    
        }
    
        let showGreetingButton = UIButton()
    
        let greetingLabel = UILabel()
    
        override func viewDidLoad() {
    
            super.viewDidLoad()
    
            self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)
    
        }
    
        // layout code goes here
    
    }
    
    // Assembling of MVVM
    
    let model = Person(firstName: "David", lastName: "Blaine")
    
    let viewModel = GreetingViewModel(person: model)
    
    let view = GreetingViewController()
    
    view.viewModel = viewModel
    
    • Distribution:在Cocoa MVVM中,View相对于MVP中的View担负了更多的功能,譬如需要构建数据绑定等等

    • Testability:ViewModel拥有View中的所有数据结构,因此很容易就可以进行测试

    • 易用性:相对而言有很多的冗余代码

    感谢作者:wxyyxc1992

    公告通知

    自动化运维班、架构师班、区块链正在招生中

    各位小伙伴们,欢迎试听和咨询:


    扫码添加小助手微信,备注"公开课,来源简书",进入分享群

    相关文章

      网友评论

        本文标题:GUI应用程序架构的十年变迁(二)

        本文链接:https://www.haomeiwen.com/subject/mbdklftx.html