[Head First设计模式]山西面馆中的设计模式——观察者模式
系列文章
[Head First设计模式]山西面馆中的设计模式——装饰者模式
引言
不知不自觉又将设计模式融入生活了,吃个饭也不得安生,也发现生活中的很多场景,都可以用设计模式来模拟。原来设计模式就在我身边。
为什么观察者模式会出现呢?
为了建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。在此,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展,这就是为什么需要观察者模式。
观察者模式定义
观察者模式(Observer Pattern):定义了对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又被称为发布-订阅(Publish/Subscribe)模式,模型-视图(Model/View)模式,源-监听器(Source/Listener)模式,或从属者(Dependents)模式。观察者模式是一种对象行为型的模式。
观察者模式UML
当两个对象是松耦合的,它们之间能够交互,但是相互了解的很少。
观察者模式提供了主题和观察者之间的松耦合设计。因为主题只知道观察者实现了某个接口(即Observer接口)。主题不需要知道具体观察者是谁,做了些什么或其它任何细节。要增加新的观察者或删除观察者,主题不会受到任何影响,不必修改主题代码。
可以独立地复用主题和观察者,他们之间互不影响,即是松耦合。
设计原则:在交互的对象之间争取松耦合设计
由于松耦合设计使得对象间的依赖最小化,所以,我们能够创建柔性的oo系统,应对变化的情况,因为对象间的依赖降到了最低。
实例分析
书中的气象站例子:
代码实现:
1 public interface Subject 2 { 3 void RegisterObserver(Observer o);//这两个方法都需要一个观察者作为参数,该观察者是用来注册或被删除的。 4 void RemoveObserver(Observer o); 5 void NotifyObservers();//当主题改变状态时,这个方法会被调用,以通知所有的观察者 6 }
1 public interface Observer 2 { 3 /// <summary> 4 /// 当气象观测值改变时,主题会把这些状态值作为方法的参数传给观察者 5 /// </summary> 6 /// <param name="temp"></param> 7 /// <param name="humidity"></param> 8 /// <param name="pressure"></param> 9 void Update(float temp,float humidity,float pressure); 10 }
1 /// <summary> 2 /// 该接口之包含一个方法,也就是display方法,当布告板需要显示时,调用次方法。 3 /// </summary> 4 public interface DisplayElement 5 { 6 void Display(); 7 }
在WeatherData中实现主题接口
1 /// <summary> 2 /// WeatherData实现了subject接口 3 /// </summary> 4 public class WeatherData : Subject 5 { 6 /// <summary> 7 /// 我们加上一个ArrayList来记录观察者,此ArrayList是在构造器中建立的 8 /// </summary> 9 private ArrayList observers; 10 private float temperature; 11 private float humidity; 12 private float pressure; 13 public WeatherData() 14 { 15 observers = new ArrayList(); 16 } 17 public void RegisterObserver(Observer o) 18 { 19 //当注册观察者的时候,秩序把他们加在ArrayList后面就行了 20 observers.Add(o); 21 } 22 23 public void RemoveObserver(Observer o) 24 { 25 //同样,当观察者想取消注册,只需要移除 26 int i = observers.IndexOf(o); 27 if (i > 0) 28 { 29 observers.Remove(i); 30 } 31 } 32 /// <summary> 33 /// 有趣的地方来了,在这里,我们把状态告诉每一个观察者, 34 /// 因为观察者都实现了Update方法,所以我们知道如何通知他们 35 /// </summary> 36 public void NotifyObservers() 37 { 38 for (int i = 0; i < observers.Count; i++) 39 { 40 Observer observer = (Observer)observers[i]; 41 observer.Update(temperature, humidity, pressure); 42 } 43 } 44 /// <summary> 45 /// 当气象站得到更新观测值的时,我们通知观察者。 46 /// </summary> 47 public void MeasurementChanged() { 48 NotifyObservers(); 49 } 50 public void SetMeasurements(float temperature, float humidity, float pressure) 51 { 52 this.temperature = temperature; 53 this.humidity = humidity; 54 this.pressure = pressure; 55 } 56 }
建立布告板
1 /// <summary> 2 /// 此布告板实现了Observer接口,所以可以从weatherdata对象中获得改变, 3 /// 同时也实现了DisplayElement接口,因为我们的API规定所有的布告板必须实现此接口 4 /// </summary> 5 public class CurrentConditionsDisplay:Observer,DisplayElement 6 { 7 private float temperature; 8 private float humidity; 9 10 private Subject weatherData; 11 /// <summary> 12 /// 构造器需要weatherdata对象作为注册用 13 /// </summary> 14 /// <param name="weatherData"></param> 15 public CurrentConditionsDisplay(Subject weatherData) 16 { 17 this.weatherData = weatherData; 18 weatherData.RegisterObserver(this); 19 } 20 public void Update(float temp, float humidity, float pressure) 21 { 22 //当update被调用的时候,我们把温度和湿度保存起来,然后调用Display(); 23 this.temperature = temp; 24 this.humidity = humidity; 25 Display(); 26 27 28 } 29 /// <summary> 30 /// 只是将最近的湿度和温度显示出来。 31 /// </summary> 32 public void Display() 33 { 34 Console.WriteLine("Current conditions:"+temperature+" F degrees and"+humidity+"% humidity"); 35 } 36 }
测试代码
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //首先创建weatherData对象 6 WeatherData weatherData = new WeatherData(); 7 CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); 8 weatherData.SetMeasurements(80, 65, 30.4f); 9 //通知 10 weatherData.NotifyObservers(); 11 weatherData.SetMeasurements(82, 70, 29.2f); 12 weatherData.NotifyObservers(); 13 weatherData.SetMeasurements(78, 90, 29.2f); 14 weatherData.NotifyObservers(); 15 Console.Read(); 16 } 17 }
结果:
实例分析二
场景:山西面馆中,我点餐,服务员传话给厨师,厨师应答,厨师做饭。
分析:在这个场景中,我:被观察者,服务员,厨师:观察者。
我:“一份西红柿鸡蛋汤面”
一系列动作
服务员:1.向厨师传话“一份西红柿鸡蛋汤面”
厨师:1.收到,2.开始做。
在.NET中,C#使用委托以及事件,可以很好的实现观察者模式。委托相当于“订阅清单”的角色,当目标中关联了该委托的事件被触发时,则委托将自动按序执行观察者注册于委托中的方法。
代码实现:
基类的实现
1 /// <summary> 2 /// 自定义事件参数 3 /// </summary> 4 public class EatSomthingEventArgs : EventArgs 5 { 6 private string foodName; 7 public EatSomthingEventArgs(string foodName) 8 { 9 this.foodName = foodName; 10 } 11 public string FoodName 12 { 13 get { return foodName; } 14 set { foodName = value; } 15 }
1 /// <summary> 2 /// 声明一个委托,用于代理一系列自定义方法 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 public delegate void EatSomethingEventHandler(object sender, EatSomthingEventArgs e);
1 /// <summary> 2 /// 在Observer Pattern(观察者模式)中,此类作为所有Subject(主题)的抽象基类 3 /// 此抽象类无抽象方法,主要是为了不能实例化该类对象,确保模式完整性. 4 /// 具体的主题(比如:customer)就继承自该类,也可以说是被观察者的基类。 5 /// </summary> 6 public abstract class Subject 7 { 8 public event EatSomethingEventHandler EatSomethingHandler; 9 public string FoodName { set; get; } 10 /// <summary> 11 /// 封装了触发事件的方法 12 /// 主要为了规范化及安全性,除观察者基类外,其派生类不直接触发委托事件 13 /// </summary> 14 protected void Notify() 15 { 16 if (EatSomethingHandler != null) 17 { 18 EatSomethingHandler(this, new EatSomthingEventArgs(this.FoodName)); 19 } 20 } 21 }
1 /// <summary> 2 /// 此类作为所有Observer(观察者)的抽象基类 3 /// 此类作为观察者基类,用于规划所有观察者(即订阅方)订阅行为 4 /// 具体实施过程: 5 /// 1.指定观察者所观察的对象(即发布方).(通过构造器传递) 6 /// 2.规划观察者自身需要作出响应方法列表 7 /// 3.注册需要委托执行的方法.(通过构造器实现) 8 /// </summary> 9 public abstract class Observer 10 { 11 /// <summary> 12 /// 构造时通过传入具体主题subject,把观察者与模型关联,并完成订阅. 13 /// </summary> 14 /// <param name="subject"></param> 15 public Observer(Subject subject) 16 { 17 subject.EatSomethingHandler += new EatSomethingEventHandler(Response); 18 } 19 /// <summary> 20 /// 规划了观察者的一种行为(方法),所有派生于该观察者基类的具体观察者都 21 /// 通过覆盖该方法来实现作出响应的行为. 22 /// </summary> 23 /// <param name="sender"></param> 24 /// <param name="e"></param> 25 public abstract void Response(object sender, EatSomthingEventArgs e); 26 }
1 /// <summary> 2 /// 另一个观察者基类.该观察者类型拥有两个响应行为 3 /// </summary> 4 public abstract class Observer2 5 {/// <summary> 6 /// 构造时通过传入具体主题subject,把观察者与模型关联,并完成订阅. 7 /// </summary> 8 /// <param name="subject"></param> 9 public Observer2(Subject subject) 10 { 11 subject.EatSomethingHandler += new EatSomethingEventHandler(Response); 12 subject.EatSomethingHandler += new EatSomethingEventHandler(Response2); 13 } 14 /// <summary> 15 /// 规划了观察者的一种行为(方法),所有派生于该观察者基类的具体观察者都 16 /// 通过覆盖该方法来实现作出响应的行为. 17 /// </summary> 18 /// <param name="sender"></param> 19 /// <param name="e"></param> 20 public abstract void Response(object sender, EatSomthingEventArgs e); 21 public abstract void Response2(object sender, EatSomthingEventArgs e); 22 }
观察者实现
1 /// <summary> 2 /// 厨师类 继承自抽象观察者基类Observer2 两个反应 3 /// </summary> 4 public class Cook : Observer2 5 { 6 public Cook(Subject subject) 7 : base(subject) 8 { } 9 public override void Response(object sender, EatSomthingEventArgs e) 10 { 11 Console.WriteLine("厨师:收到了,要做一份{0}", e.FoodName); 12 } 13 14 public override void Response2(object sender, EatSomthingEventArgs e) 15 { 16 Console.WriteLine("厨师:{0}已经在做了,稍等.....", e.FoodName); 17 } 18 }
1 /// <summary> 2 /// 具体的观察者 服务员 继承自观察者基类 3 /// </summary> 4 public class Waiter : Observer 5 { 6 public Waiter(Subject subject) 7 : base(subject) 8 { } 9 public override void Response(object sender, EatSomthingEventArgs e) 10 { 11 Console.WriteLine("美女服务员:康师傅做一份{0}", e.FoodName); 12 } 13 }
被观察者
1 /// <summary> 2 /// 具体的主题类 即被观察者 3 /// </summary> 4 public class Customer : Subject 5 { 6 public Customer(string foodName) 7 { 8 base.FoodName = foodName; 9 } 10 /// <summary> 11 /// 订餐方法 此方法将触发观察者的一系列动作 12 /// </summary> 13 public void OrderMeal() 14 { 15 Console.WriteLine("顾客:我要一份{0}", base.FoodName); 16 base.Notify(); 17 } 18 }
测试:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Customer customer = new Customer("西红柿鸡蛋汤面"); 6 Waiter waiter = new Waiter(customer); 7 Cook cook = new Cook(customer); 8 //订餐 动作 9 customer.OrderMeal(); 10 Console.Read(); 11 } 12 }
结果:
观察者模式适用场景
当一个对象的数据更新时需要通知其他对象,但这个对象又不希望和被通知的那些对象形成紧耦合。
当一个对象的数据更新时,这个对象需要让其他对象也各自更新自己的数据,但这个对象不知道具体有多少对象需要更新数据
观察者模式与其他模式的关系
观察者模式与备忘录模式的关系
观察者模式使用了备忘录模式(Memento Pattern),暂时将观察者对象存储在被观察者对象里面
观察者模式与MVC模式的关系
观察者模式可以用来实现MVC模式。观察者模式中的主题便是MVC模式中的模型加控制器,而观察者便是视图
一般情况下,MVC是观察者模式、组合模式、策略模式等设计模式的组合。
总结
观察者模式的有点
1,具体主题和具体观察者是松耦合关系。
由于主题(Subject)接口仅仅依赖于观察者(Observer)接口,因此具体主题只是知道它的观察者是实现观察者(Observer)接口的某个类的实例,但不需要知道具体是哪个类。同样,由于观察者仅仅依赖于主题(Subject)接口,因此具体观察者只是知道它依赖的主题是实现主题(subject)接口的某个类的实例,但不需要知道具体是哪个类。
2,观察者模式满足“开-闭原则”。
主题(Subject)接口仅仅依赖于观察者(Observer)接口,这样,我们就可以让创建具体主题的类也仅仅是依赖于观察者(Observer)接口,因此如果增加新的实现观察者(Observer)接口的类,不必修改创建具体主题的类的代码。同样,创建具体观察者的类仅仅依赖于主题(Observer)接口,如果增加新的实现主题(Subject)接口的类,也不必修改创建具体观察者类的代码。
观察者模式的缺点
- 如果一个被观察者对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
- 如果在被观察者之间有循环依赖的话,给观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式时要特别注意这一点。
- 虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有相应的机制是观察者知道所观察的对象是怎么发生变化的
参考书
Head First 设计模式