F#奇妙游(35):MVC模式和ADT分析
前言
经常在知乎上看帖子,给人的感觉就是桌面开发已经挂了。当然知乎上每天药丸的东西实在是太多了,什么C#药丸啊,感觉上是地球分分钟就要报销。但是在工业界,桌面应用还是有一些市场的,在上位机程序中、在功能非常复杂的桌面应用还是没办法被网页所替代。
就比如说我们用来开发程序的JetBrain系列工具,虽然免费的Eclipse荣光不在,但是收费的IDE居然还能活得好好的。这说明在行业软件中,人们依然依赖界面开发良好的桌面应用来完成信息化的操作。
前几天刚看了一个WPF可能药丸的帖子,说是WPF已经被微软抛弃了。我居然还回头来整MVC,可能也是太没前途了。人家WPF好歹是MVVM。
MVC的内涵
MVVM和MVC其实都是OOP时代对UI程序的开发框架。前者(MVVM)通过分离模型和视图,并把状态和行为结合到一个对象中,称为ViewModel,ViewModel则通过数据绑定来实现视图的更新。而这里面最为核心的行为,则需要通过事件的方式为ViewModel所处理,这种事件处理方式没有一致的机制,可以通过数据绑定,也可以通过注册事件处理函数。此外,ViewModel还必须维护一个运行状态的存储机制,这就带来更高的复杂性。
MVC分离模型和视图的意图和MVVM一样,此外,MVC还定义了视图和控制器的分离。
在MVC中,控制器负责处理用户的输入(通常被抽象为事件),然后更新模型,模型更新后,会通过Controller通知视图进行更新。这里的模型和视图都是被动的(模型甚至可能是值),它们不会主动去更新自己,而是被控制器所更新。
这里核心的就是控制器对于事件的处理以及事件的抽象。这里最有意思的是对
于Controller而言,View不过就是一个事件流。
在.NET平台中,采用Observable和Observer模式来实现事件的抽象。这里的事件流就是一个Observable对象(也就是Subject),它的接口是public interface IObservable<out T>
。这个接口的泛型类型T
就是事件的类型,这个对象提供了事件的相关信息。官方文档中的描述是:
定义基于推送的通知的提供程序。
这个接口规定的方法是Subscribe
,也就是把一个观察者注册到这个事件流中。
abstract member Subscribe: observer: IObserver<'T> -> IDisposable
这里的观察者就是Controller,它通过订阅事件流,来接受事件的通知。这里的事件就是View的事件。这样,我们就把MVC中的事件抽象为了一个事件流,这个事件流就是一个Observable对象。
在F#中提供了对于事件驱动的程序设计的支持,对应的命名空间为FSharp.Core.Control
。下面的例子就是一个简单的事件流的例子。类型Event实现了IObservable接口,它的泛型类型就是事件的类型。其中Publish属性就是一个IObservable对象,Trigger方法用来触发事件。
open System.Collections.Generic
type MyClassWithCLIEvent() =
let event1 = new Event<string>()
[<CLIEvent>]
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
这里调用TestEvent
方法,就会触发事件,然后观察者就会收到通知。注册观察者的方法就是Subscribe
,它的参数就是一个观察者,这里的观察者就是一个函数,它的参数就是事件的类型。这里的事件类型就是string
。同时,FSharp还给IObservable接口提供了一个扩展方法Add
,其语法和语义与Subscribe
是一样的。
在事件驱动的基础上,我们可以构造如下的MVC模式的程序。
F#中的MVC
F#作为函数式程序设计语言,对于ADT的支持非常好,这里我们可以使用ADT来定义事件的抽象。
我们还是用那个增减计数器的例子来说明F#对MVC的支持。
type UpDownEvent =
| Up
| Down
type View = IObservable<UpDownEvent>
type Model = {State: int}
type Controller = Model -> UpDownEvent -> Model
type Mvc = Controller -> Model -> View -> Model
从上面的代码中,可以看到:
- 增减计数器的事件用可区分联合类型来表示;
- View抽象为一个事件流;
- Model抽象为一个状态,用一个不可变的记录来表示;
- Controller抽象为一个函数,它接受一个Model和一个事件,然后返回一个新的Model;
- Mvc抽象为一个函数,它接受一个Controller、一个Model和一个View,然后返回一个Model。
通过上面的类型系统,可以很好地抽象出我们要表达的MVC模式。这个简单的例子与MVVM的面向对象实现截然不同。仅仅阅读这些类型(或者进行一些ADT的分析和对应组合数的计算),就能对整个系统的结构有很好的了解。
如果我们为了适应UI开发中的绑定机制,则还可能把上面的描述做一定的修改,例如:
type UpDownEvent =
| Up
| Down
type View = IObservable<UpDownEvent>
type Model = {mutable State: int}
type Controller = Model -> UpDownEvent -> unit
type Mvc = Controller -> Model -> View -> IDisposable
这里的Model就变成了一个可变的记录,Controller也变成了一个不返回Model的函数,而是直接修改Model的状态。
完整的例子
这里就能用FSharp.Core.Event
来实现一个上面这个例子。
open System
type UpDownEvent =
| Up
| Down
type View = IObservable<UpDownEvent>
type Model = { mutable State: int }
type Controller = Model -> UpDownEvent -> unit
type Mvc = Controller -> Model -> View -> IDisposable
// 事件流
let subject = Event<UpDownEvent>()
// 事件触发
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()
这个程序就实现了前面说的MVC模式,运行
dotnet fsi mvc.fsx
得到状态的变迁过程:
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 }
这个例子中,最大的特点就是Controller的实现非常干净,并且编译器还能够提示是否所有的事件情况都被处理了。
总结
- F#中的事件驱动编程,可以通过
FSharp.Core.Event
来实现; - 通过ADT,可以很好地抽象出MVC模式;
- 通过MVC模式,可以很好地描述事件驱动的程序。