设计模式--观察者模式 Observer Pattern

本文主要是记录《Head First 设计模式》知识,目的是检查自己学到的知识,同时方便我以后进行复习和浏览。

一、概述

1-1 定义

观察者模式 Observer Pattern:Define a one-to-many dependency between object so that when one object changes state,all its dependents are notified and updated automatically.
定义对象间的一种一对多的依赖关系,每当一个对象的状态发生改变时,所有依赖于它的对象都可以得到通知并被自动更新。也称为发布-订阅(Publish/Subscribe)模式、模型-视图(Model-View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式

该模式是一种行为模式

1-2 模式结构

观察者模式包含如下角色:

  • Subject : 目标/主题。对象使用此接口注册为观察者,或者把自己从观察者中删除。
  • ConcreteSubject : 具体目标/主题。实现主题接口,除了注册和撤销方法,具体还实现了notityObserver()方法,此方法用于在状态改变时更新所有观察者。
  • Observer : 观察者。此接口只有一个update()方法,当主体状态改变时它被调用。
  • ConcreteObserver:具体观察者。观察者必须注册具体实体,以便接收更新。
图1 模式结构

1-3 模式动机

建立一种对象与对象之间的依赖,当一个对象发生改变时,会自动通知其依赖于该对象的所有对象,这些对象会作出相应反应。其中改变的对象称为主体,而被通知的对象称为观察者,一个观察目标(观察者接口)对应多个观察者,观察者之间没有互相联系,观察者本身可以根据需要决定是否注册和删除,使系统更易于扩展(具体观察者与具体目标/主题)。

二、举例:

该观察者模式存在两种情况:

  • 推模式 Push:当主题的状态发生改变时,主题会将消息以参数形式主动推送给观察者,不管观察者是否需要。
  • 拉模式 Pull:通知消息的方法本身并不带任何参数,是由观察者自己到主题对象那取回数据。

2-1 应用概述

气象监控应用:由WeatherData对象负责追踪目前的天气状况(温度、湿度、气压)。建立一种应用,有三种布告板,分别为目前的状态、气象统计及简单的预报。当WeatherData对象获取到最新值时,三种布告板必须实时更新。Weather-O-Rama希望公布一组API,好让其他开发人员可以写出自己的布告板,并插入此应用中。

图2

这是气象台送来的WeatherData源文件:


图3 WeatherData类的UML图

目前我们知道些什么:

  • WeatherData 类具有getting方法,可以取得三个测量值:温度、湿度、气压.
  • 当新的测量数据备妥时,measurementsChanged()方法就会被条用。(不在如何被条用,只在乎他被调用)
  • 需要实现三个使用天气数据的布告板:“目前状况”、“气象统计”、“天气预报”布告。一旦WeatherData有新的测量,这些布告必须马上更新。
  • 此系统可扩展,用户可以随心所欲地添加或删除任何布告板。目前初始的布告板有三类:“目前状态”布告、“气象统计”布告、“天气预报”布告

2-2 推模式

图4 观察者推模式UML

推模式代码
主题代码:

/// <summary>
/// 主题接口
/// </summary>
public interface ISubject
{
    /// <summary>
    /// 注册观察者
    /// </summary>
    /// <param name="O"></param>
    public void RegisterObserver(IObserver O);
    /// <summary>
    /// 删除观察者
    /// </summary>
    /// <param name="O"></param>
    public void RemoveObserver(IObserver O);
    /// <summary>
    /// 主题改变时,通知观察者
    /// </summary>
    public void NotifyObserver();
}

具体主题代码:

public class WeatherData : ISubject
{
    private List<IObserver> observers;
    private float Temperature;
    private float Humidity;
    private float Pressure;

    /// <summary>
    /// WeatherData数据改变
    /// </summary>
    /// <param name="temperature"></param>
    /// <param name="humidity"></param>
    /// <param name="pressure"></param>
    public void setMeasurements(float temperature, float humidity, float pressure)
    {
        this.Temperature = temperature;
        this.Humidity = humidity;
        this.Pressure = pressure;
        measurementsChanged();
    }
    public void measurementsChanged()
    {
        NotifyObserver();
    }
    public WeatherData()
    {
        observers = new List<IObserver>();
    }
    /// <summary>
    /// 通知所有绑定的观察者
    /// </summary>
    public void NotifyObserver()
    {
        foreach (var item in observers)
        {
            item.Update(Temperature, Humidity, Pressure);
        }
    }
    /// <summary>
    /// 注册观察者
    /// </summary>
    /// <param name="O"></param>
    public void RegisterObserver(IObserver O)
    {
        observers.Add(O);
        if (observers.Contains(O))
        {
            Console.WriteLine("注册观察者成功");
        }
        else
        {
            Console.WriteLine("注册观察者失败");
        }
    }
    /// <summary>
    /// 移除观察者
    /// </summary>
    /// <param name="O"></param>
    public void RemoveObserver(IObserver O)
    {

        observers.Remove(O);
        if (observers.Contains(O))
        {
            Console.WriteLine("移除观察者失败");
        }
        else
        {
            Console.WriteLine("移除观察者成功");
        }
    }
    /// <summary>
    /// 获取温度
    /// </summary>
    /// <returns></returns>
    public float getTmperature()
    {
        return Temperature;
    }
    /// <summary>
    /// 获取湿度
    /// </summary>
    /// <returns></returns>
    public float getHumidity()
    {
        return Humidity;
    }
    /// <summary>
    /// 获取气压
    /// </summary>
    /// <returns></returns>
    public float getPressure()
    {
        return Pressure;
    }
}

布告板有共同的方法Display,所以提出来布告板相同的部分

/// <summary>
/// 布告板接口
/// </summary>
interface IDisplayElement
{
    public void Display();
}

观察者代码:

/// <summary>
/// 观测者接口
/// </summary>
public interface IObserver
{
    /// <summary>
    /// 改变接口
    /// </summary>
    /// <param name="temp"></param>
    /// <param name="humidity"></param>
    /// <param name="pressure"></param>
    public void Update(float temp,float humidity,float pressure);
}

具体观察者代码:

/// <summary>
/// 目前状况布告板
/// </summary>
public class CurrentConditionsDisplay : IObserver, IDisplayElement
{
    private float Temperature;
    private float Humidity;
    private float Pressure;
    private ISubject WeartherData;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="weatherData"></param>
    public CurrentConditionsDisplay(ISubject weatherData)
    {
        this.WeartherData = weatherData;
        weatherData.RegisterObserver(this);
    }
    /// <summary>
    /// 展示
    /// </summary>
    public void Display()
    {
        Console.WriteLine("Current conditions:{0}F 、{1}%humidity and {2}pressure", Temperature, Humidity, Pressure);
    }
    /// <summary>
    /// 改变
    /// </summary>
    /// <param name="temp"></param>
    /// <param name="humidity"></param>
    /// <param name="pressure"></param>
    public void Update(float temp, float humidity, float pressure)
    {
        this.Temperature = temp;
        this.Humidity = humidity;
        this.Pressure = pressure;
        Display();
    }
}

测试程序代码:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("推模式");
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay current = new CurrentConditionsDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(85, 55, 33.4f);
        weatherData.setMeasurements(75, 43, 27.4f);
    }
}

运行结果:

图5 运行结果

2-3拉模式

图6 拉模式UML

主题如何送出通知
① 先调用setChanged()方法,标记状态已经改变的事实
② 然后调用两种NotifyObservers()方法中的一个:NotifyObservers()或NotifyObservers(object arg)

观察者如何接受通知
同以前一样,观察者实现更新的方法,但签名不太一样:Update(ISubject sub,object arg)

拉模式代码
拉模式与推模式有相同代码,所以只贴出来不同部分的代码。
具体主题代码:

class WeatherData : ISubject
{
    private List<IObserver> observers;
    private float Temperature;
    private float Humidity;
    private float Pressure;
    private bool changed = false;

    /// <summary>
    /// 构造函数
    /// </summary>
    public WeatherData()
    {
        observers = new List<IObserver>();
    }
    /// <summary>
    /// 主题状态改变,调用该方法
    /// </summary>
    /// <param name="temperature"></param>
    /// <param name="humidity"></param>
    /// <param name="pressure"></param>
    public void setMeasurements(float temperature, float humidity, float pressure)
    {
        this.Temperature = temperature;
        this.Humidity = humidity;
        this.Pressure = pressure;
        measurementsChanged();
    }
    public void measurementsChanged()
    {
        SetChanged();
        NotifyObserver();
    }

    /// <summary>
    /// 查看是否修改状态
    /// </summary>
    public void SetChanged()
    {
        changed = true;
    }
    /// <summary>
    /// 通知所有绑定的观察者
    /// </summary>
    /// <param name="arg">传入NotityObserver()的数据对象,如果没有说明则为空</param>
    public void NotifyObserver(object arg)
    {
        if (changed)
        {
            foreach (var item in observers)
            {
                item.Update(this, arg);
            }
            changed = false;
        }
    }
    /// <summary>
    /// 通知所有绑定的观察者
    /// </summary>
    public void NotifyObserver()
    {
        NotifyObserver(null);
    }
    /// <summary>
    /// 注册观察者
    /// </summary>
    /// <param name="O"></param>
    public void RegisterObserver(IObserver O)
    {
        observers.Add(O);
        if (observers.Contains(O))
        {
            Console.WriteLine("注册观察者成功");
        }
        else
        {
            Console.WriteLine("注册观察者失败");
        }
    }
    /// <summary>
    /// 移除观察者
    /// </summary>
    /// <param name="O"></param>
    public void RemoveObserver(IObserver O)
    {
        observers.Remove(O);
        if (observers.Contains(O))
        {
            Console.WriteLine("移除观察者失败");
        }
        else
        {
            Console.WriteLine("移除观察者成功");
        }
    }
    /// <summary>
    /// 获取温度
    /// </summary>
    /// <returns></returns>
    public float getTmperature()
    {
        return Temperature;
    }
    /// <summary>
    /// 获取湿度
    /// </summary>
    /// <returns></returns>
    public float getHumidity()
    {
        return Humidity;
    }
    /// <summary>
    /// 获取气压
    /// </summary>
    /// <returns></returns>
    public float getPressure()
    {
        return Pressure;
    }
}

观察者代码:

/// <summary>
/// 拉模式的观察者 接口
/// </summary>
public interface IObserver
{
    /// <summary>
    /// 改变接口
    /// </summary>
    public void Update(ISubject sub, Object arg);
}

具体观察者代码:

/// <summary>
/// 目前状况布告板
/// </summary>
class CurrentConditionsDisplay : IObserver, IDisplayElement
{
    ISubject Subject;
    private float Temperature;
    private float Humidity;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="sub">主题</param>
    public CurrentConditionsDisplay(ISubject sub)
    {
        this.Subject = sub;
        Subject.RegisterObserver(this);
    }
    public void Display()
    {
        Console.WriteLine("Current conditions:{0}F 、{1}%humidity ", Temperature, Humidity);
    }
    /// <summary>
    /// 修改部分
    /// </summary>
    /// <param name="sub">主题</param>
    /// <param name="arg"></param>
    public void Update(ISubject sub, object arg)
    {
        if (sub is WeatherData)
        {
            WeatherData weather = (WeatherData)sub;
            this.Temperature = weather.getTmperature();
            this.Humidity = weather.getHumidity();
        }
        Display();
    }
}

测试程序代码:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("推模式");
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay current = new CurrentConditionsDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(85, 55, 33.4f);
        weatherData.setMeasurements(75, 43, 27.4f);
    }
}

运行结果:

图7 运行结果

三 总结

3-1 模式优缺点

观察者模式优点

  • 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。
  • 观察者模式在观察目标与观察者(Oserver)建立一个抽象耦合。
  • 观察者模式支持广播通信
  • 观察者模式符合"开-闭原则"

观察者模式缺点

  • 如果一个观察目标有很多直接和简介观察者,通知所有的观察者会花费很多的时间。
  • 如果观察目标和观察者之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  • 观察者模式没有相应的机制让观察者知道所观察的目标是怎么发生变化,仅仅知道观察目标发生了变化。

观察者模式推优点是实时、高效。缺点是:观察者会收到主题传送的内部状态;当观察者种类比较多,主题维护观察者比较麻烦;当观察者只需要一点数据是,会被迫收到一堆数据。

观察者模式拉优点:如果观察者众多,会将订阅关系放在Observer;观察者自行决定获取数据;当扩展功能时(增加更多的状态),只需改变自己的getting方法。缺点是:主题会暴露我们不想暴露的内部成员

3-2 适合场景

以下情况可以使用观察者模式:

  • 一个抽象模型有两个方面,其中一个方面依赖于另一方面。将这些方面封装在独立的对象中使它们可以独立地改变和复用。
  • 一个对象的改变将导致一个或多个对象进行改变,不知道具体多少对象将发生改变,可以降低系统的耦合度。
  • 一个对象必须通知其他对象,而并不知道这些对象是谁。
  • 需要在系统中创建一个触发链,A对象改变通知B对象,B对象的行为影响C对象......,可以使用观察者模式创建一个触发链。

3-3 模式应用

观察者模式使用非常广泛,凡是涉及一对一或者一对多的对象都可以使用观察者模式,例子:某购物网站执行发送后将打折信息发送给用户,某手机软件有活动点击发送后通知所有用户。

总结

  • 观察者模式定义了对象之间一对多的关系
  • 主题(也就是可观察者)用一个共同的接口来更新观察者
  • 观察者与可观察者之间用松耦合方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口。
  • 使用此模式,你可以使用被观察者处推(Push)或拉(Pull)数据(然而,推的方式认为更正确)。
  • 有多个观察者时,不可以依赖特定的通知次序。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容