读书笔记-设计模式-可复用版-Observer 观察者模式

读书笔记-设计模式-可复用版-Observer 观察者模式

wikipedia:

https://en.wikipedia.org/wiki/Observer_pattern

对于设计模式,重要的是要知道他的意图,能够解决什么样的问题,优缺点有哪些,这就够了,而不必去深究书中的每一个细节,GOF书中提到了很多在不同情境下要面对的情况以及解决方案,他更像是一本工具书,我们在应用特定设计模式的时候,看看它符合书中提到的哪些点,然后参考建议去实现, 这样会更有助于我们理解它。

意图:

定义对象间的一种,一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象,都会得到通知并自动更新。

生活中最为常见的微信公众号订阅,YOUTUBE频道订阅(a millions of subscribers)微博上的关注,当有内容更新时,订阅者(subscribes),即观察者(observers),可以得到通知。

可以说这是任何一个商业项目中,都会应用到的设计模式了~

Observer 观察者模式 又称发布-订阅模式(Publish-Subscribe)

解释:

我通常将一个系统分割成一系列相互协作的类,但这种方式通常会有一个常见的副作用,这些相互协作的类之间通常交互是比较紧紧密的,这就会便他们之间的耦合性变高,降低了可重用性。

书上的一个例子:

image.png

目标数据{a=50%,b=30%,c=20%}

会由三种 图表Graph的方式来表示,当数据变更的时候,三种图表均需要进行更新,显示最新的数据,同样,如果图表是可编辑的状态,那么我修改了图标里的内容,目标数据要进行更新,其它的两个图标也要跟着更新,可以想象一下,我们将上图想象成为四个对象,每一个对象中,都要有其它三个对象的引用,这样我们才能够通过引用去更新其它对象,这种耦合性非常重,重用性很差。

观察者模式Observer,可以解决这种实际场景的,降低耦合性,提高重用性,对象之间,不需要建立直接的引用关系。

Observer描述了如何建立这种关系,关键对象是目标(subject)和观察者(Observer)

一个Subject目标,可有任意数目依赖于它的观察者Observer。

一旦目标的状态发生变化,所有的观察者都将得到通知并做出响应。

这种模式也叫publish-observer 发布订阅,publish发布就是上面的目标subject

当我们提到观察者模式的时候,首先要想到的两个重要对象:

目标Subject

观察者Observer

Subject可以有任意数目依赖于它的Observer,当Subject发生改变的时候,所有的Observer都会得到通知,并做出响应。

结构图:

image.png

Subject->基本类或是接口,包括了通用的Attach,Detach,Notify

Observer->是观察者的基类或是接口,包括了Update,具体对消息通知进行响应处理 。

ConcreteSubject->具体的Subject,继承自Subject,当发生改变时,会通知所有注册它的观察者。

concreteObserver->具体的观察者,注册到具体的Subject上,并接受Subject的改变通知,做出响应更新。

目标和观察者之间抽象耦合:

目标和观察者之间是抽象的耦合,即目标只知道他有一系列的观察者,这些观察者有着共同的基类或是接口,但目标并不知道这些观察者各自属于哪一个具体的类,这样,他们之间的耦合性是最小的。所谓的面向接口编程。

广播通知:

当目标发生改变的时候,会通知所有注册它的观察者,并不会指定某一个特定的观察者,目标并不关心有多少观察者对此感兴趣,目标在变化的时候,就会通知所有的观察者,至于观察者如何处理,这不是目标所关心的。

缺点:

1.意外的更新:一个观察进并不知道其它观察者的存在,那么当更新了其中一个观察者的时候,其它的观察者也会进行更新,但有时候这种操作并不是必要的,比如一个目标,可能有多达几十个观察者,所以在使用中,需要注意这种情况,比如可以进行更为细致的拆分,比如加入类型,只更新注册了对应类型的观察者即可,避免了所有观察者都进行更新。

当然,这里还会涉及到另外一个问题,就是无效监听Lapsed Listener,这会在后面提到。

2.Attach&Detach(现已废弃):

这是要成对出现的操作,如果我们进行了Attach操作,就必须要有对应的Detach,就像C/C++中那样,分配了内存,就必须进行释放,否则就会出现memory leak

(When you have registered a content observer, it is your responsibility to also unregister it. Otherwise you would create a memory leak and your Activity would never be garbage collected.

我们注册一个observer,就有义务去unregister反注册它,否则当前的对象就没有办法被垃圾回收器处理,产生memory leak!)

大话设计模式里关于Observer的解释是非棒的,从面向具体的双向耦合的实现到面向抽象解耦,再到通过C# Event实现方式,下面是自己从书中总结的一些笔记:

双向耦合的代码:

image.png
image.png
image.png

Secretary->Subject
StockObserver->Observer

Observer模式的思想已经实现了,但Secretary and StockObserver之间是相互耦合,强耦合,如果此时我再添加别的类型的Observer,那么Secretary类就必须做出修改,这种糟糕的设计,非常的不灵活,不健壮,并且也非违反了依赖倒置原则,应该在抽象的层面,相互依赖,而不是具体的类。

应用面向具体,设计要面向抽象!

那么从实现的角度,Secretary和StockObserver都应该是派生类,他们分别有自己的抽象基类Subject和Observer.

更好的实现版本:

image.png
image.png
image.png

在抽象层面,相互依赖,这样,如果我再添加其它的observer,只需继承observer基类实现即可,并不会影响到subject.

应用面向具体,设计要面向抽象。

但这种版本也是存在问题的,观察者一定得是Observer的子类吗,观察者可以是任何类,风马牛不相干的类,而且他们也不可能都被定义为 Update方法,容易产生歧义,
所以现在的Observer的实现方案基本上都是基于委托实现的。

基于委托的好处,我们不需要一个List来维护Observer,我们不能强制要求观察者都要继承自同一个父类,这在开发中不现实,更不可取,委托就可以很好的解决这个问题,委托是引用类型,是一个方法列表,这个列表中的方法只需要拥有相同的参数列表和返回值类型,就可以满足条件。

代码如下:

image.png
image.png
image.png

新增的Neymar类,和observer1,observer2是完全不相关的类。

观察者不再被要求必须拥有同一个父类,可以是风马牛不相干的任何类。

但在使用委托的过程中 ,就需要注意我们下面会提到的Lapsed Listener,不需要接受通知的方法要及时Remove掉。

3.Lapsed Listener:#

http://ilkinulas.github.io/development/general/2016/04/17/observer-pattern.html

摘取其中的一段:

So what can possibly go wrong?

In a runtime where memory is managed automatically like Java or C#, the subject holds a strong reference to the observers, keeping them alive. The strong reference from the subject (the observed object) to the observer, prevents the observer (and any objects it references) from being garbage collected until the observer is unregistered. A memory leak happens, if the observer fails to unregister from the subject when it no longer needs to receive notifications. In our countdown timer example if observers do not call unregister when they become invisible, they will receive notifications and waste CPU cycles updating invisible UI elements. This issue is called Lapsed Listener Problem.

If you implement the observer pattern in C++ (no garbage collection, memory is managed manually) and you call delete on one of the observers without unregistering it from the subject then you are in trouble. Now you have a dangling pointer that points to invalid data. Whenever the subject notifies its observers a segmentation fault will be raised.

Always unregister your listeners unless they are interested in receiving notifications.

Another approach for dealing with lapsed listeners (observers) is to use Weak References.

--

subject holds a strong reference to the observers:

subject会持有observers的强引用,保证他们一直处于活跃的状态,因为被强引用,如果不进行移除,那么observer就会阻止GC进行垃圾回收处理,直到你将observer从subject中移除,如果这个对象不再被使用了,那么他又无法被GC回收,这时候就产生了memory leak内存泄漏。

Lapsed Listener,当subject改变后,他会通知所有注册的observer,但有些observer在当前可能已经处于invisible的状态,比如UI中,当前状态,UI被隐藏了,那么即便是按收到通知,用户也是看不到的,从合理性的角度,也不应该处理,毕竟他已经处于隐藏的状态,这种就叫Lapsed Listener无效的监听,如果这样的observer有多个,就是在消耗CPU。

针对这种情况,当对象处于invisible时,我们应该手动移除observer,当对象重新处于visible时,我们再register注册它。

类似于下面这样的代码:

image.png

这是建议使用的方式,配对出现。

另外一种方式是采用WeakReference弱引用:

uses weak references to keep track of registered observers. If the only references to an instance are weak references, then the instance becomes a candidate for Garbage Collection. In contrast to strong references, weak references don’t prevent garbage collector to finilize the instance and reclaim its memory.

弱引用不会阻止垃圾回收器进行处理,当他不再会引用的时候,就会被垃圾回收器回收掉。

daling pointer悬空指针:

当我们移除Observer时,不要简单的直接的delete掉它,而是应该从当前的字典或是List移除。

因为Observer可能也注册了其它的Subject,在观察其它的Subject的改变通知,在C++这种没有垃圾回收机制,内存需要手动管理的语言中,直接删除Observer会产生悬空指针,即指向了一个不存在的地址,这样就会造成内存泄露,并且实际调用的时候,空指针,导致crush出现。

参考文献:

http://mark-dot-net.blogspot.com/2012/10/understanding-and-avoiding-memory-leaks.html

https://www.grokkingandroid.com/use-contentobserver-to-listen-to-changes/

https://www.jetbrains.com/help/dotmemory/How_to_Find_a_Memory_Leak.html

https://www.red-gate.com/simple-talk/dotnet/.net-framework/understanding-garbage-collection-in-.net/

http://www.andymcm.com/dotnetfaq.htm#5.8

http://ilkinulas.github.io/development/general/2016/04/17/observer-pattern.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容