--- title: 'Observer 觀察者模式' disqus: kyleAlien --- Observer 觀察者模式 === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile: :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**Observer 觀察者模式 | JDK Observer | Android Framework Listview**](https://devtechascendancy.com/observer-jdk-android-framework-listview/) ::: [TOC] ## Observer 使用場景 * 當一個狀態或是變數改變時,需要**通知所有擁有它這個變數的物件** (就像是有多人訂閱 "Hello" 日報,如果日報訂閱價格改變,就必須通知所有已訂閱的用戶) * 定義一個可觀察物件,讓可觀察物件與其他物件產生 **一對多的依賴關係**,並在刷新時通知其他物件 > 關聯關係是 ==可拆分== 的,**並非組合關係** ### Observer UML * Observer 角色介紹 | 角色 | 功能說明 | | -------- | -------- | | Observer | 觀察者抽象,**定義一個更新方法** | | ConcreteObserver | 實作一個更新方法的細節 | | Subject | 聚合 `Observer`,並宣告抽象通知方法 | | ConcreteSubject | 實作抽象通知方法 | :::info * 建議在被觀察者(`Subject`)要通知前,就做出是否通知的判斷,不要延到觀察者(`Observer`)判斷是否消費這個事件 > 減少 Observer 的個別判斷邏輯 ::: >  ### Observer 設計 - 優缺點 * **Observer 設計優點** : 1. 觀察者與被觀察者,是抽象耦合,對應業務變化 2. 增強系統靈活性、可擴充、自由度 3. 可以建立一套觸發機制(鏈式調用) * **Observer 設計缺點** : 1. 考慮到執行效率問題,由於是 **輪巡更新**,所以當一個觀察者卡頓 (重量級) 會導致後方的資料更新也卡頓 > 可以採用異步通知的方式(這種通知方式也稱為 `emit`) 2. 在 **多線程的狀況下也要考慮同步問題** ## Observer 實現 ### Observer 標準 1. **`Observer` 類**:定義一個抽象的共同方法 ```java= public interface Observer { void changePrice(int newPrize); } ``` 2. **`ConcreteObserver` 類**:Person 實做 `Observer` 類,並決定在接收到通知後的行為細節 ```java= 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); } } ``` 3. **`Subject` 類**:NewsSubject 的重點是 **聚合抽象 `Observer`**,在定義一個抽象通知方法 ```java= 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); } ``` 4. **`ConcreteSubject` 類**:這裡以報社為例,每一間報社對於改動新價格後的優惠不同,這個不同點會定義在抽想方法中 ```java= // 蘋果日報 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`) 定義細節 ```java= 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); } } ``` >  :::danger * 目前這裡暫時不關心 Multi Thread 同步問題,**如果需要使用 MultiThread,則需要加上 `sychronized` 關鍵字同步** ::: ## JDK 內建觀察者 - 介紹 [**JDK Source Code**](https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/Observable.java) 以遺棄,原因是它並 **不支援序列化** (沒實作 `Serializable` 介面),**創建時線程(執行緒)不安全** * **`Observer` 類**:JDK 抽象觀察者取名 **Observer** >  * **`Subject` 類**: 被觀察者 JDK 取名為 **Observable** (可被觀察) 1. 在多線程操作同一個對象時 `addObserver` & `deleteObserver` 是安全的 >  2. **通知時必須改變 `changed` 狀態**,相關方法 `setChanged`、`clearChanged` 方法 ``` java= 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 觀察者 :::info 做跟上面一樣的例子,不過我們用 JDK 提供的類別 ::: 1. **ConcreteObserver**:實作 `Observer` 接口,定義 Person 接收到更新後的行為 ```java= 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); } } ``` 2. **ConcreteSubject**:繼承於 `Observable` 抽象類,定義報社對於價格更新的打折細節 ```java= // 蘋果日報 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 我們來看看 Android 最常使用的列表 ListView,它是如何更新元件。以下是 ListView 的基本使用方式 ```java= 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](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/BaseAdapter.java) 聚合觀察者 * 這裡我們主要分析 BaseAdapter#`notifyDataSetChanged` 方法 1. **Subject**:一看就知道 BaseAdapter 會聚合所有觀察者,其中就包含了註冊、取消註冊、通知... 等等方法 ```java= // 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 類**,它才是通知的實作類 ```java= // 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(); } } } } ``` 2. **Observer**:DataSetObserver 為抽象觀察者,宣告所有觀察者必須實作的函數 ```java= // DataSetObserver.java public abstract class DataSetObserver { public void onChanged() { // Do nothing } public void onInvalidated() { // Do nothing } } ``` * BaseAdpater 聚合觀察者 UML 關係圖 >  ### ListView 觀察者 * 上面已經有說明了 BaseAdapter 的一部分功能就是聚合所有觀察者,並執行通知,而 **ListView 註冊觀察者則是透過 ListView#`setAdapter` 方法** ```java= // 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`) ```java= // 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(); } ... 省略其他方法 } ``` >  ## 更多的物件導向設計 物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)! :::info * [**設計建模 2 大概念- UML 分類、使用**](https://devtechascendancy.com/introduction-to-uml-and-diagrams/) * [**物件導向設計原則 – 6 大原則(一)**](https://devtechascendancy.com/object-oriented-design-principles_1/) * [**物件導向設計原則 – 6 大原則(二)**](https://devtechascendancy.com/object-oriented-design-principles_2/) ::: ### 創建模式 - Creation Patterns * [**創建模式 PK**](https://devtechascendancy.com/pk-design-patterns-factory-builder-best/) * **創建模式 - `Creation Patterns`**: 創建模式用於「**物件的創建**」,它關注於如何更靈活、更有效地創建對象。這些模式可以隱藏創建對象的細節,並提供創建對象的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結 :::success * [**Singleton 單例模式 | 解說實現 | Android Framework Context Service**](https://devtechascendancy.com/object-oriented_design_singleton/) * [**Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer**](https://devtechascendancy.com/object-oriented_design_abstract-factory/) * [**Factory 工廠方法模式 | 解說實現 | Java 集合設計**](https://devtechascendancy.com/object-oriented_design_factory_framework/) * [**Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗**](https://devtechascendancy.com/object-oriented_design_builder_dialog/) * [**Clone 原型模式 | 解說實現 | Android Framework Intent**](https://devtechascendancy.com/object-oriented_design_clone_framework/) * [**Object Pool 設計模式 | 實現與解說 | 利用 JVM**](https://devtechascendancy.com/object-oriented_design_object-pool/) * [**Flyweight 享元模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_flyweight/) ::: ### 行為模式 - Behavioral Patterns * [**行為模式 PK**](https://devtechascendancy.com/pk-design-patterns-cmd-strat-state-obs-chain/) * **行為模式 - `Behavioral Patterns`**: 行為模式關注物件之間的「**通信**」和「**職責分配**」。它們描述了一系列對象如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結 :::warning * [**Stragety 策略模式 | 解說實現 | Android Framework 動畫**](https://devtechascendancy.com/object-oriented_design_stragety_framework/) * [**Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService**](https://devtechascendancy.com/object-oriented_design_interpreter_framework/) * [**Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞**](https://devtechascendancy.com/object-oriented_design_chain_framework/) * [**State 狀態模式 | 實現解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_state/) * [**Specification 規格模式 | 解說實現 | Query 語句實做**](https://devtechascendancy.com/object-oriented_design_specification-query/) * [**Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_command_servant/) * [**Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存**](https://devtechascendancy.com/object-oriented_design_memo_framework/) * [**Visitor 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_visitor_dispatch/) * [**Template 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_template/) * [**Mediator 模式設計 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_mediator/) * [**Composite 組合模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_composite/) ::: ### 結構模式 - Structural Patterns * [**結構模式 PK**](https://devtechascendancy.com/pk-design-patterns-proxy-decorate-adapter/) * **結構模式 - `Structural Patterns`**: 結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結 :::danger * [**Bridge 橋接模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_bridge/) * [**Decorate 裝飾模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_decorate/) * [**Proxy 代理模式 | 解說實現 | 分析動態代理**](https://devtechascendancy.com/object-oriented_design_proxy_dynamic-proxy/) * [**Iterator 迭代設計 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_iterator/) * [**Facade 外觀、門面模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_facade/) * [**Adapter 設計模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_adapter/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Java 設計模式` `基礎進階`
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.