游戏开发设计模式2 观察者模式
第二章 观察者模式
在本章将会说到利用其它开发人员的经验和智慧,遭遇过的问题,怎么解决问题的方式进行实例演示。我们会看到设计模式的用途和优点和一些关键的OO设计原则。
气象观测站需求
甲方公司有接口可以检测到目前的天气状况,希望我们能够开发一个程序,有三个面板,分别显示 温度,湿度,气压 状况,当接口发生变化获取到最新的天气状况数据时,三种面板必须实时更新。
而且,这是一个可以扩展的气象站
气象监测应用的概况
此系统中的三个部分是气象站(获取实际气象数据的物理装置),WeatherData对象(追踪来自气象站的数据,并更新布告板)和布告板(显示目前天气状况给用户看)。
weatherData对象知道如何跟物理气象站联系,以取得更新的数据。WeatherData对象会随即更新三个面板的显示:目前状况(温度,湿度,气压)气象统计和天气预报。
如果我们选择接受这个项目,我们的工作就是建立一个应用,利用Weather对象取得数据,并更新三个面板:目前状况,气象统计和天气预报。
接下来让我们看一下甲方给我们的检测天气接口数据代码:
我们的工作是实现measurementsChaged(),好让它更新目前你状况,气象统计,天气预报的显示面板(三个面板显示)。
我们目前知道些什么?
气象站的要求说明并不是很清楚,我们必须搞懂该做些什么,那么目前知道些什么那?
1.WeatherData类具有get方法,可以取得三个测量值:温度,湿度,与气压
2.当新的测量数据备妥时,measurementsChanged()方法就会被调用(我们不在乎此方法时如何被调用的,我们只在乎它被调用了)。
3.我们需要实现三个使用天气数据的面板:目前状况面板,气象统计面板,天气预报面板。一旦WeatherData有新的测量,这些面板必须马上更新。
4.此系统必须可扩展,让其他开发人员建立定制的面板,用户可以随心所欲的添加或删除任何面板。目前初始的面板有三类:目前状况,气象统计,天气预报。
先看一个错误示范
这是第一个可能的实现:我们依照气象站开发人员的写的气象调用接口(measurementChanged())方法添加我们的代码:
1 | /// <summary> |
在我们上面的实现中,下列哪种说法正确?
A 我们时针对具体实现编程,而非针对接口编程
B 对于每个新的面板,我们都得修改代码。
C 我们无法在运行时动态增加或删除面板
D 面板没有实现一个共同接口。
E 我们尚未封装改变的部分
F 我们侵犯了WeatherData类的封装
1235
我们的实现有什么不对?
回想第一章的概念的原则。。。。。。
好了,那我们就来看下观察者模式,然后再回来看看如何将此模式应用到气象观测站。
认识观察者模式
1.报社的业务时出版报纸
2.向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户,你就会一直收到新报纸。
3.当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
4.只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。
出版者+订阅者=观察者模式
如果你了解报纸的订阅是怎么回事,其实就知道观察者模式是怎么回事,只是名称不太一样:出版者改称为“主题”(Subject),订阅者改称为“观察者”(Observer)。
让我们来看得更仔细一点:
定义观察者模式
当你试图勾勒观察者模式时,可以利用报纸订阅服务,以及出版者和订阅者比拟这一切。
在真实的世界中,你通常会看到观察者模式被定义成:
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
让我们看看这个定义,并和之前的例子做个对照
主题和观察者定义了一对多的关系。观察者依赖于此主题,只要主题状态一有变化,观察者就会被通知。根据通知的风格,观察者可能因此新值而更新。
稍后你会看到,实现观察者模式的方法不只一种,但是以包含Subject与Observer接口的类设计的做法最常见。
让我们来看一下。
定义观察者模式:类图
松耦合的威力
当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节
观察者模式提供了一种对象设计,让主题和观察者之间松耦合
为什么呢?
关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口)。主题不需要知道观察者的具体类是谁、做了些什么或其他任何细节。
任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可以用新的观察者取代现有的观察者,主题不会受到任何影响。同样的,也可以在任何时候删除某些观察者。
有新类型的观察者出现时,主题的代码不需要修改。假如我们有个新的具体类需要当观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所有实现了观察者接口的对象。
我们可以独立地复用主题或观察者。如果我们在其他地方需要使用主题或观察者,可以轻易地复用,因为二者并非紧耦合。
改变主题或观察者其中一方,并不会影响另一方。因为两者是松合的,所以只要他们之间的接口仍被遵守,我们就可以自由地改变他们。
设计原则
为了交互对象之间的松耦合设计而努力。
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化是因为对象之间的互相依赖降到了最低。
设计气象站
看看这个设计图,和你想的是不是有所不同
实现气象站
依照上面之前的讨论,以及上面的类图,我们要开始实现这个系统。稍后,你将会在本章看到C#为观察者模式提供了内置的支持,
但是,我们暂时不用它,而是先自己动手。虽然,某些时候可以利用C#内置的支持,但是有许多时候,自己建立这一切会更具弹性(况且建立这一切并不是很麻烦)。所以,让我们从建立接口开始吧:
1 | public interface Subject |
我们先做一个假设:
把观测值直接传入观察者中是更新状态的最直接的方法。你认为这样的做法明智吗? 暗示:这些观测值的种类和个数在未来有可能改变吗?如果以后会改变,这些变化是否被很好地封装?或者是需要修改许多代
码才能办到?
关于将更新的状态传送给观察者,你能否想到更好的方法解决此问题?
在我们完成第一次实现后,我们会再回来探讨这个设计决策。
在WeatherData中实现主题接口
还记得我们在本章一开始的地方就试图实现WeatherData类吗?你可以去回顾一下。现在,我们要用观察者模式实现……
1 | /// <summary> |
好了,我们现在开始写三个面板
我们已经把WeatherData类写出来了,现在轮到布告面板了。一共三个布告面板:目前状况面板、统计面板和预测面板。我们先看看目前状况面板。
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
/// <summary>
/// 目前状况面板,继承了 观察者接口和Display 显示接口
/// </summary>
public class CurrentConditionsDisplay : Observer, DisplayElement
{
private float temperature;
private float humidity;
private Subject weatherData;
/// <summary>
/// 构造器需要 weatherData对象(也就是主题)作为注册之用
/// </summary>
/// <param name="weatherData"></param>
public CurrentConditionsDisplay(Subject weatherData)
{
this.weatherData = weatherData;
weatherData.RegisterObserver(this);
}
public void Update(float temp, float humidity, float pressure)
{
this.temperature = temp;
this.humidity = humidity;
Display();
}
public void Display()
{
//目前状况面板吧目前的温度和湿度都显示出来
Debug.Log("目前状况面板:" + temperature + ":humidity:" + humidity);
}
}
问:update()是最适合调用display()的地方吗?
这个简单的例子中,当值变化的时候调用display是很合理的。当然还有更好的,的确是有很多更好的方法来设计显示数据的方式。当我们谈到MVC(Model-View-Controller)模式时会再作说明。
问:为什么要对Subject的引用呢?构造完后似乎用不着了呀?
确实如此,但是以后我们可能想要取消注册,如果已经有对Subject的引用会比较方便。
启动气象站
1.先建立一个测试程序
气象站已经完成得差不多了,我们还需要一些代码将这一切连接起来。开始我们的第一次尝试,稍后我们会再回来确定每个组件都能通过配置文件来达到容易“插拔”。好,现在开始测试吧!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//首先建立一个WeatherData 对象
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
//另外两个面板同上 只不过我们先不用这两个
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
//模拟新的气象测量
weatherData.SetMeasurements(80,64,30);
weatherData.SetMeasurements(76, 63, 29);
weatherData.SetMeasurements(69, 61, 28);
2.运行程序 看一下输出结果
另外一种思路
接下来我们可能会思考一些有深度的问题,就是没当我们去订阅得到的参数,有些是我们不想要的,它传过来3个参数,我可能只需要一个。
有一个解决的办法就是我们可以向广播者主动索取数据。其实广播订阅,和主动拿数据对于不同的场景下可以选择使用
首先,先上图
观察者模式如何运作
新的观察者模式运作方式,和我们在气象站中的实现类似,但有一些小差异。最明显的差异是WeatherData(也就是我们的主题)现在扩展自Observable类,并继承到一些增加、删除、通知观察者的方法(以及其他的方法)。
如何把对象变成观察者……
如同以前一样,实现观察者接口(Observer),然后调用任何Observable对象的addObserver()方法。不想再当观察者时,调用deleteObserver)方法就可以了。
可观察者要如何送出通知……
首先,你需要利用Observable接口产生“可观察者”类,然后,需要两个步骤:
1.先调用setChangede方法,标记状态已经改变的事实。
2.然后调用两种notifyObservers()方法中的一个:
notifyObservers()或者
notifyObservers(0bject arg)(当通知时,此版本可以传送任何的数据对象给每一个观察者。)
观察者如何接收通知……
同以前一样,观察者实现了更新的方法,但是方法的签名不太一样:
update(observable o,object arg)
主题本身当作第一个变量,好让观察者知道是哪个主题通知它, 第二个变量 则是这正是传入 notifyObservers(0bject arg)的数据对象。如果没有说明则为空。
如果你想“推”(push)数据给观察者,你可以把数据当作数据对象传送给notifyObservers(arg)方法。否则,观察者就必须从可观察者对象中“拉”(pull)数据如何拉数据?我们再做一遍气象站,你很快就会看到。
好,接下来 再写代码前我觉的你还有点不明白,就是setchanged方法是怎么一回事?
setchanged()方法是用来标记状态已经改变的,好让notifyObservers()知道被改变是应该通知观察者,如果调用notifyObservers之前没有SetChanged 那将不会被通知。
这样做其实很有必要性。setChanged0方法可以让你在更新观察者时,有更多的弹性,你可以更适当地通知观察者。比方说,如果没有setChanged()方法,我们的气象站测量温度数据会刷新很快,这会造成weaherData对象
持续不断地通知观察者,我们并不希望看到这样的事情发生。如果我们希望半度以上才更新,就可以在温度差到半度是才调用SetChange方法进行更新。
也许不会经常使用到此功能,但是这样把功能准备好,当需要是就可以马上使用了。
重做气象站
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/// <summary>
/// 我们现在继承Observable 接口
/// 我们不再需要追踪观察者了,也不需要管理注册与删除(让基类代劳即可,记住哦这次我们继承是不是接口 是类)。
/// 所以我们把注册,添加、通知的相关代码删除。
/// </summary>
public class WeatherData : Observable
{
private float temperature;
private float humidity;
private float pressure;
/// <summary>
/// 构造函数初始化一下 我们的构造器不再需要为了记住观察者们(集合数组)而建立数据结构了。
/// </summary>
public WeatherData()
{
}
/// <summary>
/// 注意:我们没有调用notifyObservers()传送数据对象,这表示我们采用的做法是“拉”
/// </summary>
public void MeasurementsChanged()
{
//在调用notifuObservers()之前,要先调
//用setChanged()来指示状态已经改变。
SetChanged();
NotifyObservers();
}
/// <summary>
/// 外部调用赋值 并且通知 啦啦啦
/// </summary>
/// <param name="temperature"></param>
/// <param name="humidity"></param>
/// <param name="pressure"></param>
public void SetMeasurements(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
MeasurementsChanged();
}
/// <summary>
/// 下面这三个get方法函数并不是新的方法,只是因为我们要使用
/// 拉的做法,所以才提醒有这些方法。观察者会利用这些方法取得
/// WeatherData对象的状态
/// </summary>
/// <returns></returns>
public float GetTemperature()
{
return temperature;
}
public float GetHumidity()
{
return humidity;
}
public float GetPressure()
{
return pressure;
}
}
接口和基类代码放出来
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
public interface Observer
{
//所有的观察者都必须实现Update() 方法,以实现观察者接口,在这里我们把观测值都传进观察者中
public void Update(Observable obs, object arg);
}
public class Observable
{
public bool changed = false;
protected List<Observer> observers=new List<Observer>();
public void AddObServer(Observer observer)
{
observers.Add(observer);
}
public void DeleteObserver() { }
public void NotifyObservers()
{
NotifyObservers(null);
}
public void NotifyObservers(object arg)
{
if (changed)
{
for (int i = 0; i < observers.Count; i++)
{
observers[i].Update(this, arg);
}
changed = false;
}
}
public void SetChanged()
{
changed = true;
}
}
现在,让我们重做CurrentGonditionsDisplay
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
public class CurrentConditionsDisplay : Observer, DisplayElement
{
private Observable observable;
private float temperature;
private float humidity;
/// <summary>
/// 初始化构造函数里面吧 自己当做参数Add进去,登记为观察者
/// </summary>
/// <param name="observable"></param>
public CurrentConditionsDisplay(Observable observable)
{
this.observable = observable;
observable.AddObServer(this);
}
/// <summary>
/// 接口里面的Update方法Observable和数据对象作为参数
/// </summary>
/// <param name="obs"></param>
/// <param name="arg"></param>
public void Update(Observable obs, Object arg)
{
//首先确定是不是WeatherData 类,然后在get 其我们需要的数据,然后调用Display 显示
if (obs is WeatherData)
{
WeatherData weatherData = (WeatherData)obs;
this.temperature = weatherData.GetTemperature();
this.humidity = weatherData.GetHumidity();
Display();
}
}
public void Display()
{
Debug.Log("目前面板:temperature" + temperature + "humidity:" + humidity);
}
}
好了,这个代码思路就是这样了,虽然我们实现了自己可以拉取自己想要的数据(之前是基于数据直接传参,这次是把自身的实例传给各个面板供他们自己拿取),而且还可以设置状态(Changed)是不是应该拉取。 但是我们运行后会发现,这违背了我们的OO设计原则 “针对接口编程,而非针对实现编程”
如同上面所说的,可观察者是一个类,而不是一个接口,而且没有实现一个接口,限制了它的使用和复用。这并不是说它没有提供有用的功能,只是提醒大家要注意一些事实。
ObServable 是一个类
我们之前说过的设计原则中,知道不应该这样写,但是,这样写到底会造成什么问题那?
首先 ObServable 是一个类,你必须设计一个类继承它,如果某一个类想同时继承它和另外一个类,就会GG了
ObServable 将关键的方法保护起来了
看一些我们之前写的函数,setchanged方法被保护起来了,这意味着除非继承ObServable,否则无法创建ObServable 实例组合到自己的对象中来,这个设计就违背了我们之前说的 第二设计原则:多用组合 ,少用继承。
最后,不管那一种实现,经过我们上面的介绍,你应该已经熟悉了观察者模式了
尾章 复习
来到第2章的结尾,我们的设计原则又多了一些东西…· 从头捋一下 先说我们的OO设计原则
封装变化的部分
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之间的松耦合设计而努力(新增)
再说我们的00设计模式
观察者模式–在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象都会收到通知,并自动更新。
要点
观察者模式定义了对象之间一对多的关系。
主题(也就是可观察者)用一个共同的接口来更新观察者
观察者和可观察者之间用松耦合方式结合,可观察者不知道观察者的细节,只知道观察者实现了观察者接口。