• F#奇妙游(35):MVC模式和ADT分析


    前言

    经常在知乎上看帖子,给人的感觉就是桌面开发已经挂了。当然知乎上每天药丸的东西实在是太多了,什么C#药丸啊,感觉上是地球分分钟就要报销。但是在工业界,桌面应用还是有一些市场的,在上位机程序中、在功能非常复杂的桌面应用还是没办法被网页所替代。

    就比如说我们用来开发程序的JetBrain系列工具,虽然免费的Eclipse荣光不在,但是收费的IDE居然还能活得好好的。这说明在行业软件中,人们依然依赖界面开发良好的桌面应用来完成信息化的操作。

    前几天刚看了一个WPF可能药丸的帖子,说是WPF已经被微软抛弃了。我居然还回头来整MVC,可能也是太没前途了。人家WPF好歹是MVVM。

    MVC的内涵

    MVVM和MVC其实都是OOP时代对UI程序的开发框架。前者(MVVM)通过分离模型和视图,并把状态和行为结合到一个对象中,称为ViewModel,ViewModel则通过数据绑定来实现视图的更新。而这里面最为核心的行为,则需要通过事件的方式为ViewModel所处理,这种事件处理方式没有一致的机制,可以通过数据绑定,也可以通过注册事件处理函数。此外,ViewModel还必须维护一个运行状态的存储机制,这就带来更高的复杂性。

    Owns
    Owns
    Notify
    Update
    View
    ViewModel
    Model

    MVC分离模型和视图的意图和MVVM一样,此外,MVC还定义了视图和控制器的分离。

    Update
    Events
    Notify
    Update
    Controller
    Model
    View

    在MVC中,控制器负责处理用户的输入(通常被抽象为事件),然后更新模型,模型更新后,会通过Controller通知视图进行更新。这里的模型和视图都是被动的(模型甚至可能是值),它们不会主动去更新自己,而是被控制器所更新。

    这里核心的就是控制器对于事件的处理以及事件的抽象。这里最有意思的是对
    于Controller而言,View不过就是一个事件流。

    在.NET平台中,采用Observable和Observer模式来实现事件的抽象。这里的事件流就是一个Observable对象(也就是Subject),它的接口是public interface IObservable。这个接口的泛型类型T就是事件的类型,这个对象提供了事件的相关信息。官方文档中的描述是:

    定义基于推送的通知的提供程序。

    这个接口规定的方法是Subscribe,也就是把一个观察者注册到这个事件流中。

    abstract member Subscribe: observer: IObserver<'T> -> IDisposable
    
    • 1

    这里的观察者就是Controller,它通过订阅事件流,来接受事件的通知。这里的事件就是View的事件。这样,我们就把MVC中的事件抽象为了一个事件流,这个事件流就是一个Observable对象。

    在F#中提供了对于事件驱动的程序设计的支持,对应的命名空间为FSharp.Core.Control。下面的例子就是一个简单的事件流的例子。类型Event实现了IObservable接口,它的泛型类型就是事件的类型。其中Publish属性就是一个IObservable对象,Trigger方法用来触发事件。

    IObservable
    +Subscribe(IObservable)
    +Add(IObservable)
    Event
    +IObservable Publish
    +Trigger(T)
    open System.Collections.Generic
    
    type MyClassWithCLIEvent() =
    
        let event1 = new Event()
    
        []
        member this.Event1 = event1.Publish
    
        member this.TestEvent(arg) =
            event1.Trigger(arg)
    
    let classWithEvent = new MyClassWithCLIEvent()
    classWithEvent.Event1.Subscribe(fun arg ->
            printfn "Event1 occurred! Object data: %s" arg)
    
    classWithEvent.TestEvent("Hello World!")
    
    System.Console.ReadLine() |> ignore
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这里调用TestEvent方法,就会触发事件,然后观察者就会收到通知。注册观察者的方法就是Subscribe,它的参数就是一个观察者,这里的观察者就是一个函数,它的参数就是事件的类型。这里的事件类型就是string。同时,FSharp还给IObservable接口提供了一个扩展方法Add,其语法和语义与Subscribe是一样的。

    在事件驱动的基础上,我们可以构造如下的MVC模式的程序。

    F#中的MVC

    F#作为函数式程序设计语言,对于ADT的支持非常好,这里我们可以使用ADT来定义事件的抽象。

    我们还是用那个增减计数器的例子来说明F#对MVC的支持。

    type UpDownEvent =
        | Up
        | Down
    
    type View = IObservable
    
    type Model = {State: int}
    
    type Controller = Model -> UpDownEvent -> Model
    
    type Mvc = Controller -> Model -> View -> Model
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    从上面的代码中,可以看到:

    1. 增减计数器的事件用可区分联合类型来表示;
    2. View抽象为一个事件流;
    3. Model抽象为一个状态,用一个不可变的记录来表示;
    4. Controller抽象为一个函数,它接受一个Model和一个事件,然后返回一个新的Model;
    5. Mvc抽象为一个函数,它接受一个Controller、一个Model和一个View,然后返回一个Model。

    通过上面的类型系统,可以很好地抽象出我们要表达的MVC模式。这个简单的例子与MVVM的面向对象实现截然不同。仅仅阅读这些类型(或者进行一些ADT的分析和对应组合数的计算),就能对整个系统的结构有很好的了解。

    如果我们为了适应UI开发中的绑定机制,则还可能把上面的描述做一定的修改,例如:

    type UpDownEvent =
        | Up
        | Down
    type View = IObservable
    type Model = {mutable State: int}
    type Controller = Model -> UpDownEvent -> unit
    type Mvc = Controller -> Model -> View -> IDisposable
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里的Model就变成了一个可变的记录,Controller也变成了一个不返回Model的函数,而是直接修改Model的状态。

    完整的例子

    这里就能用FSharp.Core.Event来实现一个上面这个例子。

    open System
    
    
    type UpDownEvent =
        | Up
        | Down
    
    type View = IObservable
    type Model = { mutable State: int }
    type Controller = Model -> UpDownEvent -> unit
    type Mvc = Controller -> Model -> View -> IDisposable
    
    // 事件流
    let subject = Event()
    
    // 事件触发
    let raiseEvents xs =
        xs |> Seq.iter (fun x -> subject.Trigger(x))
    
    // IEvent<`a>实现了IObservable<`a>,所以可以直接用
    let view = subject.Publish
    
    // 实例化模型,初始化状态为0
    let model: Model = { State = 0 }
    
    // 控制器的实现十分干净,直接更改模型的状态
    let controller model event =
        match event with
        | Up -> model.State <- model.State + 1
        | Down -> model.State <- model.State - 1
    
    // MVC的实现
    let mvc: Mvc =
        fun controller model view ->
            view.Subscribe(fun event ->
                controller model event
                printfn "%A ==> Model state: %A" event model)
    
    // 订阅事件流,返回一个IDisposable对象
    let subscription = view |> mvc controller model
    
    // 模拟事件的触发
    printfn "Raising events...%A" model
    raiseEvents [ Up; Up; Down; Up; Down; Down ]
    
    subscription.Dispose()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    这个程序就实现了前面说的MVC模式,运行

    dotnet fsi mvc.fsx
    
    • 1

    得到状态的变迁过程:

    Raising events...{ State = 0 }
    Up ==> Model state: { State = 1 }
    Up ==> Model state: { State = 2 }
    Down ==> Model state: { State = 1 }
    Up ==> Model state: { State = 2 }
    Down ==> Model state: { State = 1 }
    Down ==> Model state: { State = 0 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个例子中,最大的特点就是Controller的实现非常干净,并且编译器还能够提示是否所有的事件情况都被处理了。

    总结

    1. F#中的事件驱动编程,可以通过FSharp.Core.Event来实现;
    2. 通过ADT,可以很好地抽象出MVC模式;
    3. 通过MVC模式,可以很好地描述事件驱动的程序。
  • 相关阅读:
    20%的业务代码的Spring声明式事务,可能都没处理正确
    秋招第二周面试经验
    CNN反向求导推导
    Java中JVM、JRE和JDK三者有什么区别和联系?
    【数据库】MySQL数据表记录改操作
    爬虫工具之Beautiful Soup4
    3d游戏建模全解
    Koa 中间件使用之 koa-jwt
    Riccati 方程求解
    怎么看电脑配置?电脑配置好不好?详细教程来了!
  • 原文地址:https://blog.csdn.net/withstand/article/details/133378414