设计模式-观察者模式

1.简介

观察者模式是一种对象行为模式。
在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。

它定义对象间的一种一对多的依赖关系:
当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
主体是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅并接收通知。

观察者模式(Observer)完美的将观察者和被观察的对象分离开。
观察者模式被广泛应用于软件界面元素之间的交互,在业务对象之间的交互、权限管理等方面。

此种模式通常被用来实现事件处理系统

别名:

  • 发布-订阅(Publish/Subscribe)模式
  • 模型-视图(Model/View)模式
  • 源-监听器(Source/Listener)模式
  • 从属者(Dependents)模式

2. 设计原则

  • 交互对象之间尽量采用低耦合设计
  • 封装代码中经常变化的数据

3. 模式结构

img

观察者模式所涉及的角色有:

  • 抽象主题(Subject)角色:
    抽象主题角色把所有对观察者对象的引用保存在一个聚集(比如ArrayList对象)里,每个主题都可以有任何数量的观察者。
    抽象主题提供一个接口,可以增加和删除观察者对象,抽象主题角色又叫做抽象被观察者(Observable)角色。

  • 具体主题(ConcreteSubject)角色:
    将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。
    具体主题角色又叫做具体被观察者(Concrete Observable)角色。

  • 抽象观察者(Observer)角色:
    为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。

  • 具体观察者(ConcreteObserver)角色:
    存储与主题的状态自恰的状态。
    具体观察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。
    如果需要,具体观察者角色可以保持一个指向具体主题对象的引用。

4. 推模型与拉模型

在观察者模式中,又分为推模型和拉模型两种方式。

4.1 推模型

主题对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或部分数据。

4.2 拉模型

主题对象在通知观察者的时候,只传递少量信息。
如果观察者需要更具体的信息,由观察者主动到主题对象中获取,相当于是观察者从主题对象中拉数据。

一般这种模型的实现中,会把主题对象自身通过update()方法传递给观察者,这样在观察者需要获取数据的时候,就可以通过这个引用来获取了。

4.3 二者比较

    • 推模型是假定主题对象知道观察者需要的数据

    • 拉模型是主题对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传递给观察者,让观察者自己去按需要取值

    • 推模型可能会使得观察者对象难以复用
      因为观察者的update()方法是按需要定义的参数,可能无法兼顾没有考虑到的使用情况
      这就意味着出现新情况的时候,就可能提供新的update()方法,或者是干脆重新实现观察者

    • 拉模型就不会造成这样的情况
      因为拉模型下,update()方法的参数是主题对象本身,这基本上是主题对象能传递的最大数据集合了,基本上可以适应各种情况的需要。

5. 模式案例

5.1 案例描述

你将设计一个气象监测应用,它要求具备以下几个功能:

  • 可以从气象站更新:温度,湿度,气压三项数据

  • 可以给用户用多种视图展示数据:
    初始三种视图:目前状况,天气统计,天气预报
    可以随时增加新的视图

  • 用户视图更新有两种方式:

    • 系统按事件自动更新数据同时更新视图

    • 用户主动请求更新数据与视图

    5.2 案例分析

    天气监测应用设计类图

从上图可以分析出各个类在观察者模式中他们的角色:

  • 抽象主题(Subject)角色:Subject

  • 具体主题(ConcreteSubject)角色:WeatherData

  • 抽象观察者(Observer)角色:Observer

  • 具体观察者(ConcreteObserver)角色:StatisticsDisplay;ThirdPartDisplay;ForecastDisplay;

5.3 代码编写

5.3.1 抽象主题编写

1
2
3
4
5
6
public interface Subject
{
public void registerObserver(Observer a);//添加观察者订阅
public void removeObserver(Observer a);//移除观察者订阅
public void notifyObserver();//广播
}

5.3.2 抽象观察者编写

1
2
3
4
5
public interface Observer
{
public void update(Data x);//广播更新的数据(推模型)
public void pulldate();//观察者申请数据更新(拉模型)
}

5.3.3 具体主题编写

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
public class WeatherData:Subject
{
public struct Data
{
public double temperature;//温度
public double humidity;//湿度
public double pressure;//气压
}
private Data data;
List<Observer> listeners;//订阅的观察者名单
public Data Datas { get => data;}//用于实现拉模式的get属性

public WeatherData()
{
listeners = new List<Observer>();
measurementsChanged();
data = new Data();
}
//对Subject接口的实现
public void registerObserver(Observer a)=> listeners.Add(a);
public void removeObserver(Observer a)=> listeners.Remove(a);
public void notifyObserver()
{
foreach(Observer i in listeners)
{
i.update(data);
}
}

public void measurementsChanged()//更新天气数据
{
getRandomDate();
notifyObserver();
}
private void getRandomDate()//随机产生天气数据
{
Random random = new Random();
data.humidity = random.NextDouble();
data.temperature=random.Next(-40,40)+random.NextDouble();
data.pressure = random.Next(1, 2)+random.NextDouble();
}
}

5.3.4 具体观察者编写

显示模式的接口:

1
2
3
4
public interface DisplayElement
{
public void display();
}

三种显示模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CurrentConditionsDisplay:Observer,DisplayElement//显示当前观测值
{
private Subject weatherData;
private Data d;
public CurrentConditionsDisplay(Subject x)
{
weatherData = x;
weatherData.registerObserver(this);
}
//实现接口
public void update(Data x)
{
d = x;
display();
}
public void pulldate() => update(((WeatherData)weatherData).Datas);
public void display()
{
Console.WriteLine("---当前天气---");
Console.WriteLine("当前温度:" + d.temperature.ToString("F2") + "\t当前气压:"
+ d.pressure.ToString("F2") + "\t当前湿度:"+d.humidity.ToString("F2") + "\n") ;
}
}
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 StatisticsDisplay : Observer, DisplayElement//显示统计最大最小平均值
{
Subject weatherData;
private Queue<Data> dates;
public StatisticsDisplay(Subject x)
{
weatherData = x;
weatherData.registerObserver(this);
dates = new Queue<Data>();
}
//实现接口
public void update(Data x)
{
if(dates.Count== 3)dates.Dequeue();
dates.Enqueue(x);
display();
}
public void pulldate()=> update(((WeatherData)weatherData).Datas);
public void display()
{
Double[] average = { 0, 0, 0 };
foreach (Data i in dates)
{
average[0] += i.temperature;
average[1] += i.pressure;
average[2] += i.humidity;
}
Console.WriteLine("***天气统计***");
Console.WriteLine("最高温度:" + dates.Max(x => x.temperature).ToString("F2") +
"\t最低温度:" + dates.Min(x => x.temperature).ToString("F2") + "\t平均温度:" +
(average[0] / dates.Count).ToString("F2"));

Console.WriteLine("最高气压:" + dates.Max(x => x.pressure).ToString("F2") +
"\t最低气压:" + dates.Min(x => x.pressure).ToString("F2") + "\t平均气压:" +
(average[1] / dates.Count).ToString("F2"));

Console.WriteLine("最高湿度:" + dates.Max(x => x.humidity).ToString("F2") +
"\t最低湿度:" + dates.Min(x => x.humidity).ToString("F2") + "\t平均湿度:" +
(average[2] / dates.Count).ToString("F2") + "\n");
}
}
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
public class ForecastDisplay : Observer, DisplayElement//显示天气预报
{
private Subject weatherData;
Data d;
String weather;
double t;
public ForecastDisplay(Subject x)
{
weatherData = x;
weatherData.registerObserver(this);
}
//实现接口
public void update(Data data)
{
d = data;
if (d.humidity > 0.5 && d.pressure > 1)
{
if(d.temperature<1) weather = "雪";
else weather = "雨";
}
else weather = "晴";
t = d.temperature;
display();
}
public void pulldate() => update(((WeatherData)weatherData).Datas);
public void display()
{
Console.WriteLine("===天气预报===");
Console.WriteLine("今日天气:" + weather + "\t 温度:" + t.ToString("F2")+"\n"); ;
}
}

5.3.5 测试

1
2
3
4
5
6
7
8
9
10
11
static void Main(string[] args)
{
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay s1 = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay s2 = new StatisticsDisplay(weatherData);
ForecastDisplay s3 = new ForecastDisplay(weatherData);
for(int i=0;i<3;i++)
{
weatherData.measurementsChanged();
}
}

结果如下图所示:

测试结果

6. java中的观察者模式类

https://www.jianshu.com/p/fc4554cda68d

8. C#中实现观察者模式的方法

整理自:https://www.jb51.net/article/63077.htm

8.1 利用事件

请先学习C#事件

我们将上面讲的天气数据监测应用改成用C#事件实现的观察者模式

  1. 在WeatherData类中声明委托与事件

    1
    2
    public delegate void WeatherdateUpdate(Data data);
    public static event WeatherdateUpdate updata;
  2. 在WeatherData类中编写事件触发函数

    1
    2
    3
    4
    5
    6
    7
    public void notifyByEvent()
    {
    if(updata!=null)
    {
    updata(data);
    }
    }
  3. 在三个具体观察者中编写事件处理函数并在构造函数中订阅到事件

    1
    2
    3
    4
    5
    6
    7
    8
    //写在构造函数里
    WeatherData.updata += update;//事件订阅

    public void update(Data x)//事件处理
    {
    d = x;
    display();
    }
  4. 触发事件

    1
    2
    3
    4
    5
    6
    public void measurementsChanged()
    {
    getRandomDate();
    //notifyObserver();
    notifyByEvent();
    }

    运行输出结果与用接口实现的效果完全相同。

8.2 利用IObservable和IObserver实现

与java中类似,略

8.3 利用Action函数式

7. 优缺点

7.1 优点

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
  • 目标与观察者之间建立了一套触发机制。

7.2 缺点

  • 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。

  • 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。