觀察者模式(
Observer Pattern
)是對(duì)象之間一對(duì)多的依賴關(guān)系牙寞,當(dāng)一個(gè)對(duì)象改變時(shí),其他依賴它的對(duì)象都會(huì)收到通知并自動(dòng)更新蝙云。
怎么來(lái)理解這句話呢酸员?用微信朋友圈來(lái)舉個(gè)例子,假如你就是被依賴的對(duì)象河胎,你的好友都依賴你闯袒,這樣的關(guān)系就形成了一對(duì)多,當(dāng)你發(fā)朋友圈的時(shí)候你的好友都能收到通知游岳,并且自動(dòng)更新政敢。
觀察者模式是一個(gè)比較簡(jiǎn)單的模式,它的核心很簡(jiǎn)單胚迫,只有兩個(gè)角色喷户,一個(gè)是被依賴的對(duì)象,在朋友圈例子里面就是你自己访锻,這個(gè)叫主題或者被觀察者
褪尝,另一個(gè)對(duì)象是你的朋友叫觀察者或者訂閱者
,為了統(tǒng)一認(rèn)知期犬,后面都叫主題(被觀察者
)和訂閱者(觀察者
)河哑。
類圖
類圖不是目的,只是方便理解
[圖片上傳失敗...(image-538efc-1527174205892)]
從類圖上可以看到龟虎,主題不關(guān)心其他訂閱者的實(shí)現(xiàn)璃谨,只關(guān)心Observer
接口,所有實(shí)現(xiàn)了Observer
接口并訂閱了主題的對(duì)象都能在主題發(fā)生變化的時(shí)候得到通知遣总。
實(shí)例
現(xiàn)在來(lái)實(shí)現(xiàn)一個(gè)汽車儀表盤睬罗,汽車在行駛過(guò)程中轉(zhuǎn)速和速度都會(huì)一直處于變化中轨功,我們現(xiàn)在通過(guò)觀察者模式把轉(zhuǎn)速和速度顯示到儀表盤上。
首先容达,模擬一輛汽車從0-100加速的過(guò)程古涧,這個(gè)過(guò)程中拿到汽車把實(shí)時(shí)數(shù)據(jù):
/**
* 汽車回調(diào)數(shù)據(jù),這里會(huì)根據(jù)汽車的速度變化花盐,持續(xù)的傳遞轉(zhuǎn)速和速度
*/
fun carInfo(power:Float, speed: Float)
{
}
/**
* 汽車引擎羡滑,模擬汽車從0-100加速
*/
for (speed in 0..1000)
{
Thread.sleep(10)
carInfo(speed/200f + Random().nextInt(2), speed.toFloat()/10)
}
// 部分汽車數(shù)據(jù)打印
power: 0.0 speed: 0.0
power: 0.005 speed: 0.1
power: 0.01 speed: 0.2
power: 0.015 speed: 0.3
power: 0.02 speed: 0.4
power: 1.025 speed: 0.5
power: 1.03 speed: 0.6
power: 1.035 speed: 0.7
power: 0.04 speed: 0.8
power: 0.045 speed: 0.9
power: 0.05 speed: 1.0
power: 1.055 speed: 1.1
既然要顯示到儀表盤,現(xiàn)在還差一個(gè)儀表盤Display
用于顯示速度和轉(zhuǎn)速:
/**
* 儀表盤
* Created by Carlton on 2016/11/9.
*/
class Display
{
var power:Float = 0f
var speed:Float = 0f
fun display()
{
println("汽車當(dāng)前的 轉(zhuǎn)速:$power 速度:$speed")
}
}
現(xiàn)在我們把速度變化數(shù)據(jù)通過(guò)儀表盤Display
展示出來(lái):
val display = Display()
/**
* 汽車回調(diào)數(shù)據(jù)算芯,這里會(huì)根據(jù)汽車的速度變化柒昏,持續(xù)的傳遞轉(zhuǎn)速和速度
*/
fun carInfo(power:Float, speed: Float)
{
display.power = power
display.speed = speed
display.display()
}
// 儀表盤數(shù)據(jù)
……
汽車當(dāng)前的 轉(zhuǎn)速:3.985 速度:79.7
汽車當(dāng)前的 轉(zhuǎn)速:4.99 速度:79.8
汽車當(dāng)前的 轉(zhuǎn)速:3.995 速度:79.9
汽車當(dāng)前的 轉(zhuǎn)速:4.0 速度:80.0
汽車當(dāng)前的 轉(zhuǎn)速:5.005 速度:80.1
汽車當(dāng)前的 轉(zhuǎn)速:5.01 速度:80.2
汽車當(dāng)前的 轉(zhuǎn)速:4.015 速度:80.3
汽車當(dāng)前的 轉(zhuǎn)速:4.02 速度:80.4
汽車當(dāng)前的 轉(zhuǎn)速:5.025 速度:80.5
汽車當(dāng)前的 轉(zhuǎn)速:5.03 速度:80.6
……
現(xiàn)在我們就實(shí)現(xiàn)了一個(gè)簡(jiǎn)易的汽車儀表盤展示數(shù)據(jù),這樣寫(xiě)有什么問(wèn)題呢熙揍?如果我們給汽車擴(kuò)展一個(gè)后視鏡顯示速度职祷,中控臺(tái)展示速度,我們又需要來(lái)修改carInfo()
去設(shè)置和顯示后視鏡届囚、中控臺(tái)有梆,顯然不符合設(shè)計(jì)原則。
現(xiàn)在我們知道整個(gè)系統(tǒng)中有兩個(gè)角色:汽車變化的數(shù)據(jù)意系、儀表盤泥耀。按照觀察者模式,把汽車變化的數(shù)據(jù)定義成主題(被觀察者
)蛔添,儀表盤定義訂閱者(觀察者
)痰催,然后用觀察者模式重構(gòu)整個(gè)系統(tǒng)。
首先迎瞧,實(shí)現(xiàn)觀察者接口:
/**
* 主題夸溶,有的地方叫觀察者Observable
* @param T 更新的數(shù)據(jù)回調(diào)
* Created by Carlton on 2016/11/9.
*/
interface Subject<T>
{
/**
* 注冊(cè)成為觀察者
*/
fun registeObserver(observer: Observer<T>)
/**
* 刪除觀察者
*/
fun removeObserver(observer: Observer<T>)
/**
* 通知觀察者數(shù)據(jù)已經(jīng)發(fā)生了變化
*/
fun notifyObservers(value: T)
}
/**
* 觀察者
* @param T 觀察者回調(diào)的數(shù)據(jù)類型
* Created by Carlton on 2016/11/9.
*/
interface Observer<in T>
{
/**
* 數(shù)據(jù)更新
*/
fun update(value: T)
}
接下來(lái)把數(shù)據(jù)封裝成一個(gè)主題(被觀察者
)CarSubject
:
/**
* 具體的主題,被觀察者
* Created by Carlton on 2016/11/9.
*/
class CarSubject : Subject<Array<Float>>
{
/**
* 觀察者
*/
val observers = ArrayList<Observer<Array<Float>>>()
override fun registerObserver(observer: Observer<Array<Float>>)
{
observers.add(observer)
}
override fun removeObserver(observer: Observer<Array<Float>>)
{
if(observers.contains(observer))
{
observers.remove(observer)
}
}
override fun notifyObservers(value: Array<Float>)
{
for (observer in observers)
{
observer.update(value)
}
}
}
現(xiàn)在改造一下我們的系統(tǒng)夹攒,把數(shù)據(jù)用主題綁定起來(lái):
val carSubject = CarSubject()
/**
* 汽車回調(diào)數(shù)據(jù)蜘醋,這里會(huì)根據(jù)汽車的速度變化,持續(xù)的傳遞轉(zhuǎn)速和速度
*/
fun carInfo(power:Float, speed: Float)
{
carSubject.notifyObservers(arrayOf(power, speed))
}
到這里我們實(shí)現(xiàn)了一個(gè)可擴(kuò)展的觀察者模式系統(tǒng)咏尝,觀察者模式中的接口部分一般都是固定的压语,包括java里面都有支持觀察者模式,后面會(huì)說(shuō)道编检,所以如果我們要實(shí)現(xiàn)一個(gè)觀察者模式胎食,接口部分我們只需要實(shí)現(xiàn)一次,或者直接使用java api提供的接口允懂,主要需要實(shí)現(xiàn)主題或者觀察者接口厕怜。
在上面的例子中,我們自己提供了觀察者接口Subject
、Observer
粥航,接著我們實(shí)現(xiàn)了一個(gè)主題CarSubject
用于封裝汽車變化的數(shù)據(jù)琅捏,提供給其他對(duì)這個(gè)數(shù)據(jù)感興趣的觀察者們。那么递雀,現(xiàn)在把儀表盤做為觀察者柄延,去訂閱主題,修改一下之前的Display
:
/**
* 儀表盤缀程,傳遞的數(shù)據(jù)是一個(gè)數(shù)組搜吧,0下標(biāo)存的是轉(zhuǎn)速,1下標(biāo)存的是速度
* Created by Carlton on 2016/11/9.
*/
class Display : Observer<Array<Float>>
{
override fun update(value: Array<Float>)
{
power = value[0]
speed = value[1]
display()
}
var power:Float = 0f
var speed:Float = 0f
fun display()
{
println("汽車當(dāng)前的 轉(zhuǎn)速:$power 速度:$speed")
}
}
運(yùn)行系統(tǒng):
val carSubject = CarSubject()
// 添加儀表盤觀察者
carSubject.registerObserver(Display())
// 儀表盤顯示
……
汽車當(dāng)前的 轉(zhuǎn)速:4.525 速度:90.5
汽車當(dāng)前的 轉(zhuǎn)速:4.53 速度:90.6
汽車當(dāng)前的 轉(zhuǎn)速:4.535 速度:90.7
汽車當(dāng)前的 轉(zhuǎn)速:5.54 速度:90.8
汽車當(dāng)前的 轉(zhuǎn)速:5.545 速度:90.9
汽車當(dāng)前的 轉(zhuǎn)速:4.55 速度:91.0
汽車當(dāng)前的 轉(zhuǎn)速:4.555 速度:91.1
汽車當(dāng)前的 轉(zhuǎn)速:4.56 速度:91.2
汽車當(dāng)前的 轉(zhuǎn)速:4.565 速度:91.3
……
如果現(xiàn)在儀表盤不需要監(jiān)聽(tīng)主題的數(shù)據(jù)了杨凑,可以調(diào)用
carSubject.removeObserver()
移除對(duì)象滤奈,這樣主題數(shù)據(jù)發(fā)生變化后,就不會(huì)通知到這個(gè)觀察者對(duì)象撩满。
接下來(lái)蜒程,添加中控臺(tái)和后視鏡的數(shù)據(jù)顯示,把中控臺(tái)和后視鏡當(dāng)成觀察者去訂閱CarSubject
伺帘。
這里解答一個(gè)疑惑搞糕,為什么觀察者去訂閱主題,反而要把訂閱和移除訂閱的方法放到主題里面而不是觀察者里面曼追,這樣也很好理解啊汉规?主要原因是現(xiàn)實(shí)世界和程序世界還是有區(qū)別礼殊,如果我們把這兩個(gè)方法按照現(xiàn)實(shí)的理解放到觀察者里面,代碼會(huì)變得比較復(fù)雜针史,沒(méi)有現(xiàn)在這種實(shí)現(xiàn)方式簡(jiǎn)單明確晶伦,有興趣的可以自己去按照現(xiàn)實(shí)的理解方式實(shí)現(xiàn)一個(gè)。設(shè)計(jì)模式只是一種編程思想啄枕,不是編程的形式婚陪,理解到思想就行了。
新添加兩個(gè)觀察者频祝,中控臺(tái)(CenterConsoleDisplay
)泌参、后視鏡(RearviewBack
):
/**
* 中控臺(tái)
* Created by Carlton on 2016/11/9.
*/
class CenterConsoleDisplay : Observer<Array<Float>>
{
override fun update(value: Array<Float>)
{
println("中控臺(tái)顯示的速度:${value[1]}")
}
}
/**
* 后視鏡
* Created by Carlton on 2016/11/9.
*/
class RearviewBack : Observer<Array<Float>>
{
override fun update(value: Array<Float>)
{
println("后視鏡顯示: 速度 - ${value[1]} 轉(zhuǎn)速 - ${value[0]}")
}
}
訂閱這兩個(gè)新的,啟動(dòng):
val carSubject = CarSubject()
// 添加儀表盤觀察者
carSubject.registerObserver(Display())
// 添加中控臺(tái)
carSubject.registerObserver(CenterConsoleDisplay())
// 添加后視鏡
carSubject.registerObserver(RearviewBack())
// 各個(gè)地方的數(shù)據(jù)展示
……
中控臺(tái)顯示的速度:98.9
后視鏡顯示: 速度 - 98.9 轉(zhuǎn)速 - 4.945
汽車當(dāng)前的 轉(zhuǎn)速:4.95 速度:99.0
中控臺(tái)顯示的速度:99.0
后視鏡顯示: 速度 - 99.0 轉(zhuǎn)速 - 4.95
汽車當(dāng)前的 轉(zhuǎn)速:4.955 速度:99.1
中控臺(tái)顯示的速度:99.1
后視鏡顯示: 速度 - 99.1 轉(zhuǎn)速 - 4.955
汽車當(dāng)前的 轉(zhuǎn)速:5.96 速度:99.2
中控臺(tái)顯示的速度:99.2
后視鏡顯示: 速度 - 99.2 轉(zhuǎn)速 - 5.96
……
數(shù)據(jù)的推和拉
觀察者模式中獲取數(shù)據(jù)的方式有兩種常空,一種是推給觀察者沽一,一種是觀察者根據(jù)需要自己拉,有什么區(qū)別呢漓糙?如果是推的方式不管觀察者對(duì)這部分信息是否感興趣都會(huì)推給觀察者铣缠,有冗余數(shù)據(jù)比如我們的中控臺(tái)(CenterConsoleDisplay
)只對(duì)速度感興趣。如果是用拉的方式呢?這樣就能根據(jù)觀察者自己的需要獲取想要的數(shù)據(jù)蝗蛙,Java里面兩種方式都支持蝇庭,下面改造一下通過(guò)拉的方式實(shí)現(xiàn)數(shù)據(jù)傳遞,這樣的話CarSubject
也就是主題需要暴露一些獲取數(shù)據(jù)的方法:
/**
* 觀察者
* @param T 觀察者回調(diào)的數(shù)據(jù)類型
* Created by Carlton on 2016/11/9.
*/
interface Observer<T>
{
/**
* 數(shù)據(jù)更新
*/
fun update(value: T)
// ------------------ 變化的部分 --------------------------
/**
* 重載一個(gè)方法捡硅,讓觀察者可以用拉的方式獲取數(shù)據(jù)
*/
fun update(subject: Subject<T>)
// ------------------ 變化的部分 --------------------------
}
/**
* 主題哮内,有的地方叫觀察者Observable
* @param T 更新的數(shù)據(jù)回調(diào)
* Created by Carlton on 2016/11/9.
*/
interface Subject<T>
{
/**
* 注冊(cè)成為觀察者
*/
fun registerObserver(observer: Observer<T>)
/**
* 刪除觀察者
*/
fun removeObserver(observer: Observer<T>)
/**
* 通知觀察者數(shù)據(jù)已經(jīng)發(fā)生了變化
*/
fun notifyObservers(value: T)
// ------------------ 變化的部分 --------------------------
/**
* 通知觀察者數(shù)據(jù)已經(jīng)發(fā)生了變化,可以拉數(shù)據(jù)了
*/
fun notifyObservers()
// ------------------ 變化的部分 --------------------------
}
/**
* 具體的主題病曾,被觀察者
* Created by Carlton on 2016/11/9.
*/
class CarSubject : Subject<Array<Float>>
{
// ------------------ 變化的部分 --------------------------
var speed:Float = 0f
var power:Float = 0f
override fun notifyObservers()
{
for (observer in observers)
{
observer.update(this)
}
}
// ------------------ 變化的部分 --------------------------
/**
* 觀察者
*/
val observers = ArrayList<Observer<Array<Float>>>()
override fun registerObserver(observer: Observer<Array<Float>>)
{
observers.add(observer)
}
override fun removeObserver(observer: Observer<Array<Float>>)
{
if(observers.contains(observer))
{
observers.remove(observer)
}
}
override fun notifyObservers(value: Array<Float>)
{
for (observer in observers)
{
observer.update(value)
}
}
}
/**
* 中控臺(tái)
* Created by Carlton on 2016/11/9.
*/
class CenterConsoleDisplay : Observer<Array<Float>>
{
// ------------------ 變化的部分 --------------------------
override fun update(subject: Subject<Array<Float>>)
{
val carSubject:CarSubject = subject as CarSubject
println("中控臺(tái)顯示的速度:${carSubject.speed}")
}
// ------------------ 變化的部分 --------------------------
override fun update(value: Array<Float>)
{
println("中控臺(tái)顯示的速度:${value[1]}")
}
}
/**
* 汽車回調(diào)數(shù)據(jù)牍蜂,這里會(huì)根據(jù)汽車的速度變化,持續(xù)的傳遞轉(zhuǎn)速和速度
*/
fun carInfo(power:Float, speed: Float)
{
// ------------------ 變化的部分 --------------------------
carSubject.power = power
carSubject.speed = speed
carSubject.notifyObservers()
// ------------------ 變化的部分 --------------------------
carSubject.notifyObservers(arrayOf(power, speed))
}
更靈活的設(shè)計(jì)
不知道大家發(fā)現(xiàn)一個(gè)問(wèn)題沒(méi)有泰涂,打印出來(lái)的速度和轉(zhuǎn)速都是有小數(shù)鲫竞,正常情況下速度表變化都是整數(shù)每次變化為1,為了說(shuō)明簡(jiǎn)單的用速度和轉(zhuǎn)速大于2的時(shí)候才通知觀察者來(lái)代替這個(gè)需求逼蒙。如果我們要處理這個(gè)問(wèn)題可以在觀察者對(duì)象中拿到數(shù)據(jù)后處理从绘,不過(guò)觀察者模式有一個(gè)比較優(yōu)雅的處理方式setChanged()
,有什么用呢是牢?用來(lái)標(biāo)記數(shù)據(jù)是否發(fā)生了不變化如果沒(méi)有發(fā)生變化則不通知觀察者僵井,這樣我們就能控制何時(shí)通知觀察者,現(xiàn)在改造一下CarSubject
驳棱,并在Subject中新增接口setChanged()
:
/**
* 具體的主題批什,被觀察者
* Created by Carlton on 2016/11/9.
*/
class CarSubject : Subject<Array<Float>>
{
var speed:Float = 0f
var power:Float = 0f
override fun notifyObservers()
{
if (!isChanged)
{
return
}
for (observer in observers)
{
observer.update(this)
}
isChanged = false
}
/**
* 數(shù)據(jù)是否發(fā)生變化
*/
var isChanged: Boolean = false
override fun setChanged()
{
isChanged = true
}
/**
* 觀察者
*/
val observers = ArrayList<Observer<Array<Float>>>()
override fun registerObserver(observer: Observer<Array<Float>>)
{
observers.add(observer)
}
override fun removeObserver(observer: Observer<Array<Float>>)
{
if(observers.contains(observer))
{
observers.remove(observer)
}
}
override fun notifyObservers(value: Array<Float>)
{
if (!isChanged)
{
return
}
for (observer in observers)
{
observer.update(value)
}
isChanged = false
}
}
現(xiàn)在在通知觀察者之前我們必須設(shè)置數(shù)據(jù)更新標(biāo)志調(diào)用setChanged()
,這里我們讓轉(zhuǎn)速和速度都大于2的時(shí)候才通知觀察者:
/**
* 汽車回調(diào)數(shù)據(jù)社搅,這里會(huì)根據(jù)汽車的速度變化驻债,持續(xù)的傳遞轉(zhuǎn)速和速度
*/
fun carInfo(power:Float, speed: Float)
{
carSubject.power = power
carSubject.speed = speed
// 這里我們可以讓速度和轉(zhuǎn)速大于2的時(shí)候才通知觀察者
if (carSubject.power > 2 && carSubject.speed > 2)
{
carSubject.setChanged()
}
carSubject.notifyObservers()
carSubject.notifyObservers(arrayOf(power, speed))
}
Java里面的觀察者
Java里面提供了一個(gè)被觀察者類:java.util.Observable
這不是一個(gè)接口,里面有具體的實(shí)現(xiàn)形葬,跟我們的CarSubject
一樣合呐,已經(jīng)做好了添加觀察者、重置標(biāo)志符等功能笙以,需要的時(shí)候直接繼承淌实。還有一個(gè)java.util.Observer
觀察者接口,跟我們上面的是一樣的猖腕。Java把java.util.Observable
定義成一個(gè)類拆祈,主要是流程化了主題功能,不像用接口的時(shí)候需要自己實(shí)現(xiàn)添加觀察者等功能谈息,這樣也有一個(gè)問(wèn)題就是擴(kuò)展性變差了缘屹,因?yàn)榻涌诳偸潜阮愐`活。如果用Java自帶的觀察者API來(lái)實(shí)現(xiàn)我們的系統(tǒng)侠仇,只需要用CarSubject
來(lái)繼承java.util.Observable
就可以了轻姿,CarSubject
中就不需要再去實(shí)現(xiàn)這些方法了犁珠,因?yàn)楦割愐呀?jīng)實(shí)現(xiàn)好了:
/**
* 注冊(cè)成為觀察者
*/
fun registerObserver(observer: Observer<T>)
/**
* 刪除觀察者
*/
fun removeObserver(observer: Observer<T>)
/**
* 通知觀察者數(shù)據(jù)已經(jīng)發(fā)生了變化
*/
fun notifyObservers(value: T)
/**
* 通知觀察者數(shù)據(jù)已經(jīng)發(fā)生了變化,可以拉數(shù)據(jù)了
*/
fun notifyObservers()
/**
* 標(biāo)記數(shù)據(jù)變化
*/
fun setChanged()
MVC說(shuō)幾句
MVC有很多實(shí)現(xiàn)方式互亮,但是用觀察者模式實(shí)現(xiàn)是我覺(jué)得最好用的方式犁享,我們把V想成訂閱者,把M實(shí)現(xiàn)成主題豹休,這樣的話炊昆,當(dāng)我們的Model
中有數(shù)據(jù)變化的時(shí)候就可以直接通知到View
,讓View
用數(shù)據(jù)來(lái)更新界面威根,這樣寫(xiě)出來(lái)的架構(gòu)更容易理解和解耦凤巨。
總結(jié)
觀察者模式主要有兩個(gè)角色,一個(gè)是觀察者洛搀,一個(gè)是被觀察者敢茁,所有觀察者都能夠收到被觀察者數(shù)據(jù)變化的通知,如果某個(gè)觀察者不在關(guān)心主題的數(shù)據(jù)了留美,也可以從被觀察者的列表中刪除這個(gè)觀察者彰檬,這樣它就不會(huì)收到通知了。如果使用Java谎砾,沒(méi)有特別的需求情況下逢倍,不建議自己實(shí)現(xiàn)觀察者接口,而是直接使用Java API景图。觀察者有很多可以應(yīng)用的地方较雕,非常有用的一種編程思路,比如RxJava里面等等很多框架都有觀察者模式挚币。觀察者模式在項(xiàng)目中是經(jīng)常使用的一種模式郎笆,當(dāng)明白它的核心思想后,在項(xiàng)目中能幫助我們實(shí)現(xiàn)更好維護(hù)的代碼忘晤。
不登高山,不知天之高也激捏;不臨深溪设塔,不知地之厚也
感謝指點(diǎn)、交流远舅、喜歡