观察者模式

定义

“观察者模式”是一个坑很多的模式。

要实现一个简单的观察者模式可能并不困难,但如果想要实现好一个没有 bug、功能完善的观察者模式,或许也不容易,这其中有许多值得注意的地方。

本文将通过一个布告板的例子1,首先用最直观的方式实现,分析代码实现中可能存在的问题,引出并应用观察者模式,以达到“优美”地解决案例的目的,在读者对观察者模式有一定了解之后,提出关于观察者模式的一些小知识点,以及不同场景下应用该模式需要注意的地方,最终介绍观察者模式的实际应用。

案例

现在已经有一个能获取实际气象数据(温度、湿度、空气质量指数 AQI)的气象站以及三个布告板,需要实现一个功能,当气象数据有变化时:

  1. 能在“当前状态”布告板上刷新当前显示的温度、湿度
  2. 能在“统计”布告板上刷新今日的最高温度和最低温度
  3. 能在“AQI”布告板上刷新 AQI 指数及空气质量等级

在这里插入图片描述

类图

在这里插入图片描述

思路

  1. 定义三个布告板类:当前状态布告板 currentConditionDisplayBoard,统计布告板 statisticsDisplayBoard 以及 AQI 布告板 aqiDisplayBoard。其中当前状态布告板与 AQI 布告板每当其 update 方法被调用,就展示传入的最新数据,统计布告板需要储存历史的最高、最低温度,若最新温度不在该范围内,则不刷新展示;
  2. 定义一个 weatherData 类,该类用于储存上一次观测到的温度 temperature、湿度 humidity 以及空气质量指数 aqi,以调用 measurementChange 方法来模拟气象站“监测”到了数值的改变,方法的入参为三个指针,只有当某项数值有改变时,才会传入有效的地址,否则只会传入 NULLmeasurementChange 方法内部根据不同的值被更新,将调用不同 DisplayBoardupdate 方法,更新字段及布告板对应关系如下表:
更新字段布告板
temperaturecurrentConditionDisplayBoard / statisticsDisplayBoard
humiditycurrentConditionDisplayBoard
aqiaqiDisplayBoard

代码1

详细代码见:observer_mode_sample/sample1

current_condition_display_board

class currentConditionDisplayBoard {
public:
    void update(double temperature, double humidity) {
        printf("=========== Current Condition Display Board ============\n");
        // output update data
    }
};

statistics_display_board

class statisticsDisplayBoard {
private:
    double minTemperature;
    double maxTemperature;

public:
    statisticsDisplayBoard() {
        minTemperature = std::numeric_limits<double>::infinity();
        maxTemperature = -minTemperature;
    }

    void update(double temperature) {
        if (temperature >= minTemperature && temperature <= maxTemperature) {
            return ;
        }

        maxTemperature = std::max(maxTemperature, temperature);
        minTemperature = std::min(minTemperature, temperature);

        printf("=============== Statistics Display Board ===============\n");
        // output update data
    }
};

aqi_display_board

class aqiDisplayBoard {
public:
    void update(double aqi) {
        printf("================== AQI Display Board ===================\n");
        // output update data
    }
};

weather_data

class weatherData {
private:
    int updateTimes;

    double temperature;
    double humidity;
    double aqi;

    currentConditionDisplayBoard &ccBoard;
    statisticsDisplayBoard &sBoard;
    aqiDisplayBoard &aqiBoard;

public:
    weatherData(currentConditionDisplayBoard &ccBoard, statisticsDisplayBoard &sBoard, aqiDisplayBoard &aqiBoard):
        ccBoard(ccBoard), sBoard(sBoard), aqiBoard(aqiBoard) {
        // Initialize other member variables to 0
    }

    void mesurementChange(double *temperature, double *humidity, double *aqi) {
        if (temperature != NULL) {
            this->temperature = *temperature;
        }
        if (humidity != NULL) {
            this->humidity = *humidity;
        }
        if (aqi != NULL) {
            this->aqi = *aqi;
        }

        ++updateTimes;
        printf("Update case %d:\n", updateTimes);

        if (temperature != NULL || humidity != NULL) {
            ccBoard.update(this->temperature, this->humidity);
        }
        if (temperature != NULL) {
            sBoard.update(this->temperature);
        }
        if (aqi != NULL) {
            aqiBoard.update(this->aqi);
        }
    }
};

main

int main() {
    double temperature;
    double humidity;
    double aqi;

    currentConditionDisplayBoard ccBoard;
    statisticsDisplayBoard sBoard;
    aqiDisplayBoard aqiBoard;
    weatherData wd(ccBoard, sBoard, aqiBoard);

    // ccBoard 与 sBoard 都会更新,aqiBoard 不会更新
    temperature = 25;
    humidity = 0.9;
    wd.mesurementChange(&temperature, &humidity, NULL);

    temperature = 26;
    wd.mesurementChange(&temperature, NULL, NULL);

    // ccBoard 会更新,sBoard 和 aqiBoard 都不会更新
    temperature = 25.5;
    wd.mesurementChange(&temperature, NULL, NULL);

    // ccBoard 和 sBoard 都不会更新,aqiBoard 会更新
    aqi = 50;
    wd.mesurementChange(NULL, NULL, &aqi);

    return 0;
}

运行结果:

Update case 1:
=========== Current Condition Display Board ============
        temperature: 25.0       humidity: 0.9

=============== Statistics Display Board ===============
        max temperature: 25.0   min temperature: 25.0

Update case 2:
=========== Current Condition Display Board ============
        temperature: 26.0       humidity: 0.9

=============== Statistics Display Board ===============
        max temperature: 26.0   min temperature: 25.0

Update case 3:
=========== Current Condition Display Board ============
        temperature: 25.5       humidity: 0.9

Update case 4:
================== AQI Display Board ===================
        aqi: 50.0               level: 1

代码分析

查看上面的代码可以发现,weatherData 类持有了三个布告板的引用,并且需要在初始化时将这三个对象传入,每当有数据变更时,都需要调用每个一布告板的 update 方法,相比于 weatherData 类,布告板的展示内容、它们所关心的变更数据内容的变化是更频繁的:

  1. 统计布告板后续可能需要展示近 7 天的 AQI 数据变化,那么 weatherData 类就需要在代码中添加 “AQI 值变更时调用 statisticsDisplayBoard 类的 update 方法”的逻辑,此时 update 方法的入参也需要修改;
  2. 后面可能新增各种各样的布告板,以展示不同的信息,此时 mesurementChange 方法的代码量将随着需要 update 布告板数量的增加而增加

相对而言,weatherData 类监测的数据类型变化的频率会更低一些,低频变动的模块依赖于高频变动模块的具体实现,将导致低频变动模块代码的频繁改动,大大提高了代码的开发维护成本,这违反了依赖倒置原则开放封闭原则

同时,由于 weatherData 类需要更新哪几个布告板是硬编码的,这就导致了无法在程序运行过程中进行动态地绑定、解绑这种 update 的调用关系,

观察者模式

定义

观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并更新。

类图

在这里插入图片描述

参与成员

  • subject:被观察者,能知道它的所有观察者,提供注册、删除观察者对象的接口;
  • observer:观察者,为被观察者发生改变时需要获得通知的对象定义了更新接口的抽象类;
  • concreteSubject:具体的被观察者(目标对象),当其状态发生改变时,向各个观察者发送通知;
  • concreteObserver:具体的观察者,维护一个指向 concreteSubject对象的引用,以及 subject 相关的状态 observerState,这些状态应与被观察者状态保持一致。

说明

目标对象(被观察者)对外提供注册观察者的 attach 方法,以及取消注册的 detach 方法,所有注册的观察者都储存于 os 数据成员中,observer 是一个抽象类,只要是继承了 observer 并实现其 update 方法的类型就可以作为 subject 的观察者。

每当目标对象的状态发生改变时,就会通过 notify 方法通知所有 observer,调用它们的 update 方法,而每个 observer 在接收到通知后,执行相应的逻辑,其中可能需要获取目标对象的状态,concreteSubject 提供了 getState 方法让收到通知的观察者获取到它的最新状态,因此在 concreteObserver 下持有目标对象的引用。

适用性

  • 当一个抽象模型有两个方面 , 其中一个方面依赖于另一方面时,可以将这二者封装在独立的对象中以使它们可以各自独立地改变和复用;
  • 当对一个对象的改变需要同时改变其它对象 , 而不知道具体有多少对象待改变时;
  • 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。或者说 , 我们不希望这些对象是紧密耦合的。

应用于案例

类图

在这里插入图片描述

思路

weatherData 即被观察者,三个布告板为观察者,每当 weatherData 监测到数值发生变化时,就直接向所有的 observer 发送通知,它不需要关心哪些观察者对哪些数据的变更感兴趣。

当观察者收到通知时,再向 weatherData 对象获取自己所感兴趣的数据,通过与上一次储存的目标对象状态相比较,来判断对应状态是否发生了改变,若发生改变,则刷新布告板,因此三个布告板类型都持有 weatherData 的引用以及需要关心的目标状态。

代码2

详细代码见:observer_mode_sample/sample2

observer

class observer {
public:
    virtual void update() = 0;
};

current_condition_display_board

class currentConditionDisplayBoard: public observer {
private:
    double temperature;
    double humidity;
    weatherData &wd;

public:
    currentConditionDisplayBoard(weatherData &wd): wd(wd) { /* Initialize other member variables to 0 */ }

    void update() {
        double temperature = wd.getTemperature();
        double humidity = wd.getHumidity();

        if (util::equal(temperature, this->temperature) && util::equal(humidity, this->humidity)) {
            return ;
        }

        this->temperature = temperature;
        this->humidity = humidity;
        printf("=========== Current Condition Display Board ============\n");
        // output udpate data
    }
};

statistics_display_board

class statisticsDisplayBoard: public observer {
private:
    double minTemperature;
    double maxTemperature;
    weatherData &wd;

public:
    statisticsDisplayBoard(weatherData &wd): wd(wd) {
        minTemperature = std::numeric_limits<double>::infinity();
        maxTemperature = -minTemperature;
    }

    void update() {
        double temperature = wd.getTemperature();

        if (temperature >= minTemperature && temperature <= maxTemperature) {
            return ;
        }

        maxTemperature = std::max(maxTemperature, temperature);
        minTemperature = std::min(minTemperature, temperature);

        printf("=============== Statistics Display Board ===============\n");
        // output update data
    }
};

aqi_display_board

class aqiDisplayBoard: public observer {
private:
    double aqi;
    weatherData &wd;

public:
    aqiDisplayBoard(weatherData &wd): wd(wd) { /* Initialize other member variables to 0 */ }

    void update() {
        double aqi = wd.getAQI();
        if (util::equal(aqi, this->aqi)) {
            return ;
        }

        this->aqi = aqi;
        printf("================== AQI Display Board ===================\n");
        // output update data
    }
};

subject

class subject {
private:
    std::vector<observer*> os;

public:
    void attach(observer *o) {
        os.push_back(o);
    }

    void detach(observer *o) {
        os.erase(std::remove(os.begin(), os.end(), o), os.end());
    }

    void notify() {
        for (auto o: os) {
            o->update();
        }
    }
};

weather_data

class weatherData: public subject {
private:
    int updateTimes;

    double temperature;
    double humidity;
    double aqi;

public:
    weatherData() { /* Initialize member variables to 0 */ }

    double getTemperature() { return temperature; }
    double getHumidity() { return humidity; }
    double getAQI() { return aqi; }

    void mesurementChange(double *temperature, double *humidity, double *aqi) {
        if (temperature != NULL) {
            this->temperature = *temperature;
        }
        if (humidity != NULL) {
            this->humidity = *humidity;
        }
        if (aqi != NULL) {
            this->aqi = *aqi;
        }

        ++updateTimes;
        printf("Update case %d:\n", updateTimes);

        notify();
    }
};

main

int main() {
    double temperature;
    double humidity;
    double aqi;

    weatherData wd;
    currentConditionDisplayBoard ccBoard(wd);
    statisticsDisplayBoard sBoard(wd);
    aqiDisplayBoard aqiBoard(wd);

    wd.attach(&ccBoard);
    wd.attach(&sBoard);
    wd.attach(&aqiBoard);

    // ccBoard 与 sBoard 都会更新,aqiBoard 不会更新
    temperature = 25;
    humidity = 0.9;
    wd.mesurementChange(&temperature, &humidity, NULL);

    temperature = 26;
    wd.mesurementChange(&temperature, NULL, NULL);

    // ccBoard 会更新,sBoard 和 aqiBoard 都不会更新
    temperature = 25.5;
    wd.mesurementChange(&temperature, NULL, NULL);

    // ccBoard 和 sBoard 都不会更新,aqiBoard 会更新
    aqi = 50;
    wd.mesurementChange(NULL, NULL, &aqi);

    // aqiBoard 取消订阅,关于 aqi 的更新将不会触发 aqiBoard 的刷新显示
    wd.detach(&aqiBoard);
    aqi = 300;
    wd.mesurementChange(NULL, NULL, &aqi);

    return 0;
}

运行结果:

Update case 1:
=========== Current Condition Display Board ============
        temperature: 25.0       humidity: 0.9

=============== Statistics Display Board ===============
        max temperature: 25.0   min temperature: 25.0

Update case 2:
=========== Current Condition Display Board ============
        temperature: 26.0       humidity: 0.9

=============== Statistics Display Board ===============
        max temperature: 26.0   min temperature: 25.0

Update case 3:
=========== Current Condition Display Board ============
        temperature: 25.5       humidity: 0.9

Update case 4:
================== AQI Display Board ===================
        aqi: 50.0               level: 1

Update case 5:

代码分析

从代码中可以看出,后续若有新布告板需要监听 weatherData 中数据的变更,只要新增一个布告板类,并继承 observer 抽象类即可,即使原布告板需要关注新的变更字段,也无需修改 weatherData 类中的任何一行代码,只需要修改对应布告板中的代码。达到了面向扩展开放,面向修改封闭的目的。

weatherData 类只依赖 observer 抽象类,而不依赖观察者类的具体实现,观察者与目标对象其中一方的改变并不会影响另一方,实现了松耦合

main 函数中可以看到,由于 subject 提供了 attachdetach 方法,这使得我们可以在运行时动态地添加、删除观察者,不因将观察逻辑写死在代码中而限制这种绑定关系。

补充说明

util::equal 方法是 util 类的静态成员函数,用于比较两个 double 类型的变量是否相等,由于浮点数在计算过程中容易损失精度,因此在 equal 方法中认为待比较的两个浮点数误差在 1 0 − 6 10^{-6} 106 范围内即相等。

上面的代码中,并没有对 observer 做重复注册的检查,如果同一个 observer 进行了多次注册,我们认为观察者想要在目标对象发生一次变更时收到多条通知;若由于特殊原因,观察者不得不进行多次注册,而最终只期望在一次变更时收到一次通知,则目标对象可以在每次变更发生时生成一个唯一 id,由观察者来进行幂等性的逻辑保证。

Bug

上一段代码已经实现了观察者模式,但其中存在一个 bug,如果▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 2(建议思考后,再跳转至注脚查看),为了解决这个 bug,下面提供了两种方案(当然最简单的方案是令布告板类中的 observerState 类型为指针,初始值为 NULL 表示未更新过,不过为了介绍关于观察者模式的更多实现,请读者耐心往下看~)。

方案一

类图

在这里插入图片描述

思路

布告板在调用 attach 注册时,显式地指定其需要关注的“方面”(aspect,如温度、湿度),当 weatherData 监测到数据发生变更时,提取出变更所对应的 aspect,只向关注这些“方面”的布告板发送变更通知。注册后布告板与 aspect 的对应关系记录在一个 observerWithAspect 类型的对象中,通过查找变更的 aspect 是否存在于 unordered_set 中,来判断是否向对应布告板发送通知。

当布告板接收到通知时,可以知道自己关注的部分(温度、湿度、AQI)一定发生了变更,它只要负责更新(刷新布告板)即可,不需要做新、旧状态的比较才能得出 weatherData 是否发生了变更的结论。可以发现,在 AQI 与当前状态布告板类型中,已经不再储存 subjectState 相关的字段,而统计布告板由于始终需要记录历史最高 / 最低温度,因此仍然不能取消 maxTemperatureminTemperature 成员变量。

代码3

详细代码见:observer_mode_sample/sample3

subject

typedef int aspect;

class subject {
private:
    class observerWithAspect {
    private:
        observer *o;
        std::unordered_set<aspect> as;

    public:
        observerWithAspect(observer *o, const std::initializer_list<aspect> &al): o(o) {
            for (auto a: al) {
                this->as.insert(a);
            }
        }

        friend class subject;
    };

    std::vector<observerWithAspect*> owas;
    static const int npos = -1;

public:
    ~subject() {
        for (auto owa: owas) {
            delete owa;
        }
    }

    void attach(observer *o, std::initializer_list<aspect> al) {
        observerWithAspect *owa = new observerWithAspect(o, al);
        owas.push_back(owa);
    }

    void detach(observer *o, std::initializer_list<aspect> al) {
        int pos = indexOfObserver(o);
        if (pos == npos) {
            return ;
        }

        for (auto a: al) {
            owas[pos]->as.erase(a);
        }

        if (owas[pos]->as.empty()) {
            delete owas[pos];
            owas.erase(owas.begin() + pos);
        }
    }

    void notify(const std::vector<aspect> &as) {
        for (auto owa: owas) {
            for (auto a: as) {
                if (owa->as.find(a) != owa->as.end()) {
                    owa->o->update();
                    break;
                }
            }
        }
    }
};

weather_data

class weatherData: public subject {
private:
    int updateTimes;

    double temperature;
    double humidity;
    double aqi;

public:
    static const aspect aspectTemperature;
    static const aspect aspectHumidity;
    static const aspect aspectAQI;

    weatherData() { /* Initialize member variables to 0 */ }

    void mesurementChange(double *temperature, double *humidity, double *aqi) {
        std::vector<aspect> as;

        if (temperature != NULL) {
            this->temperature = *temperature;
            as.push_back(this->aspectTemperature);
        }
        if (humidity != NULL) {
            this->humidity = *humidity;
            as.push_back(aspectHumidity);
        }
        if (aqi != NULL) {
            this->aqi = *aqi;
            as.push_back(aspectAQI);
        }

        ++updateTimes;
        printf("Update case %d:\n", updateTimes);

        notify(as);
    }
};

const aspect weatherData::aspectTemperature = 0;
const aspect weatherData::aspectHumidity = 1;
const aspect weatherData::aspectAQI= 2;

main

int main() {
    double temperature;
    double humidity;
    double aqi;

	weatherData wd;
	currentConditionDisplayBoard ccBoard(wd);
	statisticsDisplayBoard sBoard(wd);
	aqiDisplayBoard aqiBoard(wd);

	wd.attach(&ccBoard, {wd.aspectTemperature, wd.aspectHumidity});
	wd.attach(&sBoard, {wd.aspectTemperature});
	wd.attach(&aqiBoard, {wd.aspectAQI});

	// bug fix:现在可以触发所有更新了
	temperature = 0;
	humidity = 0;
	aqi = 0;
	wd.mesurementChange(&temperature, &humidity, &aqi);

	// ccdBoard 和 sdBoard 都会更新,aqiBoard 不会更新
	temperature = 25;
	humidity = 0.9;
	wd.mesurementChange(&temperature, &humidity, NULL);

	temperature = 26;
	wd.mesurementChange(&temperature, NULL, NULL);

	// ccdBoard 会更新,sdBoard 和 aqiBoard 都不会更新
	temperature = 25.5;
	wd.mesurementChange(&temperature, NULL, NULL);

	// ccdBoard 和 sdBoard 都不会更新,aqiBoard 会更新
	aqi = 50;
	wd.mesurementChange(NULL, NULL, &aqi);

	// aqiBoard 取消订阅,关于 aqi 的更新将不会触发 aqiBoard 的刷新显示
	wd.detach(&aqiBoard, {wd.aspectAQI});
	aqi = 300;
	wd.mesurementChange(NULL, NULL, &aqi);

    return 0;
}

补充说明

  1. 以上只展示了相比于原观察者模式实现中有明显差异的代码,关于 observer 及三个布告板的实现尽管有些许变动,但读者可以通过上面的类图了解其实现,完整代码请移步:observer_mode_sample/sample3
  2. initializer_listc++11 提供的新特性,在这里用于实现可变个数参数的传参,在调用时可以传入任意数量的 aspect,实际上可以用 vector 代替;
  3. subjectobserverWithAspect 作为其私有内部类,原因是 observerWithAspect 类只用于拼装 observeraspect,不应该被除 subject 以外的其他类型所调用。

方案二

类图

在这里插入图片描述

思路

每当 weatherData 类监测到数据变更时,将全量的变更消息通过 notify 中的 void* 参数推送出去,由布告板通过接收到的消息来判断自己所关心的变化是否发生了,如果发生,则刷新布告板的内容。

c++ 中,void* 可以与任意类型的指针互相转换,这样就可以将变更信息通过一个结构体指针传出,当然 void* 转换前后必须是相同的结构体指针类型,否则程序可能出现严重错误。也可以将 void* 改为 char 数组,使用序列化、反序列化的方式来进行变更信息的转化,所有接收通知的布告板都需要遵守 weatherData 定下的变更通知信息的转化规则,才能收到正确的信息。

注意到这里已经不存在布告板到 weatherData 实例的引用,weatherData 类也不再对外提供 getState 方法,因为在布告板收到通知时,已经知道了所有的变更信息,不需要再通过 weatherData 的引用来获取它的最新状态了。

代码4

详细代码见:observer_mode_sample/sample4

observer

class observer {
public:
    virtual void update(const void* changeInfo) = 0;
};

aqi_display_board

class aqiDisplayBoard: public observer {
private:
    double aqi;

    bool getInfo(const void *changeInfo) {
        const weatherData::changeInfo *ci = reinterpret_cast<const weatherData::changeInfo*>(changeInfo);
        if (ci->aqi == NULL) {
            return true;
        }

        aqi = *(ci->aqi);
        return false;
    }

public:
    aqiDisplayBoard() { /* Initialize member variables to 0 */ }

    void update(const void *changeInfo) {
        bool skip = getInfo(changeInfo);
        if (skip) {
            return ;
        }

        printf("================== AQI Display Board ===================\n");
        printf("\taqi: %.1f\t\tlevel: %d\n\n", aqi, getLevel());
    }
};

subject

class subject {
private:
    std::vector<observer*> os;

public:
    void notify(const void *changeInfo) {
        for (auto o: os) {
            o->update(changeInfo);
        }
    }
};

weather_data

class weatherData: public subject {
private:
    int updateTimes;

    double temperature;
    double humidity;
    double aqi;

public:
    struct changeInfo {
        double *temperature;
        double *humidity;
        double *aqi;

        changeInfo(double *temperature, double *humidity, double *aqi):
            temperature(temperature), humidity(humidity), aqi(aqi) {}
    };

    weatherData() { /* Initialize member variables to 0 */ }

    void mesurementChange(double *temperature, double *humidity, double *aqi) {
        changeInfo *ci = new changeInfo(temperature, humidity, aqi);

        if (temperature != NULL) {
            this->temperature = *temperature;
        }
        if (humidity != NULL) {
            this->humidity = *humidity;
        }
        if (aqi != NULL) {
            this->aqi = *aqi;
        }

        ++updateTimes;
        printf("Update case %d:\n", updateTimes);

        notify(ci);
    }
};

补充说明

  1. 以上只展示部分代码,由于另外两种布告板的实现与 AQI 布告板实现相似,subjectattachdetach 方法的实现与应用于案例中的实现完全相同,故没有展示,完整代码请移步:observer_mode_sample/sample4
  2. 携带变更信息的 changeInfo 使用 const void* 类型,目的是防止在多个 update 调用过程中,changeInfo 中的值被修改,造成严重影响;
  3. 由于 changeInfo 只储存关于 weatherData 的变更信息,且应该允许外部访问到 changeInfo 中的数据成员,因此将 changeInfo 作为 weatherDatapublic 内部结构体。

关于

推拉模型

  • sample2sample3 都是在被观察者发生变更时,只通知观察者有变更发生,而具体的变更则需要在观察者收到通知后,再调用 subjectgetState 方法拉取详细的信息。这种在发生变更时,只将最小通知发送给观察者,其他详细信息等待观察者询问的模型称为拉模型,如果观察者较多,则拉模型将导致多次观察者的询问行为,可能使程序效率低下;
  • sample4 中, 被观察者不论时什么变更,都将所有变更信息通过 changeInfo 推送出去。这种在每次发生变更时,将变更信息推送给观察者,被观察者不需要关心其他观察者是否需要这些信息的模型称为推模型,推模型可能导致观察者对象变得难以复用。

不论是推模型还是拉模型,都要注意的是:不要设计特定于某个观察者的更新协议,因为我们无法预料后续可能会出现的其他观察者。

触发 notify 的时机

  1. 在每次目标对象的状态发生改变时,就自己在内部调用 notify 方法通知被观察者
  • 优点:目标的调用者无需在改变目标状态后,调用 notify 进行通知(不会忘记)

  • 缺点:多次对同一个目标的更新可能发送多条更新信息,降低效率

  1. notify 方法的调用交给目标的调用者,由调用者来确定在“适当的时机”进行调用
  • 优点:这样可以在一系列状态改变完毕后,一次性触发 notify,避免不必要的通知

  • 缺点:notify 的触发容易被调用者忘记

复杂的依赖

可能存在一个观察者观察多个被观察者的情况,例如一个表格依赖多个数据源,或者是游戏中的成就系统,甚至可能存在多对多的情况(例如用户使用界面按钮点击之间的互斥关系,点击了某几个按钮,则与其互斥状态的按钮需要 disable)。当观察者和被观察者之间的依赖关系变得更复杂的时候,需要引入一个 ChangeManager 管理这种依赖关系,目前有两种 ChangeManager:

  • simpleChangeManager:如上,总是更新每一个目标的所有观察者
  • dagChangeManager:当一个观察者观察多个目标,而这多个目标之间的更新又有相互关联时,它们之间可能形成一种有向无环图,这种情况下使用 dagChangeManager 可以有效避免冗余的更新操作

dagChangeManager 是 Mediator(中介者)模式的一个实例,通常情况下只需要一个 ChangeManager 且全局可见,此时也可以使用 Singleton(单例)模式。

注意

意外的更新

如果被观察者允许某些观察者更新自身的状态,由于每个观察者都并不知道其他观察者的存在,因此它对改变被观察者所产生的影响与代价一无所知。如果依赖关系的定义或维护不当,可能造成错误的更新,而这种错误通常很难被捕捉到。

状态一致

在发送通知之前需要确保被观察者的状态自身是一致的,因为在发出通知之后,观察者需要查询目标的状态,若此时自身状态不一致,则容易出现逻辑错误,下面这段代码就非常容易在无意中犯下这样的错误:

class baseSubject {
protected:
    int value;

public:
    void notify() {
        printf("notify now!\nvalue: %d\n", value);
    }

    void operation(int newValue) {
        value = newValue;
        notify();
    }
};

class subject: public baseSubject {
public:
    void operation(int newValue) {
        baseSubject::operation(newValue);
        // 通知已发送完毕
        value += newValue;
    }
};

/**
* 预期执行结果:
* notify now!
* value: 4
* 实际执行结果:
* notify now!
* value: 2
*/

int main() {
    subject s;
    s.operation(2);

    return 0;
}

使用模板方法模式可以保证 notify 函数在所有状态确定之后才被调用:

class baseSubject {
protected:
    int value;

public:
    void notify() {
        printf("notify now!\nvalue: %d\n", value);
    }

    virtual void doOperation(int newValue) {
        value = newValue;
    }

    void operation(int newValue) {
        doOperation(newValue);
        notify();
    }
};

class subject: public baseSubject {
public:
    void doOperation(int newValue) {
        baseSubject::doOperation(newValue);
        value += newValue;
    }
};

int main() {
    subject s;
    s.operation(2);

    return 0;
}

同步通知

观察者模式中的通知是同步进行的,只有所有观察者的操作都执行完毕之后,被观察者才会继续后面的操作,如果观察者进行的操作是比较耗时的,此时目标对象就会被观察者的流程阻塞,如果这种阻塞不是必须的,可以将观察者的操作推到另一个线程或工作队列中去。

在观察者模式中要十分小心其中混合的线程和锁,如果观察者试图获取目标的锁,这时就会产生死锁。在多线程引擎中,最好采取事件队列来做异步通信。

悬挂引用

sample2sample3 中,所有的观察者实例都持有一个 weatherData 的引用,而 sample4 中没有,sample2 与 3 中的 weatherData 作用是实现“拉模型”时需要通过该引用获取到目标状态,实际上可以将该引用放在 notify 中:

class subject {
public:
    void notify(const weatherData &wd) {
        for (auto o: os) {
            o->update(wd);
        }
    }
};

观察者的 update 接口获取到该引用,就可以拿到目标状态。

实际上观察者保留指向被观察者的引用还有另一个更重要的作用:当观察者销毁时,它可以通过引用找到被观察者,取消自身在观察者的注册,如果被观察者的生命周期比观察者的周期更长,这是十分有必要的。

  • 对于需要手动释放空间的语言,销毁观察者自身时,若未取消在目标对象中的注册,在目标对象调用 notify 时就会访问到非法地址;
  • 对于有垃圾回收器的语言,由于没有显式地取消在被观察者上的注册,后续每一个动作都会调用一次无意义(甚至可能产生副作用)的通知

举个栗子,当玩家进入一个打怪副本时,会 new 出一个玩家的血条展示在界面的左上角,每当玩家受到攻击时,血条作为观察者就会收到一次攻击事件,并做出“扣血”的响应。当玩家退出副本后,副本中的血条要么被注销,要么仍然被目标对象(被观察者)引用,当玩家进入一个新副本时,每当他受到一次攻击,就会向上一个副本的血条发送一个攻击事件,而此时血条做出的任何更新响应都是无意义的。若观察者不是血条,而是受到攻击时播放声音的控件,此时玩家在受到攻击时就会听到双重攻击效果的声音,这种会产生副作用的通知,对玩家而言就是一个 bug。

同样,当被观察者的生命周期结束时,也应该向观察者发送通知,重置观察者到自身的引用,这可以通过向观察者发送一个“死亡”事件达到目的。

应用

  • Java 内置了观察者模式,在 java.util.Observer / java.util.Observable 包中,java 将 Observable 实现为一个具体的类,提供了setChangednotifyObserversnotifyObservers 方法,同时支持推拉模式,在调用 notify 方法之前需要先 setChanged 表示状态已改变,否则 notify 将不会通知观察者,在通知结束后,将会调用 clearChangedchanged 标记清除
  • Java swing 中的 Listener 可以让 Swing 组件响应不同类型的事件(鼠标点击、键盘按键等事件)
  • Qt 中利用 SignalSlot 实现了组件之间的通信,每当一个 Signal 被发射,与其连接的 Slot 立即被执行,信号槽机制是 Qt 的核心机制,它是 Qt 自行定义的一种通信机制

参考

《游戏编程模式》

设计模式:可复用面向对象软件的基础

《head first 设计模式》

bilibili:23 个设计模式

代码仓库

git clone https://github.com/Dmaxiya/observer_mode_sample

  1. 改编自《head first 设计模式》第二章观察者模式。 ↩︎

  2. 如果首次更新的 temperaturehumidity 值为 0,那么 ccBoardsBoard 将不会刷新 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值