观察者模式 in Modern C++

观察者模式 - Observer

很多情况下,我们不仅仅关系实例的具体属性值,还关心实例的属性值变化情况,例如每日股票变化、每月净收入增减、每年年龄变化……

观察者模式是一种普遍且重要的设计模式,在诸多语言(尤其是面向对象语言)中都有它身影,遗憾的是C++没有提供可用的观察者模式实现

观察者 && 被观察者

假设我们正在检测一个实例属性 Person::age 是否发生变化,并将其显示出来

一个很容易想到的方式是,在 Person::set_age()方法中检测 age 是否发生变化,同时将 Person::age设为私有变量,避免发生监测范围外的修改

struct Person {
  void set_age(const int value) {
    if (value == age) return;
    age = value;
    cout << "age is changing" << endl;
  }
private:
  int age;
}

Observer 和 Observable 基本结构 及 其连接

然而其他类可能也面临同样的问题,因此我们更希望采用模板类 Observable以应对多样化的需求,此外既然 Observable是一个可被观测到类,那么它需要实现一些被观察者相应的功能:

  • 维护一个观察者的列表,用于记录当前实例被哪些观察者观察
  • 可以实现观察者的订阅和取消订阅,观察者订阅后将其存入观察者列表,取消订阅则删除
  • 实现观察者和被观察者之间的信息传递

与之对应,观察者 Observer 需要支持获取来自被观察者的信息

template<class T>
struct Observer {
    // do something
    virtual void field_changed(T& source, const string& field_name) = 0;
};

template <class T>
struct Observable {
    // call Observer
    virtual void notify(T& source, const string& name);
    // add new Observer
    virtual void subscribe(Observer<T> *observer) { observers.push_back(observer); }
    // delete Observer
    virtual void unsubscribe(Observer<T> *observer);
private:
    // Observers list
    vector<Observer<T>*> observers;
};

template<class T>
void Observable<T>::unsubscribe(Observer<T> *observer) {
  // erase-remove: erase multi-observer
    observers.erase(
            remove(observers.begin(), observers.end(), observer),
            observers.end());
}

template<class T>
void Observable<T>::notify(T &source, const string &name) {
  // call all observers
    for (auto observer : observers) {
        observer->field_changed(source, name);
    }
}

erase-remove 是 STL 中一种非常实用的删除复数个元素的操作

// remove作用为:移除(但不删除)[__first, __last)之间所有与__value相等的元素
// 原理为将不想等的元素依次前移,并且最后返回重排位置后的最后一个元素的下一位置
// 由于不删除元素,所以容器总大小并不会发生变化
template <class _ForwardIterator, class _Tp>
_LIBCPP_NODISCARD_EXT _LIBCPP_CONSTEXPR_AFTER_CXX17 _ForwardIterator
remove(_ForwardIterator __first, _ForwardIterator __last, const _Tp& __value);

// vector<T>::erase 是删除[__first, __last)之间的元素
_LIBCPP_CONSTEXPR_AFTER_CXX17 iterator erase(const_iterator __first, const_iterator __last);

Person、Dog 以及 ConsoleObserver的实现

PersonDog 作为被观察者ObservableConsoleObserver 作为观察者Observer

struct Person : Observable<Person> {
    void set_age(const int value) {
        if (value == age) return;
        age = value;
        notify(*this, "age");
    }
    int get_age() const {
        return age;
    }
private:
    int age;
};

struct Dog : Observable<Dog> {
    void set_name(const string& value) {
        if (value == name) return;
        name = value;
        notify(*this, "name");
    }
    string get_name() const {
        return name;
    }
private:
    string name;
};


struct ConsoleObserver : Observer<Person>, Observer<Dog> {
    void field_changed(Person& source, const string& field_name) override {
        cout << "Person's " << field_name << " has change to " << source.get_age() << ".\n";
    }
    void field_changed(Dog& source, const string& field_name) override {
        cout << "Dog's " << field_name << " has change to " << source.get_name() << ".\n";
    }
};


int main() {
    Person person;
    ConsoleObserver co1;
    ConsoleObserver co2;
    person.subscribe(&co1);
    person.subscribe(&co2);
    person.set_age(10);		//	Person's age has change to 10.
													//	Person's age has change to 10.
    Dog dog;
    dog.subscribe(&co1);
    person.unsubscribe(&co2);
    person.set_age(30);		//	Person's age has change to 30.
    dog.set_name("pipi");	//	Dog's name has change to pipi.
    return 0;
}

通过信号(Signal)实现观察者模式

template <typename T>
struct Observable
{
  virtual ~Observable() = default;
  boost::signals2::signal<void(T&, const string&)> property_changed;
};
struct Person : Observable<Person>
{
  explicit Person(int age): age(age){}
  int get_age() const
  {
    return age;
  }

	void set_age(const int age)
  {
    if (this->age == age) return;
		this->age = age;
    property_changed(*this, "age");
  }

private:
	int age;
};

int main()
{
  Person p{123};
  auto conn = p.property_changed.connect([](Person&, const string& prop_name)
                             {
                             			cout << prop_name << " has been changed" << endl;
                             });
  p.set_age(20);	// age has been changed
	
  conn.disconnect();
	getchar();
  return 0;
}

线程安全

上述实现仅仅考虑了单线程,很容易发现, subscribe()unsubscribe() 都会修改 vector,一个很容易想到的解决办法是为所有涉及到 vector 的操作上锁,然而对操作加锁可能会导致可重入性问题,即我们不能从一个操作中调用另外一个操作,因为我们不能重复获得相同的锁

template <class T>
struct Observable {
    // call Observer
    virtual void notify(T& source, const string& name) {
      scoped_lock<mutex> lock{latch_};
      ...
    };
    // add new Observer
    virtual void subscribe(Observer<T> *observer) { 
      scoped_lock<mutex> lock{latch_};
      ...
    };
    // delete Observer
    virtual void unsubscribe(Observer<T> *observer) {
      scoped_lock<mutex> lock{latch_};
      ...
    };
private:
    // Observers list
    vector<Observer<T>*> observers;
  	// lock
  	mutex latch_;
};

通过改变锁的作用范围,在读操作(notify())中避免可重入性问题很容易,只要我们通过锁获取一份当前vector的副本vector_copy,解锁后对vector_copy进行任何读操作都不会带来可重入性问题

virtual void notify(T& source, const string& name) {
  vector<Observer<T>*> observers_copy;
  {
    lock_guard<mutex> lock{latch_};
    observers_copy = observers;
  }
  // do something
};

然而如何解决subscribe()unsubscribe() 之间的资源冲突是一个问题

  • 使用递归锁 recursive_mutex,这允许一个线程多次获取锁,当然也需要相应次数的解锁锁才可以真正释放
  • 使用 concurrent_vector ,尽管标准库并没有提供线程安全的容器,但是我们可以自己实现或者使用第三方提供的线程安全容器,这一切的前提是可以接受顺序先后不一致
  • 业务实现避免重入问题
  • 管理锁
  • 每个线程维护自己的 thread_local变量,当需要统计的时候,再将所有内容汇总,适用于订阅多,而取消订阅少的情况

参考资料

  • 《Design Patterns in Modern C++》 Dmitri Nesteruk
  • 《Effective C++ 改善程序与设计的55个具体做法》 Scott Meyers
  • https://cplusplus.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在现代的C++中,设计模式扮演着非常重要的角色。设计模式是一种被广泛应用的解决问题的方法和思想,能够帮助我们构建出可复用、可扩展和易于维护的代码。 在现代的C++中,很多经典的设计模式依然适用,例如单例模式、工厂模式、观察者模式等。这些设计模式能够帮助我们解决常见的设计问题,并且能够提高代码的可读性和可维护性。 此外,现代的C++中还出现了一些更加灵活和高级的设计模式,例如策略模式、代理模式、依赖注入等。这些设计模式能够帮助我们更好地应对复杂的需求和变化,并且提供了更加灵活和可测试的代码结构。 值得一提的是,现代的C++还引入了一些新的语言特性和库,例如模板元编程、智能指针、Lambda表达式等,这些特性可以与设计模式相结合,提供更强大和灵活的解决方案。 总之,设计模式在现代的C++中依然扮演着非常重要的角色。通过运用设计模式,我们能够更好地组织和管理代码,提高代码的可维护性和可扩展性,从而更好地应对变化和需求的挑战。 ### 回答2: 在现代C语言中,设计模式是一种广泛应用于软件开发的编程方法。设计模式是一种可重复使用的解决问题的方式,它可以帮助开发者更好地组织代码、提高代码的可读性和可维护性。 在现代C语言中,常用的设计模式包括单例模式、工厂模式、观察者模式等。这些设计模式通过提供特定的设计思路和结构,使得代码更容易被理解和维护。 单例模式是一种创建唯一对象的方式,它保证在整个程序中只有一个实例存在。在C语言中,可以使用静态变量来实现单例模式。 工厂模式是一种通过工厂类来创建对象的方式,它将对象的创建逻辑封装在工厂类中,客户端只需要通过工厂类来获取对象实例。在C语言中,可以使用函数指针来实现工厂模式。 观察者模式是一种当一个对象状态发生变化时,自动通知其它关联对象的方式。在C语言中,可以使用函数指针和回调函数来实现观察者模式。 除了上述提到的设计模式,现代C语言还可以使用其他的设计模式,如策略模式、装饰者模式等。这些设计模式都可以帮助开发者更好地组织和管理代码,提高代码的可维护性和重用性。 总之,设计模式在现代C语言中起着重要的作用。使用设计模式可以使代码更易懂、易于维护,并且可以提高代码的可重用性和可扩展性。对于开发者来说,了解和掌握设计模式是非常有益的。 ### 回答3: 在现代C语言中,设计模式是一种经过验证并被广泛应用的解决问题的方法。设计模式是一套被设计用来解决特定问题的代码结构和实现方式。以下是现代C语言中常用的几种设计模式: 1. 单例模式(Singleton):用于确保类只有一个实例,并提供一个全局访问点。在C语言中,可以通过全局静态指针或静态变量来实现。 2. 工厂模式(Factory):用于根据不同的条件创建对象。在C语言中,可以使用函数指针和条件语句来实现,根据条件选择不同的函数实现。 3. 观察者模式(Observer):用于实现对象之间的一对多依赖关系,当一个对象的状态发生改变时,其依赖的对象能够及时得到通知。在C语言中,可以使用函数指针回调来实现观察者和被观察者之间的通信。 4. 策略模式(Strategy):用于在运行时选择算法或行为。在C语言中,可以使用函数指针或函数指针数组来实现,通过调用不同的函数来进行不同的策略。 5. 适配器模式(Adapter):用于将一个类的接口转换成另一个类的接口,使得原本因接口不兼容而不能工作的类能够一起工作。在C语言中,可以通过封装原有类的数据类型,并提供统一的接口来实现。 总结来说,设计模式在现代C语言中依然具有很大的意义。通过使用设计模式,我们可以使代码更加易于维护、扩展和重用,并且在面对复杂的问题时能够提供清晰的解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值