观察观察者模式

[TOC]

示例

股票变化(C#实现)

监控某一个公司(Microsoft)的股票价格变化,可以有多种方式,通知的对象可以是投资者,或者是发送到移动设备,还有电子邮件等

一开始我们先不考虑Observer模式,通过一步步地重构,最终重构为Observer模式

现在有这样两个类:MicrosoftInvestor,如下图所示:

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
public class Microsoft
{
public void Update()

{
Investor.SendData(this);
}

public Investor Investor { get; set; }

public String Symbol { get; set; }

public double Price { get; set; }
}

public class Investor
{
private string _name;

public Investor(string name)

{
this._name = name;
}

public void SendData(Microsoft ms)

{
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _name, ms.Symbol, ms.Price);

}

}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Program
{
static void Main(string[] args)
{
Investor investor = new Investor("Jom");

Microsoft ms = new Microsoft
{
Investor = investor,

Symbol = "Microsoft",

Price = 120.00
};


ms.Update();

Console.ReadLine();
}
}

运行后结果如下:

1
Notified Jom of Microsoft's change to ¥120

可以看到,这段代码运行并没有问题,也确实实现了我们最初的设想的功能,把Microsoft的股票价格变化通知到了Jom投资者那儿。

但是这里面出现了如下几个问题:

  • Microsoft和Investor之间形成了一种双向的依赖关系,即Microsoft调用了Investor的方法,而Investor调用了Microsoft类的属性。如果有其中一个类变化,有可能会引起另一个的变化。

  • 当出现一种的通知对象,比如说是移动设备Mobile:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Mobile
    {
    private string _no;

    public Mobile(string no)

    {
    this._no = no;
    }

    public void SendData(Microsoft ms)

    {
    Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _no, ms.Symbol, ms.Price);

    }

    }

    这时候对应的Microsoft的类就应该改变为如下代码,在Microsoft类中增加Mobile,同时修改Update()方法使其可以通知到移动设备:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Microsoft
    {
    public void Update()

    {
    Investor.SendData(this);
    Mobile.SendData(this);
    }
    public Mobile Mobile { get; set; }
    public Investor Investor { get; set; }

    public String Symbol { get; set; }

    public double Price { get; set; }
    }

显然这样的设计极大的违背了“开放-封闭”原则,这不是我们所想要的。

对此做进一步的抽象,既然出现了多个通知对象,我们就为这些对象之间抽象出一个接口,用它来取消Microsoft和具体的通知对象之间依赖

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
public interface IObserver
{
void SendData(Microsoft ms);
}


public class Microsoft
{
public void Update()

{
Investor.SendData(this);
//Mobile.SendData(this);
}
//public Mobile Mobile { get; set; }
public IObserver Investor { get; set; }

public String Symbol { get; set; }

public double Price { get; set; }
}

public class Investor: IObserver
{
private string _name;

public Investor(string name)

{
this._name = name;
}

public void SendData(Microsoft ms)

{
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _name, ms.Symbol, ms.Price);

}

}


public class Mobile

{
private string _no;

public Mobile(string no)

{
this._no = no;
}

public void SendData(Microsoft ms)

{
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", _no, ms.Symbol, ms.Price);

}

}

可以看到,我们在降低两者的依赖性上已经迈进了一小步,正在朝着弱依赖性这个方向变化。在Microsoft类中已经不再依赖于具体的Investor,而是依赖于接口IObserver。

但同时我们看到,再新出现一个移动设备这样的通知对象,Microsoft类仍然需要改变,对此我们再做如下重构,在Microsoft中维护一个IObserver列表,同时提供相应的维护方法。

此时的实现

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
public class Microsoft
{
public void Update()
{
foreach (IObserver observer in observers)
{
observer.SendData(this);
}

}

public void AddObserver(IObserver observer)
{
observers.Add(observer);
}

public void RemoveObserver(IObserver observer)
{
observers.Remove(observer);
}

private List<IObserver> observers = new List<IObserver>();

public IObserver Investor { get; set; }

public String Symbol { get; set; }

public double Price { get; set; }
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Program
{
static void Main(string[] args)
{
IObserver investor1 = new Investor("Tom");

IObserver investor2 = new Investor("Jerry");

Microsoft ms = new Microsoft
{
Symbol = "Microsoft",

Price = 120.00
};

ms.AddObserver(investor1);

ms.AddObserver(investor2);

ms.Update();

Console.ReadLine();
}
}

走到这一步,已经有了Observer模式的影子了,Microsoft类不再依赖于具体的Investor,而是依赖于抽象的IOberver。

存在着的一个问题是Investor仍然依赖于具体的公司Microsoft,况且公司还会有很多IBM,Google等,解决这样的问题很简单,只需要再对Microsoft类做一次抽象。如下图所示:

此时的实现

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
68
69
70
71
public interface IObserver
{
void SendData(Stock stock);
}

public abstract class Stock
{
private List<IObserver> observers = new List<IObserver>();

public Stock(String symbol, double price)

{
this.Symbol = symbol;

this.Price = price;
}

public void Update()

{
foreach (IObserver ob in observers)

{
ob.SendData(this);
}

}

public void AddObserver(IObserver observer)

{
observers.Add(observer);
}

public void RemoveObserver(IObserver observer)

{
observers.Remove(observer);
}

public String Symbol { get; }

public double Price { get; }
}

public class Microsoft : Stock
{
public Microsoft(String symbol, double price) : base(symbol, price)

{ }
}

public class Investor : IObserver
{
private string _name;

public Investor(string name)

{
this._name = name;
}

public void SendData(Stock stock)

{
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}",
_name, stock.Symbol, stock.Price);

}

}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Program
{
static void Main(string[] args)
{
Stock ms = new Microsoft("Microsoft", 120.00);

ms.AddObserver(new Investor("Tom"));

ms.AddObserver(new Investor("Jerry"));

ms.Update();

Console.ReadLine();
}
}

到这里我们可以看到,通过不断的重构,不断地抽象,我们由一开始的很糟糕的设计,逐渐重构为使用Observer模式的这样一个方案。

在这个例子里面,IOberser充当了观察者的角色,而Stock则扮演了主题对象角色,在任何时候,只要调用了Stock的Update()方法,它就会通知它的所有观察者对象。

同时可以看到,通过Observer模式,取消了直接依赖,变为间接依赖,这样大大提供了系统的可维护性和可扩展性。

.NET中的观察者模式

利用事件和委托来实现Observer模式我认为更加的简单和优雅,也是一种更好的解决方案。

因为在上面的示例中我们可以看到,虽然取消了直接耦合,但是又引入了不必要的约束(暂且这么说吧)。即那些子类必须都继承于主题父类,还有观察者接口等。

上面的例子简单的用事件和委托实现如下:

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
class Program {
static void Main (string[] args) {
Stock stock = new Stock ("Microsoft", 120.00);

Investor investor = new Investor ("Tom");

stock.NotifyEvent += new NotifyEventHandler (investor.SendData);

stock.Update ();

Console.ReadLine ();
}
}

public delegate void NotifyEventHandler (object sender);

public class Stock {
public NotifyEventHandler NotifyEvent;

private String _symbol;

private double _price;

public Stock (String symbol, double price)

{
this._symbol = symbol;

this._price = price;
}

public void Update () {
OnNotifyChange ();
}

public void OnNotifyChange () {
if (NotifyEvent != null) {
NotifyEvent (this);
}
}

public String Symbol {
get { return _symbol; }
}

public double Price {
get { return _price; }
}
}

public class Investor {
private string _name;

public Investor (string name) {
this._name = name;
}

public void SendData (object obj) {
if (obj is Stock) {
Stock stock = (Stock) obj;
Console.WriteLine ("Notified {0} of {1}'s " + "change to {2:C}",
_name, stock.Symbol, stock.Price);
}
}
}

把无返回值委托换成action

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
class Program
{
static void Main(string[] args)
{
Stock1 stock = new Stock1("Microsoft", 120.00);

Investor1 investor = new Investor1("Tom");

stock.NotifyEvent += investor.SendData;

stock.Update();

Console.ReadLine();
}
}

// public delegate void NotifyEventHandler(object sender);

//public Action<object> NotifyEventHandler;

public class Stock1
{
public event Action<object> NotifyEvent;

public Stock1(string symbol, double price)

{
this.Symbol = symbol;

this.Price = price;
}

public void Update()
{
OnNotifyChange();
}

public void OnNotifyChange()
{
NotifyEvent?.Invoke(this);
}

public string Symbol { get; }

public double Price { get; }
}

public class Investor1
{
private readonly string _name;

public Investor1(string name)
{
this._name = name;
}

public void SendData(object obj)
{
if (obj is Stock1 stock)
{
Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}",
_name, stock.Symbol, stock.Price);
}
}
}

钓鱼(C#实现)

鱼竿是被观察者,
铃铛是通知工具,
垂钓者是观察者。

鱼儿咬钩,鱼竿通过铃铛通知垂钓者收钩。

先来定义鱼的品类枚举:

1
2
3
4
5
6
7
8
9
public enum FishType
{
鲫鱼,
鲤鱼,
黑鱼,
青鱼,
草鱼,
鲈鱼
}

接下来申明一个钓鱼工具的抽象类,维护订阅者列表,并负责循环通知订阅者。

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
/// <summary>
/// 钓鱼工具抽象类
/// 用来维护订阅者列表,并通知订阅者
/// </summary>
public abstract class FishingTool
{
private readonly List<ISubscriber> _subscribers;

protected FishingTool()
{
_subscribers = new List<ISubscriber>();
}

public void AddSubscriber(ISubscriber subscriber)
{
if (!_subscribers.Contains(subscriber))
_subscribers.Add(subscriber);
}

public void RemoveSubscriber(ISubscriber subscriber)
{
if (_subscribers.Contains(subscriber))
_subscribers.Remove(subscriber);
}

public void Notify(FishType type)
{
foreach (var subscriber in _subscribers)
subscriber.Update(type);
}

}

鱼竿的实现,这里用随机数模拟鱼儿咬钩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// 鱼竿
/// </summary>
public class FishingRod : FishingTool
{
public void Fishing()
{
Console.WriteLine("开始下钩!");

//用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
if (new Random().Next() % 2 == 0)
{
var type = (FishType)new Random().Next(0, 5);
Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了");
Notify(type);
}
}
}

定义简单的观察者接口:

1
2
3
4
5
6
7
8
/// <summary>
/// 订阅者(观察者)接口
/// 由具体的订阅者实现Update()方法
/// </summary>
public interface ISubscriber
{
void Update(FishType type);
}

垂钓者实现观察者接口,并定义了NameFishCount 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// 垂钓者实现观察者接口
/// </summary>
public class FishingMan : ISubscriber
{
public FishingMan(string name)
{
Name = name;
}

public string Name { get; set; }
public int FishCount { get; set; }

public void Update(FishType type)
{
FishCount++;
Console.WriteLine("{0}:钓到一条[{2}],已经钓到{1}条鱼了!", Name, FishCount, type);
}
}

测试:

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
class Program
{
static void Main(string[] args)
{
Console.WriteLine("简单实现的观察者模式:");
Console.WriteLine("=======================");
//1、初始化鱼竿
var fishingRod = new FishingRod();

//2、声明垂钓者
var fisher1 = new FishingMan("fisher1");
var fisher2 = new FishingMan("fisher2");

//3、将垂钓者观察鱼竿
fishingRod.AddSubscriber(fisher1);
fishingRod.AddSubscriber(fisher2);

//4、循环钓鱼
while (fisher1.FishCount < 5 || fisher2.FishCount < 5)
{
fishingRod.Fishing();
Console.WriteLine("-------------------");
//睡眠0.5s
Thread.Sleep(500);
}
}
}

用委托实现

有了委托,我们就不再需要定义专门的抽象被观察者对象了,直接实现鱼竿:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// <summary>
/// 鱼竿
/// </summary>
public class FishingRod
{
public delegate void FishingHandler(FishType type); //声明委托
public event FishingHandler FishingEvent; //声明事件

public void Fishing()
{
Console.WriteLine("开始下钩!");

//用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
if (new Random().Next() % 2 == 0)
{
var a = new Random(10).Next();
var type = (FishType)new Random().Next(0, 5);
Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了");
FishingEvent?.Invoke(type);
}
}
}

因为被观察者定义了委托,我们也没必要定义专门的观察者接口,只需要在具体的观察者中实现对应的委托即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// 垂钓者(观察者)
/// </summary>
public class FishingMan
{
public FishingMan(string name)
{
Name = name;
}

public string Name { get; set; }
public int FishCount { get; set; }

public void Update(FishType type)
{
FishCount++;
Console.WriteLine("{0}:钓到一条[{2}],已经钓到{1}条鱼了!", Name, FishCount, type);
}
}

客户端:

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

class Program
{
static void Main(string[] args)
{
Console.WriteLine("委托实现的观察者模式:");
Console.WriteLine("=======================");
//1、初始化鱼竿
var fishingRod = new FishingRod();

//2、声明垂钓者
var fisher = new FishingMan("fisher1");

//3、注册观察者
fishingRod.FishingEvent += fisher.Update;

//4、循环钓鱼
while (fisher.FishCount < 5)
{
fishingRod.Fishing();
Console.WriteLine("-------------------");
//睡眠0.5s
Thread.Sleep(500);
}
}
}

进制转换器(python实现)

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""http://code.activestate.com/recipes/131499-observer-pattern/"""


class Subject(object):
def __init__(self):
self._observers = []

def attach(self, observer):
if not observer in self._observers:
self._observers.append(observer)

def detach(self, observer):
try:
self._observers.remove(observer)
except ValueError:
pass

def notify(self, modifier=None):
for observer in self._observers:
if modifier != observer:
observer.update(self)


# Example usage
class Data(Subject):
def __init__(self, name=''):
Subject.__init__(self)
self.name = name
self._data = 0

@property
def data(self):
return self._data

@data.setter
def data(self, value):
self._data = value
self.notify()


class HexViewer:
def update(self, subject):
print('HexViewer: Subject %s has data 0x%x' %
(subject.name, subject.data))


class DecimalViewer:
def update(self, subject):
print('DecimalViewer: Subject %s has data %d' %
(subject.name, subject.data))


# Example usage...
def main():
data1 = Data('Data 1')
data2 = Data('Data 2')
view1 = DecimalViewer()
view2 = HexViewer()
data1.attach(view1)
data1.attach(view2)
data2.attach(view2)
data2.attach(view1)

print("\nDetachSetting Data 1 = 10")
data1.data = 10

print("\nSetting Data 2 = 15")
data2.data = 15

print("\nSetting Data 1 = 3")
data1.data = 3

print("\nSetting Data 2 = 5")
data2.data = 5

print("\nDetach HexViewer from data1 and data2.")
data1.detach(view2)
data2.detach(view2)
print("\nSetting Data 1 = 10")
data1.data = 10

print("\nSetting Data 2 = 15")
data2.data = 15



if __name__ == '__main__':
main()

登录网站模块(JavaScript实现)

JavaScript中的订阅-发布模式和别的语言(比如Java)中的实现还是有区别的.

在Java中,通常会把订阅者对象自身当成引用传入发布者对象中,同时订阅者对象还需提供一个名为诸如update 的方法,供发布者对象在合适的时候调用

在JavaScript中,用注册回调函数的形式来代替传统的发布-订阅模式,显得更加优雅和简单

注:在这里的回调其实就相当于例1中的事件委托stock.NotifyEvent += new NotifyEventHandler (investor.SendData);.NET使用事件封装了发布订阅模式,语法糖更易用,但是不可能达到解释型语言的灵活性

假如我们正在开发一个商城网站,网站里有header头部,nav 导航,消息列表,购物车等模块.这几个模块渲染有一个共同的前提条件,就是必须先用ajax 异步请求获取用户的登录信息

1
2
3
4
5
6
7
login.succ(function(data) {

header.setAvatar(data.avatar); //设置header模块的头像
nav.setAvatar(data.avatar);//设置导航模块的头像
message.refresh()//刷新消息列表
cart.refresh()//刷新购物车列表
});

header,nav等各个模块和用户信息产生了强耦合,方法名也不能随意再改变

等到有一天,项目中增加了一个收货地址管理的模块,则必须在原来的逻辑中增加

1
address.refresh()

用发布订阅模式重写之后,登录模块不用关心业务模块究竟做什么,也不想了解内部细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$.ajax('http://xxx.com?login', function(data) {
login.trigger('loginSucc', data);
});

var header = (function() { // header模块
login.listen('loginSucc', function() {
header.setAvatar(data.avatar);
})
return setAvatar:function(avatar){
console.log('设置header模块的头像')
}
})();


var nav = (function() { // header模块
login.listen('loginSucc', function() {
nav.setAvatar(data.avatar);
})
return setAvatar:function(avatar){
console.log('设置nav模块的头像')
}
})();

我们可以随时更改setAvatar 方法名

如果某天需求增加了刷新收货列表的行为,只需要增加监听消息的方法即可

1
2
3
4
5
6
7
8
var address = (function() { //address模块
login.listen('loginSucc', function() {
address.refresh();
})
return refresh: function() {
console.log('刷新收货地址列表')
}
})();

事件管理器(JavaScript实现)

使用发布订阅模式写一个事件管理器,可以实现如下方式调用

1
2
3
4
5
EventManager.on('text:change', function(val){
console.log('text:change... now val is ' + val);
});
EventManager.trigger('text:change', '饥人谷');
EventManager.off('text:change');
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
68
69
70
71
72
73
74
75
76
77
78
79
//ES5
var EventManager = (function() {
var events = {};

function on(evt, handler) {

if (typeof handler !== 'function') {
throw new TypeError('handler is not a function!');
return;
}

events[evt] = events[evt] || [];
events[evt].push(handler);
}

function trigger(evt, args) {
if (!events[evt]) {
return;
}

events[evt].forEach(function(handler) {
handler(args);
})
}

function off(evt) {
delete events[evt];
}

return {
on: on,
trigger: trigger,
off: off
};
})();


//ES6
const EventManager = ((() => {
const events = {};

function on(evt, handler) {

if (typeof handler !== 'function') {
throw new TypeError('handler is not a function!');
return;
}

events[evt] = events[evt] || [];
events[evt].push(handler);
}

function trigger(evt, args) {
if (!events[evt]) {
return;
}

events[evt].forEach(handler => {
handler(args);
})
}

function off(evt) {
delete events[evt];
}

return {
on,
trigger,
off
};
}))();

//调用
EventManager.on('text:change', function(val){
console.log('text:change... now val is ' + val);
});
EventManager.trigger('text:change', '饥人谷');
EventManager.off('text:change');