设计模式:Observer观察者模式剖析

看这篇文章的读者,一般是学习设计模式的新手或初学者,老手一般是不需要看的,因为他已经掌握了这个知识。那么如何快速的学习观察者设计模式?对这个模式个有个大概的了解,知道它的原理是什么?怎么去使用它?我想从下面几个方面来进行阐述。

  1. 明白什么是观察者设计模式(Observer)?
  2. 为什么要学习它?
  3. 怎么样去使用它?
  4. 观察者设计模式的优缺点是什么?
  5. 在使用观察者设计模式的时候应该要注意注意什么?这点只有在经过了大量的工程实践后,才能真正的明白,达到灵活运用。作为初学者只需要了解大概就可以了,等自己会使用它了,再来仔细的品读它。
  6. 观察者设计模式与其它的设计模式之间的关系?设计模式中大多数的模式,并不是单一存在的,彼此之间有一定的关联。

1. 什么是Observer?

  • 定义对象之间的一种一对多的依赖关系,以便每当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。观察者模式也叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。

  • 在观察者模式中,发生改变的对象称为观察目标(Subject),而被通知的对象称为观察者(Observer),一个 Subject 可以对应无数个 Observer,而且这些 Observer 相互之间可以没有的任何联系,一旦 Subject 的状态发生改变 , 所有的Observer 都得到通知。作为对这个通知的响应,每个观察者都将查询观察目标,使其状态与目标的状态同步。

    使用一个图来简单说明
    在这里插入图片描述

2. 为什么要用observer?

  • 一般的设计是将一个系统分割成一系列相互协作的类,这样做有一个常见的副作用:需要维护相关对象间的一致性。但是我们不希望为了维持这种一致性而使各个类紧密耦合,这样做降低了它们的可重用性。

  • 可以根据需要增加和删除观察者,使得系统更易于扩展。

3. 怎样去使用observer?

  • 当一个抽象模型有两个方面 , 其中一个方面依赖于另一方面。将这二者封装在独立的对象中,使它们可以各自独立地改变和复用。

  • 当对一个对象的改变需要同时改变其它的对象, 但是不知道具体有多少对象要被改变。

  • 当一个对象必须通知其它对象时,而它又不能假定其它对象是谁。换言之 , 你不希望这些对象之间是紧密耦合的。

  • UML图
    在这里插入图片描述

  • Subject(观察目标)

    • 观察目标知道它的观察者,可以有任意多个观察者观察同一个目标。
    • 提供注册和删除观察者对象的接口。
  • Observe(观察者)

    • 为那些在观察目标发生改变时需获得通知的对象定义一个更新接口。
  • ConcreteSubject(具体的观察目标)

    • 将有关状态存入各个 ConcreteObserver 对象。
    • 当它的状态发生改变时, 向它的各个观察者发出通知。
  • ConcreteObserver(具体的观察者)

    • 维护一个指向 ConcreteSubject 对象的引用。
    • 存储有关状态,这些状态应与观察目标的状态保持一致。
    • 实现 Observer 更新接口以使自身状态与观察目标的状态保持一致。
  • 执行步骤

    • 当 ConcreteSubject 发生任何可能导致其观察者与其本身状态不一致的改变时,它将通知它的各个观察者。
    • 在得到一个具体观察目标的改变通知后, ConcreteObserver 对象可向观察目标对象查询信息。ConcreteObserver 使用这些信息让它的状态与观察目标对象的状态一致。
      在这里插入图片描述

    注意发出改变请求的 Observer 对象并不立即更新 ,而是将其推迟到它从目标得到一个通知之后。Notify 不总是由目标对象调用。它也可被一个观察者或其它对象调用。

4. 优缺点

Observer 模式允许你独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用其观察者, 反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。

  • 优点

    • 观察目标和观察者间的抽象耦合。一个观察目标所知道的仅仅是它有一系列观察者, 每个都符合抽象 Observer 类的简单接口,观察目标并不知道任何一个观察者是属于哪一个具体的类,这样观察目标和观察者之间的耦合是抽象的、也是最小的。

      因为观察目标和观察者不是紧密耦合的, 它们属于一个系统中的不同抽象层次。一个处于较低层次的观察目标对象可与一个处于较高层次的观察者通信并通知它, 这样就保持了系统层次的完整性。如果观察目标和观察者混在一块 , 那么得到的对象要么横贯两个层次 (违反了层次性), 要么必须放在这两层的某一层中(这可能会损害层次抽象)。

    • 支持广播通信。不像通常的请求, 观察目标发送的通知不需指定它的接收者,通知被自动广播给所有已向该观察目标对象登记的有关对象。观察目标对象并不关心到底有多少对象对自己有用,它唯一的责任就是通知它的各个观察者。这给了你在任何时刻增加和删除观察者的自由,处理还是忽略一个通知取决于观察者。

  • 缺点

    • 意外的更新。一个观察者并不知道还有其它的观察者存在, 它可能对改变观察目标的最终代价一无所知。在观察目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。如果依赖准则的定义或维护不当,常常会引起错误的更新, 这种错误通常很难捕捉。
    • 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。
    • 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
    • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

5. 使用时注意事项?

  1. 创建观察目标到其观察者之间的映射。一个观察目标对象跟踪它应通知的观察者的最简单的方法是显式地在观察目标中保存对它们的引用。然而, 当观察目标很多而观察者较少时, 这样存储可能代价太高。一个解决办法是用时间换空间, 用一个关联查找机制 (例如一个 hash 表)来维护观察目标到观察者的映射。这样一个没有观察者的观察目标就不产生存储开销。但另一方面, 这一方法增加了访问观察者的开销。

  2. 观察多个观察目标。在某些情况下, 一个观察者依赖于多个观察目标可能是有意义的。例如, 一个表格对象可能依赖于多个数据源。在这种情况下, 必须扩展 Update 接口以使观察者知道是哪一个观察目标送来的通知。观察目标对象可以简单地将自己作为 Update 操作的一个参数, 让观察者知道应去检查哪一个观察目标。

  3. 谁触发更新。 观察目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用 Notify 来触发更新? 此时有两个选择:

    • 由观察目标对象的状态设定操作在改变观察目标对象的状态后自动调用 Notify。这种方法的优点是客户不需要记住要在观察目标对象上调用 Notify,缺点是多个连续的操作会产生多次连续的更新, 可能效率较低。
    • 让客户负责在适当的时候调用 Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用 Notify,这种方式较易出错。
  4. 对已删除观察目标的悬挂引用。 删除一个观察目标时应注意不要在其观察者中遗留对该观察目标的悬挂引用。一种避免悬挂引用的方法是:当一个目标被删除时,让它通知它的观察者将对该观察目标的引用复位。一般来说, 不能简单地删除观察者, 因为其他的对象可能会引用它们 , 或者也可能它们还在观察其他的观察目标。

  5. 在发出通知前确保观察目标的状态自身是一致的。在发出通知前确保状态自身一致这一点很重要, 因为观察者在更新其状态的过程中需要查询观察目标的当前状态。

  6. 避免特定于观察者的更新协议:推/拉模型。观察者模式的实现经常需要让观察目标广播关于其改变的其他一些信息。观察目标将这些信息作为 Update 操作一个参数传递出去。这些信息的量可能很小,也可能很大。

    • 一个极端情况是,观察目标向观察者发送关于改变的详细信息, 而不管它们需要与否,我们称之为推模型(push model)。另一个极端是拉模型(pull model): 观察目标除最小通知外什么也不送出 ,而在此之后由观察者显式地向目标询问细节。
    • 拉模型强调的是观察目标不知道它的观察者 , 而推模型假定观察目标知道一些观察者的需要的信息。推模型可能使得观察者相对难以复用,因为观察目标对观察者的假定可能并不总是正确的。另一方面,拉模型可能效率较差, 因为观察者对象需在没有观察目标对象帮助的情况下确定什么改变了。
  7. 显式地指定感兴趣的改变。 你可以扩展目标的注册接口 ,让各观察者注册为仅对特定事件感兴趣,以提高更新的效率。当一个事件发生时, 观察目标仅通知那些已注册为对该事件感兴趣的观察者。支持这种做法一种途径是,对使用目标对象的方面(aspects)的概念。可用如下代码将观察者对象注册为对观察目标对象的某特定事件感兴趣:
    void Subject::Attach(Observer*, Aspect& interest);
    此处 interest 指定感兴趣的事件。在通知的时刻, 观察目标将这方面的改变作为 Update 操作的一个参数提供给它的观察者,例如 :
    void Observer::Update(Subject*, Aspect& interest);

  8. 封装复杂的更新语义。当观察目标和观察者间的依赖关系特别复杂时, 可能需要一个维护这些关系的对象,我们称这样的对象为更改管理器(ChangeManager)。它的目的是尽量减少观察者反映其观察目标的状态变化所需的工作量。例如, 如果一个操作涉及到对几个相互依赖的目标进行改动, 就必须保证仅在所有的观察目标都已更改完毕后,才一次性地通知它们的观察者 ,而不是每个观察目标都通知观察者。ChangeManager有三个责任:

    • 它将一个观察目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由观察目标来维护对其观察者的引用, 反之亦然。
    • 它定义一个特定的更新策略。
    • 根据一个观察目标的请求, 它更新所有依赖于这个目标的观察者。
  9. 结合观察目标类和观察者类。用不支持多重继承的语言(如Smalltalk)书写的类库通常不单独定义 Subject 和 Observer 类, 而是将它们的接口结合到一个类中。这就允许你定义一个既是一个观察目标又是一个观察者的对象,而不需要多重继承。

6. 与其它设计模式之间的关系

  • Chain of Responsibility(责任链模式)、 Command(命令模式)、 Mediator(中介者模式)和 Observer(观察者模式)用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。
  • 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。

    • 中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。

    • 有一种流行的中介者模式实现方式依赖于观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者非常相似。

    • 当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同, 但这仍是一种中介者模式。

    • 假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。

7. 具体的代码实现

#include <iostream>
#include <cstring>
#include <list>
using namespace std;

// abstract obsrver
class IObserver
{
public:
    IObserver(){};
    virtual ~IObserver(){};
    virtual void Update(const string& message_from_subject) = 0;
};

// abstract subject
class ISubject
{
private:
    /* data */
public:
    ISubject(/* args */) {}
    virtual ~ISubject() {}
    virtual void Attach(IObserver* observer) = 0;
    virtual void Detach(IObserver* observer) = 0;
    virtual void Notify() = 0;
};

// concrete subject
class Subject : public ISubject
{
public:
    Subject(/* args */) {}
    virtual ~Subject() { cout << "Goodbye, I was the Subject." << endl; }

    /**
     * The subscription management methods.
     */
    void Attach(IObserver* observer) override 
    {
        m_list_observer.push_back(observer);
    }

    void Detach(IObserver* observer) override
    {
        m_list_observer.remove(observer);
    }

    void CreateMessage(string message = "Empty")
    {
        this->m_message = message;
        Notify();
    }

    void HowManyObserver() 
    {
        cout << "There are " << m_list_observer.size() 
             << " observers in the list." << endl;
    }

    void Notify() override
    {
        list<IObserver*> ::iterator iterator = m_list_observer.begin();
        HowManyObserver();
        while (iterator != m_list_observer.end()) {
            (*iterator)->Update(m_message);
            ++iterator;
        }
    }

    /**
     * Usually, the subscription logic is only a fraction of what a Subject can
     * really do. Subjects commonly hold some important business logic, that
     * triggers a notification method whenever something important is about to
     * happen (or after it).
     */
    void SomeBuinessLogic()    // no use
    {
        this->m_message = "change message";
        Notify();
        cout << "I'm about to do something important." << endl;
    }

private:
    list<IObserver*> m_list_observer;
    string m_message;
};

// concrete oberver
class Observer : public IObserver
{
public:
    Observer(Subject& subject)
        :m_subject(subject) 
    {
        this->m_subject.Attach(this);   
        cout << "Hi, I'm the observer \"" << ++Observer::m_static_number 
            << "\"." << endl;

        this->m_number = Observer::m_static_number;
    }
    virtual ~Observer() 
    {
        cout << "Goodbye, I was the observer \"" << this->m_number << "\"" << endl;
    }

    void Update(const string& message_from_subject) override
    {
        m_message_from_subject = message_from_subject;
        PrintInfo();
    }

    void PrintInfo() 
    {
        cout << "Observer \"" << this->m_number 
             << "\" : a new message is available --> "
             << this->m_message_from_subject << endl;
    }

    void RemoveMeFromList() 
    {
        m_subject.Detach(this);    // this: 运行时,解析的是哪个就传入的是哪个
        cout << "Observer \"" << this->m_number << " \" remove from list." << endl;
    }

private:
    int m_number;
    Subject& m_subject;
    string m_message_from_subject;
    static int m_static_number;
};

// init static
int Observer::m_static_number = 0;

void ClientCode()
{
    Subject* sub = new Subject;
    Observer* obser1 = new Observer(*sub);
    Observer* obser2 = new Observer(*sub);
    Observer* obser3 = new Observer(*sub);
    Observer* obser4;
    Observer* obser5;

    sub->CreateMessage("Hello: D");
    obser3->RemoveMeFromList();

    sub->CreateMessage("The weather is hot today! :p");
    obser4 = new Observer(*sub);

    obser2->RemoveMeFromList();
    obser5 = new Observer(*sub);

    sub->CreateMessage("My new car is great!");
    obser5->RemoveMeFromList();

    obser4->RemoveMeFromList();
    obser1->RemoveMeFromList();

    delete obser1;
    delete obser2;
    delete obser3;
    delete obser4;
    delete obser5;
}

using namespace std;

int main() 
{
    ClientCode();

    return 0;
}

代码实现的UML图
在这里插入图片描述
注: 文章所写内容,均根据自己的理解来进行阐述的,若和读者的思考不一致,读者感觉理解不是很清楚的话,请去我列出的参考文献中找到最原始的内容,进行学习和思考。
所有的文章和代码都可以在本人的Github上面找到,最新的文章一般在Github上面,博客中的文章不一定是最新的。
Github: Learning-Computer-Science-Journey

8. 参考文献

  1. Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides撰写的 《设计模式:可复用面向对象软件的基础》
  2. https://refactoring.guru/design-patterns
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值