19.空降作战——观察者模式

来源:互联网 发布:eve数据地点扫描 编辑:程序博客网 时间:2024/04/27 13:43

本章我们将要了解观察者模式(Observer Pattern)。

首先,让我们先听一个……

 

关于观察者的故事

很久很久以前,大概在设计模式出现的时候,有一种模式称为观察者模式,当人们询问为什么他叫作这个名字的时候,他是这样解释的:

观察者模式定义了对象之间一对多的依赖关系,当这一个独立的对象状态改变时,它会通知所有的依赖者(观察者)接收新的数据,这些依赖者对象会根据最新的数据做出相应的更新。

 

这个定义听起来有些陌生,但又感觉他有些熟悉;有时候,模式真的会大隐于市,我们正在接触他们的时候却不自知,为了缓解人们的恐惧,观察者模式又给了我们一个他随时会出现的故事:

有些公司会推出一些邮件订阅服务,他们是信息的发布方(Publish);同时,很多人都会“订阅”一些邮件(可能是自愿,也可以是被自愿,谁知道呢)。

我是会经常收到这些“订阅”的邮件,相信你可能也会收到,我们的好朋友Tom、Jerry、Merry也可能会收到,这样,我们都成了订阅者(Subscribe)。

一个信息发布方,很多订阅者,这就是观察者模式现实版:

 

好吧,事情就是这么简单,这就是观察者模式,只不过在这个模式的称呼中,发布者称为主题(Subject),而订阅者则称为观察者(Observer)。可能是因为观察者比较多,所以才以他们命名这个模式吧,我是这样想的。J

当这家邮件订阅服务公司不知从哪弄到一些新闻或文章后,就把它发送到我们的邮箱中;然后,我们有很多处理这些邮件的方法;我们可以阅读这些邮件,也可以立即删掉,甚至我们可以将这家公司的邮箱设置为黑名单,根本不用自己处理这些邮件;当然,我们也可以像邻家小妹Merry一样,到这家公司的网站退订,这样她再也不用接收这些对她来说没有任何吸引力的邮件了。

……

“好了,别讲故事了,活儿来了。”

“什么事?”

“老板要让士兵登上运输船和运输机,登陆作战开始了。”

“酷!让我先看一些关于诺曼底登陆的电影再说,这样可以给我带来一些灵感。”

为了工作而娱乐,这主意绝妙,合情、合理,合不合法就看你自己怎么玩了。哈哈!

 

应用观察者模式

我们再来看看观察者中的两个主要元素:主题与观察者们。

主题(Subject),作为数据的拥有者和发布者,他必须在数据改变时通知观察者们;当然,他首先需要知道具体要通知哪些人。

观察者(Observer),既然他们需要“主题”中的数据,首先他们必须加入这个主题俱乐部;然后,在主题告诉他们数据有变化时,他们必须作出相应的处理。

 

现在,我们需要在战争游戏中应用观察模式,我们要将多个士兵进入运输船或运输机,然后,向敌人的海岸和后方发起突然进攻。

谁是主题?运输船和运输机。

谁是观察者?士兵、医生和工程兵,当然,你要把指挥官(上一章那个唯一的重要人物)偷运到一个安全的地方也是可以的。

本章,我们就简单点,我们只上演《兄弟连》的场景——运输机和士兵。

 

首先,运输机是一个主题(Subject),他可以加入士兵,或者将士兵踢出飞机;但他要做什么时必须将通知这些士兵,它应该做的工作包括在接口ISubject中,定义如下:

Public Interface ISubject

    Sub AddUnit(ByVal unit As IUnit)

    Sub RemoveUnit(ByVal unit As IUnit)

    Sub Notify()

End Interface

(项目:ObserverPatternDemo    文件:ObserverPattern.vb)

 

在运输机单位中,必须实现ISubject接口;我们使用AddUnit()方法欢迎士兵登机,使用RemoveUnit()方法将士兵踢下飞机(士兵当然会背有降落伞);如果飞机有什么情况,会告诉所有安排在这架飞机的士兵,比如:到达目标,跳伞;或者,已被敌人防空炮火击中而爆炸(这真是太不幸了);为了方便使用,我们可以将这状态定义为一个枚举类型,代码如下:

Public Enum ESubjectState

    Jump = 1 '到达目标,跳伞

    Blast = 2 '飞机爆炸,全体阵亡

End Enum

(项目:ObserverPatternDemo    文件:ObserverPattern.vb)

 

接下来就是观察者接口(IObserver),在简单的观察者中,我们只需要接收数据的更新,并做出相应的处理就可以了;在本例中,我们定义一个Update()方法用于士兵接收运输机传来的信息;这样,这个观察者接口的定义就是:

Public Interface IObserver

    Sub Update(ByVal state As ESubjectState)

End Interface

(项目:ObserverPatternDemo    文件:ObserverPattern.vb)

 

现在的工作是将士兵(CSoldier类)变成伞兵,实现IObserver接口,这样他们就可以与运输机(CTransportPlane类)交流信息了;修改后的CSoldier类的代码如下:

''士兵

Public Class CSoldier

    Inherits CUnit

    Implements IObserver

    Public Sub New()

        myBehavior = New CLandBehavior

        myWeapon = New CMachineGunWeapon

        mySpeed = 15

        UnitId = EUnit.Soldier

    End Sub

    Public Sub Update(ByVal state As ESubjectState) Implements IObserver.Update

        Select Case state

            Case ESubjectState.Jump

                Console.WriteLine(Me.Name & "准备完毕!")

            Case ESubjectState.Blast

                Console.WriteLine(Me.Name & "的飞机被击中,他完了!")

        End Select

    End Sub

End Class

(项目:ObserverPatternDemo    文件:Units.vb)

 

最后,我们改写CTransportPlane类,他需要实现ISubject接口,修改后的代码如下:

''运输机

Public Class CTransportPlane

    Inherits CUnit

    Implements ISubject

    Private arrUnit As New ArrayList '伞兵信息

    Private myState As ESubjectState '运输机的状态

    Public Sub New()

        myBehavior = New CAirBehavior

        myWeapon = New CNoWeapon

        mySpeed = 70

        UnitId = EUnit.TransportPlane

    End Sub

    Public Property State As ESubjectState

        Get

            Return myState

        End Get

        Set(ByVal value As ESubjectState)

            myState = value

            Notify() '状态改变,通知观察者

        End Set

    End Property

    Public Sub AddUnit(ByVal unit As IUnit) Implements ISubject.AddUnit

        If arrUnit.Contains(unit) = False Then

            arrUnit.Add(unit)

        End If

        Console.WriteLine("欢迎" & unit.Name & "加入!")

    End Sub

    Private Sub Notify() Implements ISubject.Notify

        Dim unit As IUnit

        For Each unit In arrUnit

            CType(unit, CSoldier).Update(myState)

        Next

    End Sub

    Public Sub RemoveUnit(ByVal unit As IUnit) Implements ISubject.RemoveUnit

        If arrUnit.Contains(unit) = True Then

            arrUnit.Remove(unit)

        End If

        Console.WriteLine(unit.Name & "已离开。")

    End Sub

End Class

(项目:ObserverPatternDemo    文件:Units.vb)

 

请注意,我们在CTransportPlane类中除了实现ISubject接口的三个方法,还添加了State属性,这当然是指运输机的状态。另外,你可能也注意到了,类中的arrUnit成员用于保存运输机中的乘客信息,他是一个ArrayList类型对象,并且已经使用New关键字实例化(打开机舱,为士兵创建头等舱座位),否则在运行时会产生空引用异常。

现在,我们准备一架C-47运输机,他只运送了3名士兵,只不过还是有些不幸的事情发生了,我们为勇士们祈祷吧!测试代码如下:

Module Module1

    Sub Main()

        Dim C47 As New CTransportPlane

        C47.Name = "C-47运输机"

        Console.WriteLine(C47.Name)

        Dim soldier(2) As CSoldier

        Dim i As Integer

        For i = 0 To 2

            soldier(i) = New CSoldier

            soldier(i).Name = "士兵" & i.ToString & "号"

        Next

        '开始登机

        For i = 0 To 2

            C47.AddUnit(soldier(i))

        Next

        '有名士兵提前离开

        C47.RemoveUnit(soldier(2))

        '飞向敌后

        C47.Move(300, 300)

        '还有一分钟准备

        Console.WriteLine("旁白:一分钟准备")

        C47.State = ESubjectState.Jump

        '不幸的事情发生了

        Console.WriteLine("旁白:敌军高射炮击中了飞机油箱")

        C47.State = ESubjectState.Blast

        '

        Console.WriteLine("旁白:士兵2号实在是太幸运了!!!")

        Console.ReadLine()

    End Sub

End Module

(项目:ObserverPatternDemo    文件:Module1.vb)

 

代码运行结果如下图:

 

我承认,在本例中我们违反了老板的规定,并没有使用CreateUnit()方法创建游戏单位;好吧,这只是我有点偷懒,想少敲几个字母的代码;虽然我可以找到一堆的理由来解释这个问题,但这些都不重要,重要的是,我们了解了观察者模式,这就够了!

 

.NET 架构中的观察者

在.NET Frameword 4.0的架构中,为我们提供了观察者模式的内置创建接口,IObservable和IObserver接口,请注意,它们都是泛型接口。

其中,IObserver接口用于创建观察者角色,它有三个方法需要实现,分别是:

l         OnCompleted()方法:通知观察者,提供程序已完成发送基于推送的通知。 

l         OnError()方法:通知观察者,提供程序遇到错误情况。 

l         OnNext()方法:向观察者提供新数据。

 

IObservable接口用于创建主题(Subject)角色,也就是数据的拥有者和发布者;在这个接口中有一个方法需要实现。Subscribe()方法通知提供程序,某观察者将要接收通知。这个方法将返回IDisposable接口类型的对象。

 

下面,我们将请出这两位接口真情演绎一个主题与订阅者的故事。首先是订阅者类:

Public Class CObserver

    Implements IObserver(Of CObserver)

    '

    Public Name As String '订阅者名称

    Public Message As String '收到的信息

    Public Unsubscribe As IDisposable '退订

    '数据处理完成

Public Sub OnCompleted()  _

Implements System.IObserver(Of CObserver).OnCompleted

        Console.WriteLine(Me.Name & "信息处理完成!")

    End Sub

    '错误处理

Public Sub OnError(ByVal [error] As System.Exception)  _

Implements System.IObserver(Of CObserver).OnError

        Console.WriteLine([error].Message)

    End Sub

    '处理数据

Public Sub OnNext(ByVal value As CObserver)  _

Implements System.IObserver(Of CObserver).OnNext

        Console.WriteLine(Me.Name & "接收信息:" & Message)

    End Sub

End Class

(项目:ObserverPatternDemo1    文件:ObserverPattern1.vb)

 

接下来是主题,代码如下:

''主题,实现IObservable接口

Public Class CSubject

    Implements IObservable(Of CObserver)

    Protected arrSubscriber As New ArrayList

    Dim r As New Random()

    '添加订阅者,返回退订操作对象

Public Function Subscribe(ByVal observer As  _

System.IObserver(Of CObserver)) As System.IDisposable  _

Implements System.IObservable(Of CObserver).Subscribe

        '如果订阅者不存在则添加到列表

        If arrSubscriber.Contains(observer) = False Then

            arrSubscriber.Add(observer)

        End If

        Dim unsubscribe As IDisposable

        unsubscribe = New RemoveSubscriber(arrSubscriber, observer)

        CType(observer, CObserver).Unsubscribe = unsubscribe

        Return unsubscribe

    End Function

    '更新数据并向订阅者发送,一个4位随机数字

    Public Sub Update()

        Dim obj As CObserver

        For Each obj In arrSubscriber

            Try

                obj.Message = r.Next(1000, 10000).ToString()

                obj.OnNext(obj)

            Catch ex As Exception

                obj.OnError(ex)

            Finally

                obj.OnCompleted()

            End Try

        Next

    End Sub

    '显示订阅者名单

    Public Sub ShowList()

        Dim obj As CObserver

        For Each obj In arrSubscriber

            Console.WriteLine(obj.Name)

        Next

    End Sub

    '清理订阅者

    Private Class RemoveSubscriber

        Implements IDisposable

        Private myObserver As IObserver(Of CObserver)

        Private myObservers As ArrayList

        Public Sub New(ByVal observers As ArrayList,  _

ByVal observer As IObserver(Of CObserver))

            myObservers = observers

            myObserver = observer

        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose

            If IsNothing(myObservers) = False And  _

 myObservers.Contains(myObserver) Then

                Console.WriteLine(CType(myObserver, CObserver).Name & "已退出")

                myObservers.Remove(myObserver)

            End If

        End Sub

    End Class

End Class

(项目:ObserverPatternDemo1    文件:ObserverPattern1.vb)

 

下面是测试这两个类的代码:

Module Module1

    Sub Main()

        Dim obs(2) As CObserver

        Dim i As Integer

        '加入3名订阅者

        Console.WriteLine(">>>>加入三位订阅者")

        For i = 0 To 2

            obs(i) = New CObserver

        Next

        obs(0).Name = "Tom"

        obs(1).Name = "Jerry"

        obs(2).Name = "Merry"        '

        Dim subject As New CSubject

        For i = 0 To 2

            subject.Subscribe(obs(i))

        Next

        '第一次给数据

        subject.Update()

        'Tom退订

        Console.WriteLine(">>>>")

        obs(0).Unsubscribe.Dispose()

        '第二次给数据

        Console.WriteLine(">>>>只剩两位订阅者")

        subject.Update()

        Console.ReadLine()

    End Sub

End Module

(项目:ObserverPatternDemo1    文件:Module1.vb)

 

运行结果如下图:

 

使用.NET Framework 4.0中的IObservable和IObserver接口实现观察者模式确实有点麻烦,同时也有点过于模式化;所以,在自己的项目中,我们可以大胆地使用自己的观察者模式架构;况且,在.NET Framework 4.0以前的版本中还没有提供这两个接口。

 

主题和观察者,谁说了算

先来回头看一看前面的两个关于观察者模式的例子。

第一个例子,在我们的游戏代码中,只能通过运输机(CTransportPlane)中的AddUnit()方法和RemoveUnit()方法分别进行添加和删除观察者(订阅者)的操作;这时订阅者没有自主加入或退出的方法;而且,观察者在接收数据时,是被动的接收主题“推”给自己的数据,而不能选择“拉”回自己需要的数据。

在第二个例子中,观察者(CObserver)有了一点小自由,他可以选择退出订阅(Unsubscribe);但在获取数据时依然是靠主题(CSubject)推给自己。

 

我们知道,在现实世界中,主题和观察者双方都有选择和被选择的权利,同时,观察者也应该可以只看自己感兴趣的内容;在本节的示例中,我们将创建一个全能的观察者模式代码结构。首先是主题和观察者接口的定义:

''主题接口

Public Interface ISubject

    Sub AddObserver(ByVal observer As IObserver) '添加观察者

    Sub RemoveObserver(ByVal observer As IObserver) '删除观察者

    Sub Notify() '更新数据

End Interface

''观察者接口

Public Interface IObserver

    Sub JoinSubject(ByVal subject As ISubject) '加入主题

    Sub QuitSubject(ByVal subject As ISubject) '退出主题

    Sub Update() '处理更新数据

End Interface

(项目:ObserverPatternDemo2    文件:ObserverPatter2.vb)

 

下面是主题类(CSubject):

''主题类

Public Class CSubject

    Implements ISubject

    Protected arrObserver As New ArrayList '观察者列表

    Protected Data As Object '数据

        '添加观察者

Public Sub AddObserver(ByVal observer As IObserver)  _

Implements ISubject.AddObserver

        If arrObserver.Contains(observer) = False Then

            arrObserver.Add(observer)

        End If

        Console.WriteLine(CType(observer, CObserver).Name & "加入主题。")

    End Sub

    '删除观察者

Public Sub RemoveObserver(ByVal observer As IObserver)  _

Implements ISubject.RemoveObserver

        If arrObserver.Contains(observer) Then

            arrObserver.Remove(observer)

        End If

        Console.WriteLine(CType(observer, CObserver).Name & "退出主题。")

End Sub

'通知数据

    Public Sub Notify() Implements ISubject.Notify

        Dim obj As IObserver

        For Each obj In arrObserver

            obj.Update(Me.Data)

        Next

    End Sub

End Class

(项目:ObserverPatternDemo2    文件:ObserverPatter2.vb)

 

观察者类(CObserver):

''观察者类

Public Class CObserver

    Implements IObserver

    Public Name As String

    '

    Public Sub New(ByVal strName As String)

        Me.Name = strName

    End Sub

    '加入主题

Public Sub JoinSubject(ByVal subject As ISubject)  _

Implements IObserver.JoinSubject

        subject.AddObserver(Me)

    End Sub

    '退出主题

Public Sub QuitSubject(ByVal subject As ISubject)  _

Implements IObserver.QuitSubject

        subject.RemoveObserver(Me)

    End Sub

    '处理新数据

    Public Sub Update(ByVal data As Object) Implements IObserver.Update

        Console.WriteLine(Me.Name & "正在处理数据...")

    End Sub

End Class

(项目:ObserverPatternDemo2    文件:ObserverPatter2.vb)

 

请注意,在本例中,我们并没有给出观察者对象“拉”数据的方法;这是因为在不同的主题中会提供不同的内容和数据;如果主题中的数据允许其观察者按需读取的话,我们可以在主题类中添加一系列的GetData()方法供观察者调用,很容易实现的。

 

下面是我们的测试代码:

Module Module1

    Sub Main()

        Dim subject As New CSubject

        Dim Tom As New CObserver("Tom")

        Dim Jerry As New CObserver("Jerry")

        Dim Merry As New CObserver("Merry")

        '三人加入主题

        subject.AddObserver(Tom)

        Jerry.JoinSubject(subject)

        Merry.JoinSubject(subject)

        '第一次处理数据

        subject.Notify()

        'Tom和Merry退出主题

        subject.RemoveObserver(Tom)

        Merry.QuitSubject(subject)

        '第二次处理数据

        subject.Notify()

        '

        Console.ReadLine()

    End Sub

End Module

(项目:ObserverPatternDemo2    文件:Module1.vb)

 

本例运行的结果如下图:

 

本例中,Tom是被主题强行加入(AddObserver)的,而Jerry和Merry是自愿加入的(JoinSubject);而在退出主题时,Tom同样是被主题移除(RemoveObserver)的,而Merry同样是自己退出的(QuitSubject)。

如果在观察者类中,Name属性是必须的,还可以将它添加到IObserver接口中;可以自己动手试试。

根据这个观察者模式的基本结构,你可以项目中根据需要修改和使用它们;比如,添加观察者“拉”数据的代码。再次提示:你可以在CSubject类中添加不同数据的GetData()方法提供给观察者调用。

 

小结

在这一章,我们认识了观察者模式(Observer Pattern),他定义了对象间一对多的依赖关系;现在,就让我们回忆一下这个模式中的主角:

主题(Subject),又称为发布者(Publish)或可观察者(Observable)。他是数据的拥有者和发布者,它一般通过“通知(Notify)”的方法将更新的数据“推(Push)”到已加入本主题的观察者们。

观察者(Observer),又称为订阅者(Subscribe)。众多的观察者们可以在主题中注册或注销,可以被动的接收主题推来的数据,也可以主动的向主题“拉(Pull)”数据;这主要根据需要而定;不过,使用由主题统一推数据的方式时,代码可能更容易维护。

 

出自:http://www.caohuayu.com/books/B0003/B0003.aspx

原创粉丝点击