天天看點

娜樣美的觀察者模式1. 定義2. 場景實作3. 觀察者模式的優勢3. 應用執行個體4. 更多資料

觀察者模式,是一種異常美麗的設計模式。本文包括定義、類圖以及應用執行個體,例子采用 Java 實作。

1. 定義

觀察者模式又稱訂閱者模式(可以将其看做報紙訂閱服務,有兩個主要角色,出版商和訂閱者。假設出版商初版的雜志是周刊,那麼一旦新雜志上架,出版商就将其郵寄給一個或多個訂閱者)。

觀察者模式在生活中每天都在上演。

快遞小哥蹲在一家機關門口,被滿地的包裹圍攻,氣定神閑。小哥咬了一口雞蛋灌餅,對着手機說:“你好,你有快遞到了……”

包裹被簽收後,小哥将收件人從便簽中劃掉。

我們來簡單分析一下這個場景。首先是人物:快遞小哥一枚,收件人多隻。那麼,是什麼将小哥和收件人聯系起來的呢?是手機号,即小哥擁有所有收件人的手機号,以便通知收件人來領包裹——收件人事先在快遞小哥那裡注冊了自己的資訊(手機号)。

娜樣美的觀察者模式1. 定義2. 場景實作3. 觀察者模式的優勢3. 應用執行個體4. 更多資料

你站在橋上看風景

換言之,快遞小哥是“主題”(Subject),收件人是觀察者(Observer)。收件人向快遞小哥注冊自己的資訊(實際上是在下訂單時寫的,然後由電商轉給快遞公司,再由快遞公司分發給快遞小哥,我們簡化這一流程為收件人直接給快遞小哥),然後等待快遞小哥的通知,以便做出相應的行動。

言歸正傳,觀察者模式由2部分組成:主題/Subject,觀察者/Observer,Observer 在 Subject 中注冊,然後等待 Subject 的通知。

《Head First 設計模式》下的定義:

觀察者模式定義了對象之間的一對多依賴,當一個對象的狀态發生改變時,它的所有依賴者都會收到通知并自動更新。
娜樣美的觀察者模式1. 定義2. 場景實作3. 觀察者模式的優勢3. 應用執行個體4. 更多資料

類圖

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。

娜樣美的觀察者模式1. 定義2. 場景實作3. 觀察者模式的優勢3. 應用執行個體4. 更多資料

正常情況下,為了在 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 也是使用了觀察者模式的。

實作效果如下:

娜樣美的觀察者模式1. 定義2. 場景實作3. 觀察者模式的優勢3. 應用執行個體4. 更多資料

代碼如下(代碼中我們使用了安卓提供的 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 使用經驗總結(妹妹篇)》