•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();
?>
又经过我们的一番改造,应对主题和观察者的需求变化都没有什么问题了,以后针对类似这样的需求,我们已经非常有经验了,完全可以把这段代码稍加修改套用一下就可以了。换句话说,我们把观察者模式的实现方式做成了一个很小的框架。
转载于:https://blog.51cto.com/liansq/478088