觀察者模式,是一種異常美麗的設計模式。本文包括定義、類圖以及應用執行個體,例子采用 Java 實作。
1. 定義
觀察者模式又稱訂閱者模式(可以将其看做報紙訂閱服務,有兩個主要角色,出版商和訂閱者。假設出版商初版的雜志是周刊,那麼一旦新雜志上架,出版商就将其郵寄給一個或多個訂閱者)。
觀察者模式在生活中每天都在上演。
快遞小哥蹲在一家機關門口,被滿地的包裹圍攻,氣定神閑。小哥咬了一口雞蛋灌餅,對着手機說:“你好,你有快遞到了……”
包裹被簽收後,小哥将收件人從便簽中劃掉。
我們來簡單分析一下這個場景。首先是人物:快遞小哥一枚,收件人多隻。那麼,是什麼将小哥和收件人聯系起來的呢?是手機号,即小哥擁有所有收件人的手機号,以便通知收件人來領包裹——收件人事先在快遞小哥那裡注冊了自己的資訊(手機号)。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZwpmLxIjNJplQflUNOd2QBFkZBNXcG1WUzBnTsxUeVJkcvwVQG9CXFFzLcBDMN9CXy8GdvhGcvw1b09GawZ2Lc12bj5yYpBnbvlGdlZmLwdWbp1Savw1LcpDc0RHaiojIsJye.jpg)
你站在橋上看風景
換言之,快遞小哥是“主題”(Subject),收件人是觀察者(Observer)。收件人向快遞小哥注冊自己的資訊(實際上是在下訂單時寫的,然後由電商轉給快遞公司,再由快遞公司分發給快遞小哥,我們簡化這一流程為收件人直接給快遞小哥),然後等待快遞小哥的通知,以便做出相應的行動。
言歸正傳,觀察者模式由2部分組成:主題/Subject,觀察者/Observer,Observer 在 Subject 中注冊,然後等待 Subject 的通知。
《Head First 設計模式》下的定義:
觀察者模式定義了對象之間的一對多依賴,當一個對象的狀态發生改變時,它的所有依賴者都會收到通知并自動更新。
類圖
2. 場景實作
我們用 Java 來實作拿快遞這一場景。
2.1 Subject
我們将 Subject 定義為接口,接口中有3個行為:注冊觀察者,移除觀察者,通知觀察者。分别對應方法:registerObservers(),removeObservers(),removeObservers()。
快遞小哥實作該接口,則具有并對外公開這3個行為,并按照自己的行為具體實作之,而具體實作内容則對外隐藏,更改這些方法的具體實作不影響其他對象的調用形式。
Subject.java
public interface Subject {
public void registerObservers(Observer o);
public void removeObservers(Observer o);
public void removeObservers();
}
public class ExpressMan implements Subject {
List<Observer> observerList;
public ExpressMan() {
this.observerList = new ArrayList<>();
}
@Override
public void registerObservers(Observer o) {
observerList.add(o);
}
@Override
public void removeObservers(Observer o) {
int i = observerList.indexOf(o);
if (i > ) {
observerList.remove(i);
}
}
@Override
public void notifyObservers() {
for (Observer o : observerList) {
o.update();
}
}
}
2.2 Observer
Observer 接口隻有1個行為:update()。觀察者實作該接口,在該方法中實作自己的 update() 行為,如對于收件人 update() 則是暫停工作去取快遞。
public interface Observer {
void update();
}
public class Recipient implements Observer{
Subject subject;
public Recipient(Subject _subject) {
this.subject = _subject;
subject.registerObservers(this);
}
@Override
public void update() {
// 暫停工作,去取快遞
}
}
2.3 主函數
public class Main {
public static void main(String[] args) {
Subject Zhansan = new ExpressMan();
Observer Lisi = new Recipient(Zhansan);
Observer Wangwu = new Recipient(Zhansan);
Observer Chenliu = new Recipient(Zhansan);
Zhansan.notifyObservers();
}
}
3. 觀察者模式的優勢
之是以說觀察者模式那樣美,是因為這種模式實作了兩組對象的松耦合,它們隻知道對方實作了什麼接口,具有什麼的樣行為,而對其中的實作細節并不了解。
我們再舉個具體的例子來說明觀察者模式是如何解耦的。下圖是美團安卓 APP(v6.8.1) 的個人中心頁,圖中示範的是匿名評價功能。我們假設“匿名評價”這個 TextView 和“更多按鈕”的 ImageButton(假設是個 ImageButton)分别屬于兩個元件 FeedItemViewModel 和 FeedCommentViewModel。
正常情況下,為了在 FeedCommentViewModel 中對 FeedItemViewModel 中的“匿名評價”這個 TextView 進行更新,必然會在 FeedCommentViewModel 中持有 FeedItemViewModel 的引用,這就造成了耦合。
如果我們想實作兩個元件解耦,即任意一個元件都可以單獨拿出去使用,我們可以借助 JDK 提供的 基類 Observable 和 接口 Observer。
FeedItemViewModel 是訂閱者,實作 Observer 接口,重寫 update(Observable observable, Object data) 方法,在方法體中實作更新“匿名評價”的操作;
FeedCommentViewModel 是釋出者,繼承 Observable,通過
addObserver(Observer o)
方法注冊訂閱者,通過
notifyObservers(Object data)
通知訂閱者更新。
為了簡單起見,我們使用 DataBinding 技術實作該功能,關于 DataBinding 更多的内容請參考《安卓 DataBinding 使用經驗總結(姐姐篇)》。
代碼中接口 BaseObservable 和
notifyOnPropertyChanged(BR.id)
是 DataBinding 相關的内容。由此可以看出 DataBinding 也是使用了觀察者模式的。
實作效果如下:
代碼如下(代碼中我們使用了安卓提供的 DataBinding 技術):
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
FeedViewModel model = new FeedViewModel();
binding.setUser(model);
}
}
FeedItemViewModel.java
public class FeedViewModel extends BaseObservable implements Observer {
public @Bindable String firstName;
public FeedCommentViewModel mFeedCommentViewModel;
public FeedViewModel() {
firstName = "mmlovesyy";
mFeedCommentViewModel = new FeedCommentViewModel(this);
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(com.mmlovesyy.java_observable.BR.firstName);
}
@Override
public void update(Observable observable, Object data) {
setFirstName((String) data);
}
}
FeedCommentViewModel.java
public class FeedCommentViewModel extends Observable {
public FeedCommentViewModel(Observer observer) {
addObserver(observer);
}
public void onClick(View view) {
setChanged();
notifyObservers("mm***yy");
}
}
main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user"
type="com.mmlovesyy.java_observable.FeedViewModel" />
</data>
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.mmlovesyy.java_observable.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.firstName}'
android:textColor="#ff0000" />
<include
layout="@layout/activity_comment"
bind:comment='@{user.mFeedCommentViewModel}' />
</LinearLayout>
</layout>
activity_comment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="comment"
type="com.mmlovesyy.java_observable.FeedCommentViewModel" />
</data>
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.mmlovesyy.java_observable.MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick='@{comment.onClick}'
android:text="匿名評價" />
</LinearLayout>
</layout>
開發中我們會用到 ItemView 和其對應的 ItemModel,如果讓 ItemView 觀察 ItemModel,那麼 ItemModel 中的資料更新時,ItemView 不就自動更新了嗎?看,我們實作了一個簡化版的 Data Binding!
3. 應用執行個體
3.0 JDK 自帶 Observer & Observable
在上一節匿名評價的例子中,我們使用了 JDK 自帶的 java.util.Observer 和 java.util.Observable。
應當注意的是,Observable 是一個類,而非接口。由于 Java 的單繼承性,隻能繼承一個父類,卻可以實作多個接口,是以當某個類已經繼承了父類,就不能再繼承 Observable 類了,這是不太友善的地方。
同時,這個不太友善的地方告訴我們:如果可以,優先使用接口,而非基類。
3.1 BaseAdapter
使用安卓的 ListView 或 RecyclerView 的時候,BaseAdapter 是無法回避的。
BaseAdapter 裡面也有一對觀察者模式組合:DataSetObserver & DataSetObservable。并且提供了一系列的注冊/登出/通知的方法:
private final DataSetObservable mDataSetObservable = new DataSetObservable();
public boolean hasStableIds() {
return false;
}
public void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mDataSetObservable.unregisterObserver(observer);
}
/**
* Notifies the attached observers that the underlying data has been changed
* and any View reflecting the data set should refresh itself.
*/
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
如果 ListView / RecyclerView 内部有地方(如 headerView 或 footerView),或者 ListView / RecyclerView 外部有地方(如 ListView / RecyclerView 所在的 Fragment / Activity 内的某個 View),要監聽 BaseAdapter 資料的變化,則可以通過注冊 DataSetObserver 的方法來做到。
需要注意的是,這種方法隻能監聽資料的變化,而無法區分資料的增加或減少等細節。
3.2 BroadcastReceiver
3.3 RxJava
3.4 Data Binding
請參考我的另外兩篇部落格:《安卓 DataBinding 使用經驗總結(姐姐篇)》 和 《安卓 DataBinding 使用經驗總結(妹妹篇)》。
3.5 EventBus
3.6 OnClickListener
以 View.OnClickListener 為首的各種 Listener,也屬于觀察者模式。隻不過是簡化版的觀察這模式,因為隻有 1 個觀察者,即 1 個 Observer。此時,register 方法被 setter 方法所取代 。
4. 更多資料
- 《安卓 DataBinding 使用經驗總結(姐姐篇)》
- 《安卓 DataBinding 使用經驗總結(妹妹篇)》