跳轉到內容

觀察者

25% developed
來自華夏公益教科書,開放的書籍,為一個開放的世界

模型-檢視-控制器 計算機科學設計模式
觀察者
原型

範圍

物件

目的

行為

意圖

定義物件之間的一對多依賴關係,以便當一個物件改變狀態(主題)時,所有依賴於它的物件(觀察者)都會自動收到通知並更新。

適用性

  • 當一個抽象有兩個方面,其中一個依賴於另一個時
  • 當一個物件的更改需要更改其他物件,而你不知道需要更改多少個物件時
  • 當一個物件應該通知其他物件,而無需假設這些物件是誰時

結構

後果

  • + 模組化:主題和觀察者可以獨立變化
  • + 可擴充套件性:可以定義和新增任意數量的觀察者
  • + 可定製性:不同的觀察者提供主題的不同檢視
  • - 意外更新:觀察者彼此之間並不知道
  • - 更新開銷:可能需要提示

實現

  • 主題-觀察者對映
  • 懸空引用
  • 避免觀察者特定的更新協議:推和拉模型
  • 顯式註冊感興趣的修改
  • 單例,用於使可觀察物件唯一併全域性可訪問。
  • 中介者,用於封裝更新物件

描述

問題
在應用程式中的一個或多個地方,我們需要了解系統事件或應用程式狀態更改。我們希望有一種標準的方法來訂閱監聽系統事件,以及一種標準的方法來通知感興趣的方。在感興趣的方訂閱系統事件或應用程式狀態更改後,通知應該是自動的。還應該有一種取消訂閱的方法。
力量
觀察者和可觀察者可能應該由物件表示。觀察者物件將由可觀察者物件通知。
解決方案
訂閱後,監聽物件將透過方法呼叫方式收到通知。
鬆散耦合
當兩個物件鬆散耦合時,它們可以互動,但它們彼此瞭解得很少。努力在互動的物件之間實現鬆散耦合的設計。
  • Subject 唯一知道的關於觀察者的事情是它實現了某個介面
  • 我們可以隨時新增新的觀察者
  • 我們永遠不需要修改主題來新增新的觀察者型別
  • 我們可以獨立地重複使用主題或觀察者
  • 主題或觀察者的更改不會影響另一個

例子

觀察者模式在 Java 中被廣泛使用。例如,在以下程式碼段中

  • button 是主題
  • MyListener 是觀察者
  • actionPerformed() 等價於 update()
 JButton button = new JButton("Click me!");
 button.addActionListener(new MyListener());
 
 class MyListener implements ActionListener {
    public void actionPerformed(ActionEvent event) {
      ...
    }  
 }

另一個例子是 PropertyChangeSupportComponent 類使用 PropertyChangeSupport 物件讓感興趣的觀察者註冊以接收有關標籤、面板和其他 GUI 元件屬性更改的通知。

你能在上面的類圖中找到 SubjectObserverupdate() 嗎?

  • Component 是主題
  • PropertyChangeListener 是觀察者
  • propertyChange() 等價於 update()

成本

如果你不注意,這種模式可能很棘手。注意,表示主題狀態更改的資料可能會在不更改介面的情況下演變。如果你只傳輸一個字串,如果至少有一個新的觀察者也需要一個狀態碼,則模式可能會變得非常昂貴。除非你知道主題狀態的實現永遠不會改變,否則你應該使用中介者。

建立

這種模式的建立成本很高。

維護

這種模式的維護成本可能很高。

刪除

這種模式的刪除成本也很高。

建議

  • 在主題和觀察者類的名稱中新增 subjectobserver 術語,以向其他開發人員表明該模式的使用。
  • 為了提高效能,你可以只向觀察者傳送狀態差異,而不是新狀態。觀察者只能根據主題改變的部分進行更新,而不是主題的所有狀態。

實施

在 ActionScript 3 中的實現
// Main Class
package {
    import flash.display.MovieClip;

    public class Main extends MovieClip {
        private var _cs:ConcreteSubject = new ConcreteSubject();
        private var _co1:ConcreteObserver1 = new ConcreteObserver1();
        private var _co2:ConcreteObserver2 = new ConcreteObserver2();

        public function Main() {
            _cs.registerObserver(_co1);
            _cs.registerObserver(_co2);

            _cs.changeState(10);
            _cs.changeState(99);

            _cs.unRegisterObserver(_co1);

            _cs.changeState(17);

            _co1 = null;
        }
    }
}

// Interface Subject
package {
    public interface ISubject {
        function registerObserver(o:IObserver):void;

        function unRegisterObserver(o:IObserver):void;

        function updateObservers():void;

        function changeState(newState:uint):void;
    }
}

// Interface Observer
package {
    public interface IObserver {
        function update(newState:uint):void;
    }
}

// Concrete Subject
package {
    public class ConcreteSubject implements ISubject {
        private var _observersList:Array = new Array();
        private var _currentState:uint;

        public function ConcreteSubject() {
        }

        public function registerObserver(o:IObserver):void {
            _observersList.push( o );
            _observersList[_observersList.length-1].update(_currentState); // update newly registered
        }

        public function unRegisterObserver(o:IObserver):void {
            _observersList.splice( _observersList.indexOf( o ), 1 );
        }

        public function updateObservers():void {
            for( var i:uint = 0; i<_observersList.length; i++) {
                _observersList[i].update(_currentState);
            }
        }

        public function changeState(newState:uint):void {
            _currentState = newState;
            updateObservers();
        }
    }
}

// Concrete Observer 1
package {
    public class ConcreteObserver1 implements IObserver {
        public function ConcreteObserver1() {
        }

        public function update(newState:uint):void {
            trace( "co1: "+newState );
        }

        // other Observer specific methods
    }
}

// Concrete Observer 2
package {
    public class ConcreteObserver2 implements IObserver {
        public function ConcreteObserver2() {
        }

        public function update(newState:uint):void {
            trace( "co2: "+newState );
        }

        // other Observer specific methods
    }
}
在 C# 中的實現

傳統方法

C# 和其他 .NET Framework 語言通常不需要使用介面和具體物件來完全實現觀察者模式。以下是如何使用它們的示例,但是。

 
 
using System;
using System.Collections;

namespace Wikipedia.Patterns.Observer
{
  // IObserver --> interface for the observer
  public interface IObserver
  {
    // called by the subject to update the observer of any change
    // The method parameters can be modified to fit certain criteria
    void Update(string message);
  }

  public class Subject
  {
    // use array list implementation for collection of observers
    private ArrayList observers;

    // constructor
    public Subject()
    {
      observers = new ArrayList();
    }

    public void Register(IObserver observer)
    {
      // if list does not contain observer, add
      if (!observers.Contains(observer))
      {
        observers.Add(observer);
      }
    }

    public void Unregister(IObserver observer)
    {
      // if observer is in the list, remove
      observers.Remove(observer);
    }

    public void Notify(string message)
    {
      // call update method for every observer
      foreach (IObserver observer in observers)
      {
        observer.Update(message);
      }
    }
  }

  // Observer1 --> Implements the IObserver
  public class Observer1 : IObserver
  {
    public void Update(string message)
    {
      Console.WriteLine("Observer1:" + message);
    }
  }

  // Observer2 --> Implements the IObserver
  public class Observer2 : IObserver
  {
    public void Update(string message)
    {
      Console.WriteLine("Observer2:" + message);
    }
  }

  // Test class
  public class ObserverTester
  {
    [STAThread]
    public static void Main()
    {
      Subject mySubject = new Subject();
      IObserver myObserver1 = new Observer1();
      IObserver myObserver2 = new Observer2();

      // register observers
      mySubject.Register(myObserver1);
      mySubject.Register(myObserver2);

      mySubject.Notify("message 1");
      mySubject.Notify("message 2");
    }
  }
}

使用事件

在 C# 和其他 .NET Framework 語言(如 Visual Basic)中,使用具體和抽象觀察者和釋出者的替代方法是使用事件。事件模型透過 委託 來支援,委託定義了用於捕獲事件的方法簽名。因此,委託提供了由抽象觀察者提供的調解,方法本身提供了具體觀察者,具體主題是定義事件的類,主題是內置於基類庫中的事件系統。它是.NET 應用程式中實現觀察者模式的首選方法。

using System;

// First, declare a delegate type that will be used to fire events.
//  This is the same delegate as System.EventHandler.
//  This delegate serves as the abstract observer.
//  It does not provide the implementation, but merely the contract.
public delegate void EventHandler(object sender, EventArgs e);

// Next, declare a published event.  This serves as the concrete subject.
//  Note that the abstract subject is handled implicitly by the runtime.
public class Button
{
    // The EventHandler contract is part of the event declaration.
    public event EventHandler Clicked;

    // By convention,.NET events are fired from descendant classes by a virtual method,
    //  allowing descendant classes to handle the event invocation without subscribing
    //  to the event itself.
    protected virtual void OnClicked(EventArgs e)
    {
        if (Clicked != null)
            Clicked(this, e); // implicitly calls all observers/subscribers
    }
}

// Then in an observing class, you are able to attach and detach from the events:
public class Window
{
    private Button okButton;

    public Window()
    {
        okButton = new Button();
        // This is an attach function.  Detaching is accomplished with -=.
        // Note that it is invalid to use the assignment operator - events are multicast
        //  and can have multiple observers.
        okButton.Clicked += new EventHandler(okButton_Clicked);
    }

    private void okButton_Clicked(object sender, EventArgs e)
    {
        // This method is called when Clicked(this, e) is called within the Button class
        //  unless it has been detached.
    }
}
在 Java 中的實現

便捷的實現

你可以在 Java 中像這樣實現這種模式

 // Observer pattern -- Structural example
 // @since JDK 5.0
 import java.util.ArrayList;

 // "Subject"
 abstract class Subject {
    // Fields
    private ArrayList<Observer> observers = new ArrayList<Observer>();
    // Methods
    public void attach(Observer observer) {
       observers.add(observer);
    }
    public void detach(Observer observer) {
       observers.remove(observer);
    }
    public void notifyObservers() {
       for (Observer o : observers)    
          o.update();
   }
 }
 // "ConcreteSubject"
 class ConcreteSubject extends Subject {
  // Fields
  private String subjectState;
  // Properties
  public String getSubjectState() {
    return subjectState;
  }
  public void setSubjectState(String value) {
    subjectState = value;
  }
 }
 // "Observer"
 abstract class Observer {
   // Methods
   abstract public void update();
 }
 // "ConcreteObserver"
 class ConcreteObserver extends Observer {
  // Fields
  private String name;
  private String observerState;
  private ConcreteSubject subject;
 
  // Constructors
  public ConcreteObserver(ConcreteSubject subject, String name) {
     this.subject = subject;
     this.name = name;
     //subject.attach(this);
  }
  // Methods
  public void update() {
     observerState = subject.getSubjectState();
     System.out.printf("Observer %s's new state is %s\n", name, observerState);
  }
 }
 // Client test
 public class Client {
   public static void main(String[] args) {
      // Configure Observer structure
      ConcreteSubject s = new ConcreteSubject();
      s.attach(new ConcreteObserver(s, "A"));
      s.attach(new ConcreteObserver(s, "B"));
      s.attach(new ConcreteObserver(s, "C"));

      // Change subject and notify observers
      s.setSubjectState("NEW");
      s.notifyObservers();
   }
 }

內建支援

Java JDK 有這種模式的幾個實現:在圖形使用者介面中的應用程式,例如在 AWT 工具包、Swing 等中。在 Swing 中,每當使用者單擊按鈕或調整滑塊時,應用程式中的許多物件可能需要對更改做出反應。Swing 將感興趣的客戶端(觀察者)稱為“監聽器”,並允許你註冊任意數量的監聽器以接收有關元件事件的通知。

MVC 在 Swing 中更像 M(VC),即檢視和控制器緊密耦合;Swing 沒有將檢視與控制器分開。MVC 支援 n 層開發,即鬆散耦合的層(見下文),這些層可以獨立更改,甚至可以在不同的機器上執行。

也內建了對觀察者模式的支援。你所要做的就是擴充套件 java.util.Observable主題)並告訴它何時通知 java.util.Observer s。API 會為你完成剩下的工作。你可以使用推或拉樣式來更新你的觀察者。

java.util.Observable 是一個類,而 java.util.Observer 是一個介面。

 public void setValue(double value) {
    this.value = value;
    setChanged();
    notifyObservers();
 }

請注意,你必須呼叫 setChanged(),這樣 Observable 程式碼才能廣播更改。notifyObservers() 方法呼叫每個註冊觀察者的 update() 方法。update() 方法是 Observer 介面實現者的要求。

 // Observer pattern -- Structural example
 import java.util.Observable;
 import java.util.Observer;

 // "Subject"
 class ConcreteSubject extends Observable {
  // Fields
  private String subjectState;
  // Methods
  public void dataChanged() {
     setChanged();
     notifyObservers(); // use the pull method
  }
  // Properties
  public String getSubjectState() {
     return subjectState;
  }
  public void setSubjectState(String value) {
      subjectState = value;
      dataChanged();
  }
 }
 // "ConcreteObserver"
 import java.util.Observable;
 import java.util.Observer;

 class ConcreteObserver implements Observer {
   // Fields
   private String name;
   private String observerState;
   private Observable subject;
 
   // Constructors
   public ConcreteObserver(Observable subject, String name) {
     this.subject = subject;
     this.name = name;
     subject.addObserver(this);
  }

  // Methods
  public void update(Observable subject, Object arg) {
   if (subject instanceof ConcreteSubject) {
     ConcreteSubject subj = (ConcreteSubject)subject;
     observerState = subj.getSubjectState();
     System.out.printf("Observer %s's new state is %s\n", name, observerState);
   }
  }
 }
 // Client test
 public class Client {
   public static void main(String[] args) {
      // Configure Observer structure
      ConcreteSubject s = new ConcreteSubject();
      new ConcreteObserver(s, "A");
      new ConcreteObserver(s, "B");
      new ConcreteObserver(s, "C");
      // Change subject and notify observers
      s.setSubjectState("NEW");
   }
 }

鍵盤處理

以下是使用 Java 編寫的示例,它接收鍵盤輸入並將每行輸入視為事件。該示例基於庫類 java.util.Observerjava.util.Observable。當從 System.in 提供字串時,呼叫方法 notifyObservers,以便以呼叫其 'update' 方法的形式通知所有觀察者事件的發生——在本例中,ResponseHandler.update(...)

檔案 MyApp.java 包含一個 main() 方法,該方法可用於執行程式碼。

/* Filename : EventSource.java */
package org.wikibooks.obs;

import java.util.Observable;          // Observable is here
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class EventSource extends Observable implements Runnable {
    @Override
    public void run() {
        try {
            final InputStreamReader isr = new InputStreamReader(System.in);
            final BufferedReader br = new BufferedReader(isr);
            while (true) {
                String response = br.readLine();
                setChanged();
                notifyObservers(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/* Filename : ResponseHandler.java */

package org.wikibooks.obs;

import java.util.Observable;
import java.util.Observer;  /* this is Event Handler */

public class ResponseHandler implements Observer {
    private String resp;
    public void update(Observable obj, Object arg) {
        if (arg instanceof String) {
            resp = (String) arg;
            System.out.println("\nReceived Response: " + resp );
        }
    }
}
/* Filename : MyApp.java */
/* This is the main program */

package org.wikibooks.obs;

public class MyApp {
    public static void main(String[] args) {
        System.out.println("Enter Text >");

        // create an event source - reads from stdin
        final EventSource eventSource = new EventSource();

        // create an observer
        final ResponseHandler responseHandler = new ResponseHandler();

        // subscribe the observer to the event source
        eventSource.addObserver(responseHandler);

        // starts the event thread
        Thread thread = new Thread(eventSource);
        thread.start();
    }
}

觀察者模式的 Java 實現有優缺點

優點

  • 它隱藏了觀察者模式的許多細節
  • 它可以以推和拉方式使用。

缺點

  • 因為 Observable 是一個類,所以你必須對其進行子類化;你不能將 Observable 行為新增到子類化其他超類的現有類中(違反了面向介面程式設計原則)。如果你不能對 Observable 進行子類化,那麼使用委託,即為你的類提供一個 Observable 物件,並讓你的類轉發關鍵方法呼叫給它。
  • 因為 setChanged() 是受保護的,所以你不能偏愛組合而不是繼承。
在 PHP 中的實現

class STUDENT

<?php
class Student implements SplObserver {

  protected $type = "Student";
  private   $name;
  private   $address;
  private   $telephone;
  private   $email;
  private   $_classes = array();

  public function __construct($name)
  {
    $this->name = $name;
  }

  public function GET_type()
  {
    return $this->type;
  }

  public function GET_name()
  {
    return $this->name;
  }

  public function GET_email()
  {
    return $this->email;
  }

  public function GET_telephone()
  {
    return $this->telephone;
  }

  public function update(SplSubject $object)
  {
    $object->SET_log("Comes from ".$this->name.": I'm a student of ".$object->GET_materia());
  }

}

?>

class TEACHER

<?php
class Teacher implements SplObserver {

  protected $type = "Teacher";
  private   $name;
  private   $address;
  private   $telephone;
  private   $email;
  private   $_classes = array();

  public function __construct($name)
  {
    $this->name = $name;
  }

  public function GET_type()
  {
    return $this->type;
  }

  public function GET_name()
  {
    return $this->name;
  }

  public function GET_email()
  {
    return $this->email;
  }

  public function GET_telephone()
  {
    return $this->name;
  }

  public function update(SplSubject $object)
  {
    $object->SET_log("Comes from ".$this->name.": I teach in ".$object->GET_materia());
  }

}

?>

Class SUBJECT

<?php

class Subject implements SplSubject {

  private $name_materia;
  private $_observers = array();
  private $_log = array();

  function __construct($name)
  {
    $this->name_materia = $name;
    $this->_log[]       = "Subject $name was included";
  }

  /* Add an observer */
  public function attach(SplObserver $classes) {
    $this->_classes[] = $classes;
    $this->_log[]     = " The ".$classes->GET_type()." ".$classes->GET_name()." was included";
  }

  /* Remove an observer */
  public function detach(SplObserver $classes) {
    foreach ($this->_classes as $key => $obj) {
      if ($obj == $classes) {
        unset($this->_classes[$key]);
        $this->_log[] = " The ".$classes->GET_type()." ".$classes->GET_name()." was removed";
                }
    }
  }

  /* Notificate an observer */
  public function notify(){
    foreach ($this->_classes as $classes){
      $classes->update($this);
    }
  }

  public function GET_materia()
  {
    return $this->name_materia;
  }

  function SET_log($valor)
  {
    $this->_log[] = $valor ;
  }

  function GET_log()
  {
    return $this->_log;
  }

}
?>

應用

<?php
require_once("teacher.class.php");
require_once("student.class.php");
require_once("subject.class.php");

$subject  = new Subject("Math");
$marcus   = new Teacher("Marcus Brasizza");
$rafael   = new Student("Rafael");
$vinicius = new Student("Vinicius");

// Include observers in the math Subject
$subject->attach($rafael);
$subject->attach($vinicius);
$subject->attach($marcus);

$subject2 = new Subject("English");
$renato   = new Teacher("Renato");
$fabio    = new Student("Fabio");
$tiago    = new Student("Tiago");

// Include observers in the english Subject
$subject2->attach($renato);
$subject2->attach($vinicius);
$subject2->attach($fabio);
$subject2->attach($tiago);

// Remove the instance "Rafael from subject"
$subject->detach($rafael);

// Notify both subjects
$subject->notify();
$subject2->notify();

echo "First Subject <br>";
echo "<pre>";
print_r($subject->GET_log());
echo "</pre>";
echo "<hr>";
echo "Second Subject <br>";
echo "<pre>";
print_r($subject2->GET_log());
echo "</pre>";
?>

輸出

第一門科目

Array
(
    [0] =>  Subject Math was included
    [1] =>  The Student Rafael was included
    [2] =>  The Student Vinicius was included
    [3] =>  The Teacher Marcus Brasizza was included
    [4] =>  The Student Rafael was removed
    [5] => Comes from Vinicius: I'm a student of Math
    [6] => Comes from Marcus Brasizza: I teach in Math
)

第二門科目

Array
(
    [0] =>  Subject English was included
    [1] =>  The Teacher Renato was included
    [2] =>  The Student Vinicius was included
    [3] =>  The Student Fabio was included
    [4] =>  The Student Tiago was included
    [5] => Comes from Renato: I teach in English
    [6] => Comes from Vinicius: I'm a student of English
    [7] => Comes from Fabio: I'm a student of English
    [8] => Comes from Tiago: I'm a student of English
)
在 Python 中的實現

Python 中的觀察者模式

class AbstractSubject:
    def register(self, listener):
        raise NotImplementedError("Must subclass me")
 
    def unregister(self, listener):
        raise NotImplementedError("Must subclass me")
 
    def notify_listeners(self, event):
        raise NotImplementedError("Must subclass me")
 
class Listener:
    def __init__(self, name, subject):
        self.name = name
        subject.register(self)
 
    def notify(self, event):
        print self.name, "received event", event
 
class Subject(AbstractSubject):
    def __init__(self):
        self.listeners = []
        self.data = None

    def getUserAction(self):
        self.data = raw_input('Enter something to do:')
        return self.data

    # Implement abstract Class AbstractSubject

    def register(self, listener):
        self.listeners.append(listener)
 
    def unregister(self, listener):
        self.listeners.remove(listener)
 
    def notify_listeners(self, event):
        for listener in self.listeners:
            listener.notify(event)

 
if __name__=="__main__":
    # make a subject object to spy on
    subject = Subject()
 
    # register two listeners to monitor it.
    listenerA = Listener("<listener A>", subject)
    listenerB = Listener("<listener B>", subject)
 
    # simulated event
    subject.notify_listeners ("<event 1>")
    # outputs:
    #     <listener A> received event <event 1>
    #     <listener B> received event <event 1>
 
    action = subject.getUserAction()
    subject.notify_listeners(action)
    #Enter something to do:hello
    # outputs:
    #     <listener A> received event hello
    #     <listener B> received event hello

使用 函式裝飾器,可以在 Python 中更簡潔地實現觀察者模式。

在 Ruby 中的實現

在 Ruby 中,使用標準的 Observable 混合。有關文件和示例,請參閱 http://www.ruby-doc.org/stdlib/libdoc/observer/rdoc/index.html


Clipboard

待辦事項
新增更多插圖。


模型-檢視-控制器 計算機科學設計模式
觀察者
原型


你對本頁有疑問嗎?
在這裡提問


建立本教材中的新頁面


華夏公益教科書