Observer 觀察者模式 | JDK Observer | Android Framework Listview

Observer 觀察者模式 | JDK Observer | Android Framework Listview

Overview of Content

如有引用參考請詳註出處,感謝 😀

本文研究Observer設計模式,Observer模式在UML中的具體呈現,以及深入挖掘其設計所帶來的優勢和限制。我們將探討Observer模式的實際應用,並詳細介紹標準的Observer實現方式。

特別值得一提的是,我們將解析 JDK 內建的觀察者模式,深入探討如何實作並發揮其功能。

同時,我們將聚焦於Android源碼,分析 Observer 模式在 Android 開發中的實際應用情境,特別是在 ListView 的 BaseAdapter 中,以及 ListView 本身作為觀察者的角色。透過這些實例,讀者將更深入理解 Observer 設計模式的實際運用與優越性。


Observer 使用場景

● 當一個狀態或是變數改變時,需要通知所有擁有它這個變數的物件 (就像是有多人訂閱 "Hello" 日報,如果日報訂閱價格改變,就必須通知所有已訂閱的用戶)

● 定義一個可觀察物件,讓可觀察物件與其他物件產生 一對多的依賴關係,並在刷新時通知其他物件

關聯關係是 可拆分 的,並非組合關係

Observer UML

● Observer 角色介紹

角色功能說明
Observer觀察者抽象,定義一個更新方法
ConcreteObserver實作一個更新方法的細節
Subject聚合 Observer,並宣告抽象通知方法
ConcreteSubject實作抽象通知方法

● 建議在被觀察者(Subject)要通知前,就做出是否通知的判斷,不要延到觀察者(Observer)判斷是否消費這個事件

減少 Observer 的個別判斷邏輯

Observer 設計:優缺點

Observer 設計優點 :

A. 觀察者與被觀察者,是抽象耦合,對應業務變化

B. 增強系統靈活性、可擴充、自由度

C. 可以建立一套觸發機制(鏈式調用)

Observer 設計缺點 :

A. 考慮到執行效率問題,由於是 輪巡更新,所以當一個觀察者卡頓 (重量級) 會導致後方的資料更新也卡頓

可以採用異步通知的方式(這種通知方式也稱為 emit

B. 在 多線程的狀況下也要考慮同步問題


Observer 實現

Observer 標準

A. Observer:定義一個抽象的共同方法


public interface Observer {

    void changePrice(int newPrize);

}

B. ConcreteObserver:Person 實做 Observer 類,並決定在接收到通知後的行為細節


public class Person implements Observer {

    private final String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public void changePrice(int newPrize) {
        System.out.println("Name : " + name + ", NewsPaper: " + newPrize);
    } 

}

C. Subject:NewsSubject 的重點是 聚合抽象 Observer,在定義一個抽象通知方法


public abstract class NewsSubject {

    protected final List<Observer> list = new ArrayList<>();

    public void addObserver(Observer observer) {
        list.add(observer);
    }

    public void removeObserver(Observer observer) {
        list.remove(observer);
    }

    public abstract void notifyUpdate(int newPrice);
}

D. ConcreteSubject:這裡以報社為例,每一間報社對於改動新價格後的優惠不同,這個不同點會定義在抽想方法中


// 蘋果日報

public class AppleNews extends NewsSubject {

    private int price;

    public AppleNews(int price) {
        this.price = price;
    }

    @Override
    public void notifyUpdate(int newPrice) {
        if(price == newPrice) {
            return;
        }

        // 折價 10 %
        price = newPrice - (newPrice / 10);

        for(Observer item : list) {
            item.changePrice(price);
        }
    }

}

// --------------------------------------------------------
// 自由時報

public class FreedomNews extends NewsSubject {
    private int price;

    public FreedomNews(int price) {
        this.price = price;
    }
    
    @Override
    public void notifyUpdate(int newPrice) {
        if(price == newPrice) {
            return;
        }

        // 折扣 10 元
        price = newPrice - 10;

        for(Observer item : list) {
            item.changePrice(price);
        }
    }
}

User 測試程式:可以看到兩個重點,1. 不需要手動新、呼叫每個訂閱者,2. 透過抽象定義可以讓每個觀察管理者 (ConcreteSubject) 定義細節


public class MainObserver {

    public static void main(String[] args) {
        System.out.println("Apple news ----------------------");
        AppleNews appleNews = new AppleNews(330);

        appleNews.addObserver(new Person("Alien"));
        appleNews.addObserver(new Person("Pan"));
        appleNews.addObserver(new Person("Domo"));
        appleNews.notifyUpdate(340);

        System.out.println("Freedom news ----------------------");
        FreedomNews freedomNews = new FreedomNews(330);

        freedomNews.addObserver(new Person("Alien"));
        freedomNews.addObserver(new Person("Pan"));
        freedomNews.addObserver(new Person("Domo"));
        freedomNews.notifyUpdate(340);
    }
}

● 目前這裡暫時不關心 Multi Thread 同步問題,如果需要使用 MultiThread,則需要加上 sychronized 關鍵字同步

JDK 內建觀察者:介紹

JDK Source Code 以遺棄,原因是它並 不支援序列化 (沒實作 Serializable 介面),創建時線程(執行緒)不安全

Observer:JDK 抽象觀察者取名 Observer

Subject: 被觀察者 JDK 取名為 Observable (可被觀察)

A. 在多線程操作同一個對象時 addObserver & deleteObserver 是安全的

B. 通知時必須改變 changed 狀態,相關方法 setChangedclearChanged 方法


public class Observable {
    private boolean changed = false;
    private Vector obs;

    ...

    public void notifyObservers(Object arg) {
        Object[] arrLocal;

        synchronized (this) {

            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

    ...

    protected synchronized void setChanged() {
        changed = true;
    }

    ...

    protected synchronized void clearChanged() {
        changed = false;
    }
}

實作 JDK 觀察者

做跟上面一樣的例子,不過我們用 JDK 提供的類別

A. ConcreteObserver:實作 Observer 接口,定義 Person 接收到更新後的行為


public class Person implements Observer {

    private final String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("Name : " + name + ", NewsPaper: " + arg);
    }

}

B. ConcreteSubject:繼承於 Observable 抽象類,定義報社對於價格更新的打折細節


// 蘋果日報

public class AppleNews extends Observable {
    private int price;

    public AppleNews(int price) {
        this.price = price;
    }

    public void changePrice(int newPrice) {
        if(price == newPrice) {
            return;
        }

        price = newPrice - (newPrice / 10);

        setChanged();

        notifyObservers(newPrice - (newPrice / 10));
    }
}

// --------------------------------------------------------
// 自由時報

public class FreedomNews extends Observable {

    private int price;

    public FreedomNews(int price) {
        this.price = price;
    }

    public void changePrice(int newPrice) {
        if(price == newPrice) {
            return;
        }

        price = newPrice - 10;

        setChanged();

        notifyObservers(newPrice - 10);
    }

}

--實作--


Android Source Listview

我們來看看 Android 最常使用的列表 ListView,它是如何更新元件。以下是 ListView 的基本使用方式


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = findViewById(R.id.list_view);

        listView.setAdapter();
    }

    public final class MyAdapter extends BaseAdapter {

        private List<String> info = new ArrayList<>();

        public void addData(String title) {
            info.add(title);
            // 主要分析
            notifyDataSetChanged();
        }

        @Override
        public int getCount() {
            return info.size();
        }

        @Override
        public Object getItem(int position) {
            return info.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            LayoutInflater inflater = getLayoutInflater();

            MyViewHolder viewHolder;

            if(convertView == null) {
                viewHolder = new MyViewHolder();

                convertView = inflater.inflate(R.layout.list_item, parent, false);
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (MyViewHolder) convertView.getTag();
            }

            viewHolder.textView.setText(info.get(position));

            return convertView;
        }
    }

    private static final class MyViewHolder {
        private TextView textView;
    }
}

ListView:BaseAdapter 聚合觀察者

● 這裡我們主要分析 BaseAdapter#notifyDataSetChanged 方法

A. Subject:一看就知道 BaseAdapter 會聚合所有觀察者,其中就包含了註冊、取消註冊、通知... 等等方法


// BaseAdapter.java

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
    
    // 實際聚合觀察者的類別
    private final DataSetObservable mDataSetObservable = new DataSetObservable();


    // 註冊觀察者
    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    // 取消觀察者
    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

    // 通知所有觀察者
    public void notifyDataSetChanged() {
        // @ 追蹤 notifyChanged 方法
        mDataSetObservable.notifyChanged();
    }
}

● 其實 BaseAdapter 是包裝了一個可觀察者,也就是 DataSetObservable 類,它才是通知的實作類


// DataSetObservable.java

// 觀察者為 DataSetObserver
public class DataSetObservable extends Observable<DataSetObserver> {

    public void notifyChanged() {
        // 列表同步鎖
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                // 真正通知
                mObservers.get(i).onChanged();
            }
        }
    }

    public void notifyInvalidated() {
        synchronized (mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onInvalidated();
            }
        }
    }
}

B. Observer:DataSetObserver 為抽象觀察者,宣告所有觀察者必須實作的函數


// DataSetObserver.java

public abstract class DataSetObserver {

    public void onChanged() {
        // Do nothing
    }

    public void onInvalidated() {
        // Do nothing
    }
}

● BaseAdpater 聚合觀察者 UML 關係圖

ListView 觀察者

● 上面已經有說明了 BaseAdapter 的一部分功能就是聚合所有觀察者,並執行通知,而 ListView 註冊觀察者則是透過 ListView#setAdapter 方法


// ListView.java

    ListAdapter mAdapter;

    @Override
    public void setAdapter(ListAdapter adapter) {
        // 如果已經有 Adapter 則移除觀察者
        if (mAdapter != null && mDataSetObserver != null) {
            // 解註冊
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        ... 省略部分
        
        // 賦予 mAdapter
        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
            mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }

        if (mAdapter != null) {
            ...

            // 創建觀察者
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);


            ... 省略部分
        } else {
            ...
        }

        // 刷新 ListView layout
        requestLayout();
    }

ListView 真正的觀察者:從上面我們可以看到 ListView 會創建觀察者 AdapterDataSetObserver,它就是該 ListView 觀察數據的類 (實現在 AdapterView)


// AbsListView.java

    // 查看 AdapterDataSetObserver 類
    class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();
            if (mFastScroll != null) {
                mFastScroll.onSectionsChanged();
            }
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            if (mFastScroll != null) {
                mFastScroll.onSectionsChanged();
            }
        }
    }

// --------------------------------------------------------------
// AdapterView.java

    class AdapterDataSetObserver extends DataSetObserver {

        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            // 檢查是否有舊數據
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
              
                // 恢復舊數據
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            
            // 要求重新布局
            requestLayout();
        }
        
        ... 省略其他方法
    }


更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

設計建模 2 大概念- UML 分類、使用

物件導向設計原則 – 6 大原則(一)

物件導向設計原則 – 6 大原則(二)

創建、行為、結構型設計 8 個比較 | 包裝模式 | 最佳實踐

創建模式:Creation Patterns

創建模式 PK

創建模式 - Creation Patterns

結構模式:Structural Patterns

結構模式 PK

結構模式 - Structural Patterns

結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結

Bridge 橋接模式 | 解說實現 | 物件導向設計

Decorate 裝飾模式 | 解說實現 | 物件導向設計

Proxy 代理模式 | 解說實現 | 分析動態代理

Iterator 迭代設計 | 解說實現 | 物件導向設計

Facade 外觀、門面模式 | 解說實現 | 物件導向設計

Adapter 設計模式 | 解說實現 | 物件導向設計

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響