•1 什么是观察者模式
我们在日常生活中有很多观察者模式应用的场景。比如,凡是去过银行营业大厅办理业务的人,大多会有这么一段经历:办理业务之前,先要在营业大厅的门口领取一个排队号,然后你就可以在休息区等待叫号,当轮到自己办理业务的时候,某个柜台上方悬挂的小显示屏就会出现“请XXX号到XX柜台办理业务”。有的时候,某个柜台可能暂时停止办理业务,那么柜台上方的小屏就显示跟其它柜台上方的小屏同样的内容,以便提醒当前用户办理。对于这样的一个需求,你会怎么去实现它呢?我经过一番思考之后,写出了这么一段代码:

Java代码:

//银行柜台类
import java.util.Vector;
public class Counter {
 //小屏列表
 private Vector<SmallScreen> vectSmallScreen = new Vector<SmallScreen>();
 //当前业务号
 private String bizNo;
 //柜台名称
 private String name;
 //构造函数
 public Counter(String name){
  this.name = name;
 }
 //增加一个小屏
 public void attach(SmallScreen smallScreen){
  vectSmallScreen.add(smallScreen);
 }
 //去除一个小屏
 public void detach(SmallScreen smallScreen){
  vectSmallScreen.remove(smallScreen);
 }
 //通知小屏显示
 public void notifyChange(){
  for(int i=0; i<vectSmallScreen.size(); i++){
   SmallScreen smallScreen = vectSmallScreen.get(i);
   smallScreen.display();
  }
 } 
 //获取当前业务号
 public String getBizNo(){
  return this.bizNo;
 }
 //设置当前业务号
 public void setBizNo(String bizNo){
  this.bizNo = bizNo;
 }
 //获取主题信息
 public String getSubject(){
  return "请" + this.bizNo + "号到" + this.name + "号柜台办理业务";
 }
}
//小显示屏类
public class SmallScreen {
 private String name;
 private Counter counter;
 //构造函数
 public SmallScreen(String name,Counter counter){
  this.name = name;
  this.counter = counter;
 }
 //更新显示屏
 public void display(){
  try{
   System.out.println(this.name + ":" + counter.getSubject());
  }
  catch(Exception err){
  }
 } 
}
//业务系统类
public class BankBiz {
 public static void main(String[] args) {
  Counter counter = new Counter("1号柜台");
  //1,2号小屏
  SmallScreen smallScreen1 = new SmallScreen("1号小屏",counter);
  SmallScreen smallScreen2 = new SmallScreen("2号小屏",counter);
  //加入通知小屏
  counter.attach(smallScreen1);
  counter.attach(smallScreen2);
  //9号办理业务
  counter.setBizNo("9");
  //通知小屏更新
  counter.notifyChange();
 }
}


Php代码:

<?php
//银行柜台类
class Counter {
 //小屏列表
 private $arrSmallScreen = array();
 //当前业务号
 private $bizNo = "";
 //柜台名称
 private $name = "";
 //构造函数
 public function __construct($name){
  $this->name = $name;
 }
 //增加一个小屏
 public function attach(&$smallScreen){
  $this->arrSmallScreen[] = $smallScreen;
 }
 //去除一个小屏
 public function detach(&$smallScreen){
  for($i=0; $i<count($this->arrSmallScreen); $i++){
   $objTemp = $this->arrSmallScreen[$i];
   if($smallScreen === $objTemp){
    array_splice($this->arrSmallScreen,$i,1);
    break;
   }
  }
 }
 //通知小屏显示
 public function notifyChange(){
  foreach ($this->arrSmallScreen as $smallScreen){
   $smallScreen->display();
  }
 } 
 //获取当前业务号
 public function getBizNo(){
  return $this->bizNo;
 }
 //设置当前业务号
 public function setBizNo($bizNo){
  $this->bizNo = $bizNo;
 }
 //获取主题信息
 public function getSubject(){
  return  "请" + $this->bizNo . "号到" . $this->name . "号柜台办理业务";
 }
}
//小显示屏类
class SmallScreen {
 private $name = "";
 private $counter = null;
 //构造函数
 public function __construct($name,$counter){
  $this->name = $name;
  $this->counter = $counter;
 }
 //更新显示屏
 public function display(){
  echo $this->name . ":" . $this->counter->getSubject();
 } 
}
//业务系统类
class BankBiz {
 public static function execute() {
  $counter = new Counter("1号柜台");
  //1,2号小屏
  $smallScreen1 = new SmallScreen("1号小屏",$counter);
  $smallScreen2 = new SmallScreen("2号小屏",$counter);
  //加入通知小屏
  $counter->attach($smallScreen1);
  $counter->attach($smallScreen2);
  $counter->detach($smallScreen2);
  //9号办理业务
  $counter->setBizNo("9");
  //通知小屏更新
  $counter->notifyChange();
 }
}
BankBiz::execute();
?>


//运行结果如下

1号小屏:请9号到1号柜台号柜台办理业务

2号小屏:请9号到1号柜台号柜台办理业务

 

观察者模式又叫做发布-订阅(Publish/Subscribe)模式。观察者模式定义了一种一对多地依赖模式,让多个观察者同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有的观察者对象,使它们能够自动更新自己。这里的主题对象就是指通知者,又叫做发布者。观察者又叫订阅者。
    在上面这段代码中,我们让多个小屏同时监听银行柜台信息的变化,每当发生变化时,就立即通知所有监听的小屏,这些小屏就会更新自己的显示内容。在这个案例中,小屏就是观察者,银行柜台就是通知者,当通知者自身发生变化后,就会通知观察者,观察者会根据自身情况进行相应的更新。这就是观察者模式的雏形。
如果我们不使用观察者模式,那么用什么办法才能实现这样的需求呢?最直接的想法就是让各个小屏每隔一段时间就查询一次银行柜台,并判断银行柜台是否发生了改变,如果发生了改变,就去更新自己的显示内容,显然这是一种轮询的实现方式。根据这个思路,小屏类SmallScreen的Update方法就应该这样实现:

Java代码:

//更新显示屏
public void display(){
 try{
  //使用死循环判断主题是否发生改变
  while( counter.getSubjectChanged() ){
   //中间休息1秒钟
   Thread.sleep(1000);
   //显示改变后的主题
   System.out.println(this.name + ":" + counter.getSubject());
  }
 }
 catch(Exception err){
 }
}


Php代码:

//更新显示屏
public function display(){
 //使用死循环判断主题是否发生改变
 while( $this->counter->getSubjectChanged() ){
  //中间休息1秒钟
  sleep(1000);
  //显示改变后的主题
  echo $this->name . ":" . $this->counter->getSubject());
 }
}


这跟观察者模式到底有什么不同呢?很显然,这种实现方式是一种“拉模型”,观察者需要自己主动轮询通知者的状态。观察者模式与之相反,它是一种“推模型”,通知者把变化通知给观察者。
观察者模式又叫做发布-订阅(Publish/Subscribe)模式。观察者模式定义了一种一对多地依赖模式,让多个观察者同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有的观察者对象,使它们能够自动更新自己。这里的主题对象就是指通知者,又叫做发布者。观察者又叫订阅者。在这个例子中,小屏类SmallScreen就是观察者Observer,银行柜台类Counter就是主题对象Subject。

•2 需求变化后的尴尬
我们的代码已经解决了由“小屏往回拉信息”到“柜台推送信息”的模式改变,这种改变的好处是效率的得到了提高,至少不用小屏频繁的访问银行柜台了,但是系统的可扩展性和可维护性却变差了,为什么这么说呢?我们可以分析一下:
在“小屏往回拉信息”的模式下,银行柜台类Counter,只要提供两个方法就行了,一个是getSubjectChanged,小屏通过它就可以知道银行柜台是否发生变化,另一个是getSubject,小屏通过它就可以获取银行柜台发布的内容,甚至于这两个方法完全可以合并成一个。在这种模式下,Counter与SmallScreen耦合点就是这两个方法。类SmallScreen必须访问类Counter,但是类Counter不必访问类SmallScreen,两者是一个单向耦合的关系。
在“柜台推送信息”的模式下,我们可以看一下代码。银行柜台类Counter发生变化的时候,强制小屏类SmallScreen进行Update,所以类Counter必须用到类SmallScreen,这是第一个耦合点。小屏类SmallScreen更新自己内容的时候,需要调用银行柜台类Counter的getSubject方法,这是第二个耦合点。所以,两者互相调用对方的方法,这是一个双向耦合的关系。
双向耦合有什么危害呢?耦合越多越紧密,系统就越难以维护,无论是修改或者扩展其中一个类的功能,都可能影响到其它的类。如果类之间都是独立的,或者联系非常弱,那么修改任何一个类,都不会影响到其它类,这是非常理想的状态。
现在银行方面又追加了一个需求:除了小屏显示信息之外,还需要通过音箱进行呼叫。这个需求在排队系统中是非常普遍的。我们之前完成的代码怎样应对这种变化呢?首先,我们需要再创建一个音箱类Speaker,然后需要在类Counter里面再增加一个音箱列表,最后在类Counter通知变化的方法notifyChange里再遍历一遍音箱列表。

Java代码:

//银行柜台类
import java.util.Vector;
public class Counter {
 //小屏列表
 private Vector<SmallScreen> vectSmallScreen = new Vector<SmallScreen>();
 //音箱列表
 private Vector<Speaker> vectSpeaker = new Vector<Speaker>();
 ......
 //通知小屏显示和音箱呼叫
 public void notifyChange(){
  for(int i=0; i<vectSmallScreen.size(); i++){
   SmallScreen smallScreen = vectSmallScreen.get(i);
   smallScreen.display();
  }
  for(int i=0; i< vectSpeaker.size(); i++){
   Speaker speaker = vectSpeaker.get(i);
   speaker.call();
  }
 } 
 ......
}
//音箱类
public class Speaker {
 private String name;
 private Counter counter;
 //构造函数
 public Speaker(String name,Counter counter){
  this.name = name;
  this.counter = counter;
 }
 //更新音箱呼叫
 public void call(){
  try{
   System.out.println(this.name + ":" + counter.getSubject());
  }
  catch(Exception err){
  }
 } 
}
 

Php代码:

//银行柜台类
public class Counter {
 //小屏列表
 private $arrSmallScreen = array();
 //音箱列表
 private $arrSpeaker = array();
 ......
 //通知小屏显示和音箱呼叫
 public function notifyChange(){
  foreach ($this->arrSmallScreen as $smallScreen){
   $smallScreen->display();
  }
  foreach ($this->arrSpeaker as $speaker){
   $speaker->call();
  }  
 }
 ......
}
//音箱类
class Speaker {
 private $name = "";
 private $counter = null;
 //构造函数
 public function __construct($name,$counter){
  $this->name = $name;
  $this->counter = $counter;
 }
 //更新显示屏
 public function call(){
  echo $this->name . ":" . $this->counter->getSubject();
 } 
}
 

我们增加了一个类Speaker,还修改了类Counter,,当前提出的需求算是解决了,可是代码的耦合度却加大了,已经有3个类纠缠在一起了,那么以后再追加接收信息的终端怎么办?每一次追加观察者,都不得不改一遍类Counter的代码,如果继续做下去,出错的几率将不断加大,可维护性不断降低,这段代码明显违背开闭原则,所以,我们需要重新审视之前的设计了。

•3 继续改进观察者模式
面向对象的设计中最重要的思维就是抽象。显然,无论是小屏、大屏还是音箱,它们只是外观和更新信息的手段有所不同,更新信息的功能却是一致的,所以,我们完全可以把这些观察者抽象出来一个基类Observer,更新信息的手段由各个观察者自己负责实现。我们再修改一遍代码:

Java代码:

//银行柜台类
import java.util.Vector;
public class Counter {
 //观察者列表
 private Vector<Observer> vectObserver = new Vector<Observer>();
 //当前业务号
 private String bizNo;
 //柜台名称
 private String name;
 //构造函数
 public Counter(String name){
  this.name = name;
 }
 //增加一个小屏
 public void attach(Observer observer){
  vectObserver.add(observer);
 }
 //去除一个小屏
 public void detach(Observer observer){
  vectObserver.remove(observer);
 }
 //通知小屏显示
 public void notifyChange(){
  for(int i=0; i<vectObserver.size(); i++){
   Observer observer = vectObserver.get(i);
   observer.update();
  }
 } 
 //获取当前业务号
 public String getBizNo(){
  return this.bizNo;
 }
 //设置当前业务号
 public void setBizNo(String bizNo){
  this.bizNo = bizNo;
 }
 //获取主题信息
 public String getSubject(){
  return "请" + this.bizNo + "号到" + this.name + "号柜台办理业务";
 }
}
//通知设备基类
abstract public class Observer {
 protected String name;
 protected Counter counter;
 //构造函数
 public Observer(String name,Counter counter){
  this.name = name;
  this.counter = counter;
 }
 //更新信息
 public abstract void update();
}
//小显示屏类
public class SmallScreen extends Observer{
 //构造函数
 public SmallScreen(String name,Counter counter){
  super(name,counter);
 }
 //更新显示屏
 public void update(){
  try{
   System.out.println(this.name + ":" + counter.getSubject());
  }
  catch(Exception err){
  }
 } 
}
//音箱类
public class Speaker extends Observer{
 //构造函数
 public Speaker(String name,Counter counter){
  super(name,counter);
 }
 //更新音箱呼叫
 public void update(){
  try{
   System.out.println(this.name + ":" + counter.getSubject());
  }
  catch(Exception err){
  }
 } 
}
//业务系统类
public class BankBiz {
 public static void main(String[] args) {
  Counter counter = new Counter("1号柜台");
  //1,2号小屏
  SmallScreen smallScreen1 = new SmallScreen("1号小屏",counter);
  SmallScreen smallScreen2 = new SmallScreen("2号小屏",counter);
  //3号音箱
  Speaker speaker = new Speaker("3号音箱",counter);
  //加入通知小屏
  counter.attach(smallScreen1);
  counter.attach(smallScreen2);
  counter.attach(speaker);
  //9号办理业务
  counter.setBizNo("9");
  //通知小屏更新
  counter.notifyChange();
 }
}
 

Php代码:

<?php
//银行柜台类
class Counter {
 //观察者列表
 private $arrSmallScreen = array();
 //当前业务号
 private $bizNo = "";
 //柜台名称
 private $name = "";
 //构造函数
 public function __construct($name){
  $this->name = $name;
 }
 //增加一个小屏
 public function attach(&$smallScreen){
  $this->arrSmallScreen[] = $smallScreen;
 }
 //去除一个小屏
 public function detach(&$smallScreen){
  for($i=0; $i<count($this->arrSmallScreen); $i++){
   $objTemp = $this->arrSmallScreen[$i];
   if($smallScreen === $objTemp){
    array_splice($this->arrSmallScreen,$i,1);
    break;
   }
  }
 }
 //通知小屏显示
 public function notifyChange(){
  foreach ($this->arrSmallScreen as $smallScreen){
   $smallScreen->update();
  }
 } 
 //获取当前业务号
 public function getBizNo(){
  return $this->bizNo;
 }
 //设置当前业务号
 public function setBizNo($bizNo){
  $this->bizNo = $bizNo;
 }
 //获取主题信息
 public function getSubject(){
  return  "请" + $this->bizNo . "号到" . $this->name . "号柜台办理业务";
 }
}
//通知设备基类
abstract class Observer {
 protected $name = "";
 protected $counter = "";
 //构造函数
 public function __construct($name,$counter){
  $this->name = $name;
  $this->counter = $counter;
 }
 //更新信息
 public abstract function update();
}
//小显示屏类
class SmallScreen extends Observer{
 //构造函数
 public function __construct($name,$counter){
  parent::__construct($name,$counter);
 }
 //更新显示屏
 public function update(){
  echo $this->name . ":" . $this->counter->getSubject();
 } 
}
//音箱类
class Speaker extends Observer{
 //构造函数
 public function __construct($name,$counter){
  parent::__construct($name,$counter);
 }
 //更新显示屏
 public function update(){
  echo $this->name . ":" . $this->counter->getSubject();
 } 
}
//业务系统类
class BankBiz {
 public static function execute() {
  $counter = new Counter("1号柜台");
  //1,2号小屏
  $smallScreen1 = new SmallScreen("1号小屏",$counter);
  $smallScreen2 = new SmallScreen("2号小屏",$counter);
  //3号音箱
  $speaker = new Speaker("3号音箱",$counter);
  //加入通知小屏
  $counter->attach($smallScreen1);
  $counter->attach($smallScreen2);
  $counter->attach($speaker);
  //9号办理业务
  $counter->setBizNo("9");
  //通知小屏更新
  $counter->notifyChange();
 }
}
BankBiz::execute();
?>
 

//运行结果如下

1号小屏:请9号到1号柜台号柜台办理业务

2号小屏:请9号到1号柜台号柜台办理业务

3号音箱:请9号到1号柜台号柜台办理业务

经过我们这么一番改造,应对观察者的加入是绝对没有问题了,再也不用去修改柜台类Counter了,把柜台类对具体终端的依赖关系给去除了。不管是小屏还是音箱,柜台类一视同仁,进行同样的处理,它根本不需要知道需要通知的对象是什么。比如:银行又提出来加入大屏的显示。这样的需求,对于我们来说已经是很easy的事情了,可以从Observer类再派成出来一个大屏类LargeScreen。有人问:“大屏和小屏还不一样,为什么不用一个类来表示呢”。其实,做过排队项目的人可能很清楚,大屏幕的显示驱动和显示方式可能与小屏完全不同,甚至于供货厂商都不一样,在这个案例中,我们只是剥离出来它的其中一项显示功能而已。又比如:银行为了提升服务质量,准备加入短信提醒用户的功能。现在,我们是不是很容易对付了?

•4 又有新需求提出来
我们在开发软件的时候,用户需求很难做到百分之百的稳定,只能做到尽量稳定。银行又提出了新的需求:“除了柜台上可以发布呼号信息以外,银行内部的管理部门在某些情况下,也能利用大小屏发布一些紧急的信息”。 我们按照处理观察者方法,可以把这些通知者抽象出来,形成一个基类Subject,银行柜台和管理部门作为两个通知者,都可以从基类Subject派生出来,以后再增加通知者,就能够以此类推。我们按照这个思路形成最终一个版本:

Java代码:

//主题基类
import java.util.Vector;
public abstract class Subject {
 //观察者列表
 private Vector<Observer> vectObserver = new Vector<Observer>();
 //增加一个观察者
 public void attach(Observer observer){
  vectObserver.add(observer);
 }
 //去除一个观察者
 public void detach(Observer observer){
  vectObserver.remove(observer);
 }
 //通知观察者更新
 public void notifyObservers(){
  for(int i=0; i<vectObserver.size(); i++){
   Observer observer = vectObserver.get(i);
   observer.update();
  }
 } 
 //获取主题信息
 public abstract String getSubject();
}
//银行柜台类
public class Counter extends Subject{
 //当前业务号
 private String bizNo;
 //柜台名称
 private String name;
 //构造函数
 public Counter(String name){
  this.name = name;
 }
 //获取当前业务号
 public String getBizNo(){
  return this.bizNo;
 }
 //设置当前业务号
 public void setBizNo(String bizNo){
  this.bizNo = bizNo;
 }
 //获取主题信息
 public String getSubject(){
  return "请" + this.bizNo + "号到" + this.name + "号柜台办理业务";
 }
}
//管理部门类
public class Manager extends Subject{
 //管理部门名称
 private String name;
 //构造函数
 public Manager(String name){
  this.name = name;
 }
 //获取主题信息
 public String getSubject(){
  return this.name + "发布最新紧急公告";
 }
}
//观察者基类
public abstract class Observer {
 protected String name;
 protected Subject subject;
 //构造函数
 public Observer(String name,Subject subject){
  this.name = name;
  this.subject = subject;
 } 
 //更新信息
 public abstract void update();
}
//小显示屏类
public class SmallScreen extends Observer{
 //构造函数
 public SmallScreen(String name,Subject subject){
  super(name,subject);
 } 
 //更新显示屏
 public void update(){
  try{
   System.out.println(this.name + ":" + subject.getSubject());
  }
  catch(Exception err){
  }
 }  
}
//音箱类
public class Speaker extends Observer{
 //构造函数
 public Speaker(String name,Subject subject){
  super(name,subject);
 } 
 //更新音箱
 public void update(){
  try{
   System.out.println(this.name + ":" + subject.getSubject());
  }
  catch(Exception err){
  }
 } 
}
//业务系统类
public class BankBiz {
 public static void main(String[] args) {
  //银行柜台
  Counter counter = new Counter("1号柜台");
  //1,2号小屏、3号音箱
  SmallScreen smallScreen1 = new SmallScreen("1号小屏",counter);
  SmallScreen smallScreen2 = new SmallScreen("2号小屏",counter);
  Speaker speaker = new Speaker("3号音箱",counter);
  //银行柜台加入观察者
  counter.attach(smallScreen1);
  counter.attach(smallScreen2);
  counter.attach(speaker);
  //9号办理业务
  counter.setBizNo("9");
  //通知更新
  counter.notifyObservers();
 
  //管理部门
  Manager manager = new Manager("风险控制部"); 
  //1号小屏
  smallScreen1 = new SmallScreen("1号小屏",manager);  
  //管理部门加入观察者
  manager.attach(smallScreen1);
  //通知更新
  manager.notifyObservers();
 }
}

 

php代码:

<?php
//主题基类
abstract class Subject {
 //观察者列表
 private $arrObserver = array();
 //增加一个观察者
 public function attach(&$observer){
  $this->arrObserver[] = $observer;
 }
 //去除一个观察者
 public function detach(&$observer){
  for($i=0; $i<count($this->$arrObserver); $i++){
   $objTemp = $this->$arrObserver[$i];
   if($observer === $objTemp){
    array_splice($this->$arrObserver,$i,1);
    break;
   }
  }
 }
 //通知观察者更新
 public function notifyObservers(){
  foreach ($this->arrObserver as $observer){
   $observer->update();
  }
 } 
 //获取主题信息
 public abstract function getSubject();
}
//银行柜台类
class Counter extends Subject {
 //当前业务号
 private $bizNo = "";
 //柜台名称
 private $name = "";
 //构造函数
 public function __construct($name){
  $this->name = $name;
 }
 //获取当前业务号
 public function getBizNo(){
  return $this->bizNo;
 }
 //设置当前业务号
 public function setBizNo($bizNo){
  $this->bizNo = $bizNo;
 }
 //获取主题信息
 public function getSubject(){
  return  "请" + $this->bizNo . "号到" . $this->name . "号柜台办理业务";
 }
}
//管理部门类
class Manager extends Subject{
 //管理部门名称
 private $name = "";
 //构造函数
 public function __construct($name){
  $this->name = $name;
 }
 //获取主题信息
 public function getSubject(){
  return $this->name . "发布最新紧急公告";
 }
}
//观察者基类
abstract class Observer {
 protected $name = "";
 protected $subject = "";
 //构造函数
 public function __construct($name,$subject){
  $this->name = $name;
  $this->subject = $subject;
 }
 //更新信息
 public abstract function update();
}
//小显示屏类
class SmallScreen extends Observer{
 //构造函数
 public function __construct($name,$subject ){
  parent::__construct($name,$subject);
 }
 //更新显示屏
 public function update(){
  echo $this->name . ":" . $this->subject->getSubject();
 } 
}
//音箱类
class Speaker extends Observer{
 //构造函数
 public function __construct($name,$subject){
  parent::__construct($name,$subject);
 }
 //更新音响
 public function update(){
  echo $this->name . ":" . $this->subject->getSubject();
 } 
}
//业务系统类
class BankBiz {
 public static function execute() {
  //银行柜台
  $counter = new Counter("1号柜台");
  //1,2号小屏、3号音箱
  $smallScreen1 = new SmallScreen("1号小屏",$counter);
  $smallScreen2 = new SmallScreen("2号小屏",$counter);
  $speaker = new Speaker("3号音箱",$counter);
  //银行柜台加入观察者
  $counter->attach($smallScreen1);
  $counter->attach($smallScreen2);
  $counter->attach($speaker);
  //9号办理业务
  $counter->setBizNo("9");
  //通知更新
  $counter->notifyObservers();
  
  //管理部门
  $manager = new Manager("风险控制部"); 
  //1号小屏
  $smallScreen1 = new SmallScreen("1号小屏",$manager);  
  //管理部门加入观察者
  $manager->attach($smallScreen1);
  //通知更新
  $manager->notifyObservers();  
 }
}
BankBiz::execute();
?>
 

又经过我们的一番改造,应对主题和观察者的需求变化都没有什么问题了,以后针对类似这样的需求,我们已经非常有经验了,完全可以把这段代码稍加修改套用一下就可以了。换句话说,我们把观察者模式的实现方式做成了一个很小的框架。