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<out T>。这个接口的泛型类型T就是事件的类型,这个对象提供了事件的相关信息。官方文档中的描述是:

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

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

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

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

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

IObservable<T>
+Subscribe(IObservable<T>)
+Add(IObservable<T>)
Event<T>
+IObservable<T> Publish
+Trigger(T)
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

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

  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<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的实现非常干净,并且编译器还能够提示是否所有的事件情况都被处理了。

总结

  1. F#中的事件驱动编程,可以通过FSharp.Core.Event来实现;
  2. 通过ADT,可以很好地抽象出MVC模式;
  3. 通过MVC模式,可以很好地描述事件驱动的程序。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大福是小强

除非你钱多烧得慌……

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值