观察者模式是为了实现类与类之间的松耦合,具体作用都是当一个事件发生时,调用所有响应方法。
感觉观察者模式在c#、c++、java语言中的适用情况与意义是不一样的。
一、在c#中:
当一个事情发生了,多个观察者将出现反应。如人物被攻击这件事情发生了,则人物要播放被攻击动画,边上要显示攻击值,被攻击的人物要减少血量等等。对于这样的情况,我们一般的做法是,当某件事情发生时,则生成这些类的对象,逐个调用这些对象中相应的方法,但这些方法需要接收的参数必须是一样的:
class Model{
void animation(int blood) {
//具体实现
}
}
class TextShow{
void textShow(int blood) {
//具体实现
}
}
class DateBase{
void DeductBlood(int blood) {
//具体实现
}
}
public class Main : MonoBehaviour {
ShowText showText = new ShowText();
Model model = new Model();
Data data = new Data();
public void onClickButton()
{
int blood = 100;
showText.showText(blood);
model.Anim(blood);
data.DeductBlood(blood);
}
}
这样做的不足之处在于类与类之间是紧耦合的,而且一个函数是否需要被调用,由管理该函数的类确定,而不是由管理Event事件的类确定,这就像是订报纸,订不订由用户决定,不由报刊决定,报刊只负责出报纸。
那么我们需要将这些生成对象调用函数抽象成观察者的Action,当Event发生时,就调用Action(要求Action中所有函数的参数应该是一样的)。Event不需要知道Action中具体有哪些函数。
C#语言中,可以使用System.Action与event来实现:
Event维护一个值(报刊)与方法委托链(订阅的用户),当值发生改变时(有新报刊发布),调用委托链中的所有方法:
public class Event
{
//System.Action<int>表示调用的函数参数为Int型,Actions就是一个委托链。
public static event System.Action<int> Actions;
//设定一个值,当更改其值时,会调用set函数,当调用该函数时,则调用Actions中的所有函数。
//而这个值应该是所有函数的实参。
private int blood;
public int Blood
{
get { return blood; }
set
{
if (0 == value) return;//如果减少的血为0时,则不需要调用函数
blood = value;
Debug.Log("减少的血为" + blood);
if (Actions != null) Actions(blood);//event不知道Actions中有哪些方法
}
}
}
Observers在之前就确定当事件发生时,自己的哪些方法需要被调用,这里有三个观察者:
public class Model {
public Model()
{
Event.Actions += Anim;
}
void Anim(int blood)
{
Debug.Log("被攻击动画" + blood);
}
}
public class Data {
public Data() {
Event.Actions += DeductBlood;
}
void DeductBlood(int blood) {
Debug.Log("生命值降低"+blood);
}
}
public class ShowText {
public ShowText()
{
Event.Actions += showText;
}
void showText(int blood)
{
Debug.Log("显示数值效果" + blood);
}
}
用一个测试类,来初始化所有观察者与主题,并通过交互事件来更改主题维护的值:
public class Main : MonoBehaviour {
// Use this for initialization
Event _event = new Event();
ShowText showText = new ShowText();
Model model = new Model();
Data data = new Data();
public void onClickButton()
{
//通过Blood来更改blood
_event.Blood = 100;
}
}
这时会发现所有观察者调用了自己的函数:
C#因为有委托的概念(System.Action是以委托链实现的),所以任何一对多的模式下都可以应用观察者模式。
二、在java与C++中,没有委托的概念。
在C++中之所以使用观察者模式,是为了抽象观察者的响应事件类型。比如下载一个文件的同时需要显示进度条,一般的做法是(这里用java替代C++写一下):
package pers.soultree.Observer;
public class ObserverPattern {
public static void main(String[] args) {
OperateFiles operateFiles=new OperateFiles();
operateFiles.downloadFiles();
}
}
class OperateFiles {
ProgressBar progressBar =new ProgressBar();
public void downloadFiles() {
//根据路径寻找到文件夹,获得文件个数,比如说文件个数是10,然后逐个复制
int size=10;
for(int i=0; i<size;i++) {
float progressValue=(float)(i+1)/size;
progressBar.update(progressValue);
}
}
}
class ProgressBar {
public void update(float progressValue) {
System.out.println("更新进度条"+progressValue);
}
}
这样的话违反了依赖倒置原则(依赖:一般指编译时依赖,而不是运行时依赖,A依赖B表示B存在A才能存在。依赖倒置原则:一般要对接口编程,而不要对实现编程)
因为progressBar是一个实现细节,一般不依赖于实现细节,因为依赖细节是很容易变化的,它将来可能会变成一个数值显示,或是"......"等,所以这里应该将它抽象成一个功能类接口:
interface IProgressDisplay{
public void update(float progressValue) ;
}
不同的实现细节类型是它的子类:
class ProgressBar implements IProgressDisplay{
public void update(float progressValue) {
System.out.println("更新进度条"+progressValue);
}
}
class ProgressText implements IProgressDisplay{
public void update(float progressValue) {
System.out.println("更新文字"+progressValue);
}
}
在操作文件的类中维护一个进度类的链,需要更新的时候,则调用该链中所有类的Update函数:
List<IProgressDisplay> progressDisplays=new LinkedList<>();
public void downloadFiles() {
//根据路径寻找到文件夹,获得文件个数,比如说文件个数是10,然后逐个复制
int size=10;
for(int i=0; i<size;i++) {
float progressValue=(float)(i+1)/size;
for (IProgressDisplay iProgressDisplay : progressDisplays) {
iProgressDisplay.update(progressValue);
}
}
}
因为这个链由该类维护,所以还需要写两个方法,来增加与删除子项:
public void addProgress(IProgressDisplay progressDisplay) {
progressDisplays.add(progressDisplay);
}
public void removeProgress(IProgressDisplay progressDisplay) {
progressDisplays.remove(progressDisplay);
}
最后在测试类中添加需要更新的UI控件:
public class ObserverPattern {
public static void main(String[] args) {
OperateFiles operateFiles=new OperateFiles();
ProgressBar progressBar=new ProgressBar();
ProgressText progressText=new ProgressText();
operateFiles.addProgress(progressBar);
operateFiles.addProgress(progressText);
operateFiles.downloadFiles();
}
}
这样就是面向接口编程,没有违反依赖倒置原则。但是要注意,这里和C#中一个重要的不同是,观察者都实现了同一个功能抽象的接口,而且响应的函数都是重写接口的函数。也就是观察者做出反应的方法实现的功能是同一类的,方法名也是一样的。
三、而在java中,它内置了观察者模式,强调一对多的关系,没有强调功能一致。不需要自己写add,remove函数,而且不需要通过List实现:
事件发布者继承Observable类,这个类中会通知所有观察者调用update函数,而且要提供一个getInfo函数,供观察者获取变化的信息:
class OperateFiles extends Observable{
float progressValue;
public void downloadFiles() {
//根据路径寻找到文件夹,获得文件个数,比如说文件个数是10,然后逐个复制
int size=10;
for(int i=0; i<size;i++) {
progressValue =(float)(i+1)/size;
setChanged();//标记此类已经发生了改变
notifyObservers();//如果此类发生了改变,则通知所有观察者
}
}
public float getInfo() {
return progressValue;
}
}
所有观察者需要实现Observer接口,需要在构造函数中指定事件(被观察者),而且需要实现接口中的Update方法,并在这个方法中获得被观察者中变化的值(没错,所有观察者只能调用重写父类的update函数,而且参数也是确定的,一般用不到,获取变化的值需要通过之前指定的事件中的方法获取),也就是说,观察者中有一个被观察者,当被观察者更改值时,会调用观察者的update方法,观察者在此方法中通过被观察者的函数获得更改值:
class ProgressBar implements Observer{
private Observable ob;
public ProgressBar(Observable o) {
this.ob=o;
ob.addObserver(this);//为事件添加自己这个监听者
}
@Override
public void update(Observable o, Object arg) {
// TODO Auto-generated method stub
System.out.println("更新进度条"+((OperateFiles) ob).getInfo());
}
}
测试类:
public class ObserverPattern {
public static void main(String[] args) {
OperateFiles operateFiles=new OperateFiles();
ProgressBar progressBar=new ProgressBar(operateFiles);
ProgressText progressText=new ProgressText(operateFiles);
operateFiles.downloadFiles();
}
}
这个内置的观察者模式,其实也不需要观察者实现的功能一致,因为都是调用update函数,在这个函数中可以实现不同的功能,而且各个观察者持有一个被观察者,可以通过函数获取被观察者的其他值,感觉更灵活了。