Das Observer-Muster

Definition

Das Observer-Muster (Beobachter) ist eines der GoFEntwurfsmuster (Design Pattern) und gehört zu der Kategorie der Verhaltensmuster (Behavioral Design Pattern). Das Muster „definiert eine Eins-zu-viele-Abhängigkeit zwischen Objekten in der Art, dass alle abhängigen Objekte benachrichtigt werden, wenn sich der Zustand des einen Objekts ändert“ (EvKbF S. 51).

Beschreibung

Die Interaktion der Objekte ist ein essentieller Bestandteil der objektorientierten Programmierung. Fälle, in denen bestimmte Objekte über Veränderungen in anderen Objekten informiert werden müssen, sind recht häufig. Das Ziel sollte daher ein Entwurf sein, in dem so viel wie möglich entkoppelt ist und die Abhängigkeiten reduziert sind. Das Observer-Muster setzt auf ein OO-Entwurfsprinzip, das uns hilft dieses Ziel zu erreichen:

„Streben Sie für Objekte, die interagieren, nach Entwürfen mit lockerer Bindung.“

Die bekannteste Implementierung des Observer-Musters sind die Event Handler. Die Event Handler zeichnen sich durch eine einheitliche Schnittstelle aus, durch die der Entwurf flexibel bleibt. Um jedoch ein besseres Verständnis des Observer-Musters zu erhalten, wollen wir uns an einem Beispiel anschauen, wie man das Observer-Muster selbst implementieren kann.

Beispiel

Als Beispiel wird uns eine Wetterstation dienen. Der Entwurf soll es ermöglichen verschiedene Anzeigen zur Darstellung der Wetterdaten einzubinden, ohne grössere Änderungen am bestehenden Code durchzuführen. Wie bereits bei dem Strategy-Muster erläutert wurde, ist dies möglich, wenn man auf eine Schnittstelle statt auf eine Implementierung programmiert.

In unserem Beispiel ist die Wetterstation das Subjekt, welches von den Anzeigen als  Beobachter beobachtet wird und die bei einer Änderung der Daten benachrichtigt werden. Das Subjekt benötigt daher Methoden, die es ermöglichen Beobachter an- und abzumelden sowie über Änderungen zu benachrichtigen. Die Beobachter selbst benötigen eine Methode, die bei einer Änderung vom Subjekt aufgerufen wird und die Änderung mitteilt. Da in dem Beispiel auch eine Anzeige für die Wetterdaten verwendet wird, wird dafür ebenfalls eine Schnittstelle definiert, die die Anzeige aktualisiert.

public interface IObservable
{
	void RegisterObserver(IObserver observer);
	void UnregisterObserver(IObserver observer);
	void NotifyObservers();
	void NotifyObservers(object data);
}

public interface IObserver
{
	void Update(IObservable observable, object data);
}

public interface IDisplayElement
{
	void Display();
}

Die Implementierung des Subjekts erfolgt in einer Observable Superklasse, somit kann die Funktionalität wiederverwendet werden. Das Objekt für die Wetterdaten leitet sich von der Observable Klasse ab und benachrichtigt bei einer Änderung alle registrierten Beobachter.

public class Observable : IObservable
{
	private ArrayList _observers;

	public Observable()
	{
		_observers = new ArrayList();
	}

	public void RegisterObserver(IObserver observer)
	{
		_observers.Add(observer);
	}

	public void UnregisterObserver(IObserver observer)
	{
		int index = _observers.IndexOf(observer);

		if (index >= 0)
			_observers.RemoveAt(index);
	}

	public void NotifyObservers()
	{
		this.NotifyObservers(null);
	}

	public void NotifyObservers(object data)
	{
		foreach (IObserver observer in _observers)
		{
			observer.Update(this, data);
		}
	}
}

public class WeatherData : Observable
{
	public float Temperature { get; private set; }
	public float Humidity { get; private set; }
	public float Pressure { get; private set; }

	public WeatherData()
		: base()
	{
	}

	public void SetMeasurements(float temperature, float humidity, float pressure)
	{
		this.Temperature = temperature;
		this.Humidity = humidity;
		this.Pressure = pressure;

		MeasurementsChanged();
	}

	private void MeasurementsChanged()
	{
		this.NotifyObservers();
	}
}

Als nächstes wird die Anzeige der Wetterdaten implementiert. Da die Anzeige der Beobachter ist, implementiert sie neben dem IDisplayElement Interface, das IObserver Interface.

public class CurrentConditionsDisplay : IDisplayElement, IObserver
{
	private IObservable _observable;

	private float _temperature;
	private float _humidity;
	private float _pressure;

	public CurrentConditionsDisplay(IObservable observable)
	{
		_observable = observable;
		_observable.RegisterObserver(this);
	}

	public void Display()
	{
		string text = String.Format("Current conditions: {0} C degrees, {1}% humidity and {2} preasure.", _temperature, _humidity, _pressure);
		Console.WriteLine(text);
	}

	public void Update(IObservable observable, object data)
	{
		if (observable.GetType() == typeof(WeatherData))
		{
			WeatherData weatherData = (WeatherData)observable;

			_temperature = weatherData.Temperature;
			_humidity = weatherData.Humidity;
			_pressure = weatherData.Pressure;

			Display();
		}
	}
}

Analyse

Durch die Verwendung des Observer-Musters ist der Entwurf äußerst flexibel. Die Abhängigkeiten sind auf das notwendigste reduziert. Das Subjekt weiß vom Beobachter nur, dass es die IObserver Schnittstelle implementiert. Die Signatur der Update() Methode erwartet die Referenz des Subjekts und ggf. zusätzliche Daten. Um die Flexibilität zu erhöhen und die Abhängigkeiten weiter zu reduzieren, könnte man den observable Parameter als object deklarieren, ähnlich wie bei einem .NET EventHandler-Delegat. Weiterhin findet bei dem Observer-Muster die Anwendung des Offen-Geschlossen-Prinzips statt, da das Subjekt nicht geändert werden muss, um neue Beobachter hinzuzufügen. Darüber hinaus können Subjekt und Beobachter unabhängig voneinander wiederverwendet werden. Und Änderungen am Subjekt oder einem Beobachter haben keinen Einfluss auf den jeweils anderen.

Variationen der Update-Methode

Die Update-Methode kann auf zwei Arten implementiert werden: als Push– oder Pull-Methode. Die Wahl der Methode sollte im Hinblick auf künftige Änderungen sorgfältig überlegt sein.

Push-Methode
Bei der Push-Methode enthält die Update-Methode fest definierte Parameter. Die Signatur sieht dann z.B. wie folgt aus:

public void Update(string value1, int value2, double value3);

In diesem Fall ist der Beobachter noch stärker vom Subjekt entkoppelt, da er nur bestimmte Parameter erwartet. In bestimmten Fällen, mag diese Methodik nützlich sein. man sollte jedoch bedenken, dass der Wartungsaufwand bei der Push-Methode ziemlich hoch ist. Benötigt ein Beobachter einen weiteren Parameter, so müssen alle konkreten Beobachter entsprechend angepasst werden, auch wenn sie u.U. den Parameter gar nicht benötigen. Wird die Methode nicht angepasst, kommt es zwangsläufig zu einem Laufzeitfehler, wenn mehr Parameter übergeben werden als erwartet. Alternativ könnte man statt der einzelnen Parameter ein Event-Objekt übergeben, indem diese Parameter gekapselt sind. Das Objekt kann um weitere Informationen erweitert werden, ohne dass die Update-Methode angepasst werden muss. Dadurch werden Fehler vermieden. Diese Methodik wird beispielsweise in dem EventHandler-Delegat angewendet.

public delegate void EventHandler(Object sender, EventArgs e;)

Pull-Methode
Bei der Pull-Methode wird der Beobachter nur über die Änderung informiert und holt sich die benötigten Daten selbst aus dem konkreten Subjekt. Dazu wird entweder eine Referenz an die Update-Methode übergeben oder die Referenz des Subjekts wird bei der Anmeldung in einer Instanzvariable des Beobachters gespeichert.

public void Update(Subject subject);

Bei dieser Methodik bleibt die Signatur der Update-Methode konstant. Dem Beobachter ist das Subjekt, bzw. dessen Schnittstelle, bekannt und er ist selbst in der Pflicht herauszufinden, was sich geändert hat. Das kann u.U. zu einer komplexeren Logik führen und mehr Wartungsaufwand bedeuten.

Implementierung

Das .NET Framework enthält mit Events bereits eine Implementierung des Observer-Musters. Ob man eine eigene Implementierung des Observer-Musters verwenden möchte, hängt vom Entwurf der Software ab. Ebenso wie man das Observer-Muster implementiert. Generell empfiehlt es sich bei der Implementierung auf eine Kombination aus Superklasse und Interfaces zu setzen. So kann man bestehende Klassen um die Funktionalität erweitern, ohne dass diese sich explizit von einer Superklasse ableiten müssen. Und durch die Kapselung des Codes in einer Superklasse erreicht man höhere Wiederverwendbarkeit. Für welche Variante man sich letztendlich entscheidet, hängt von mehren Faktoren ab und sollte im Hinblick auf den Entwurf getroffen werden.

UML

Observer Design Pattern UML Diagram

Akteure

  • Subject (Subjekt)
    Definiert eine Schnittstelle für die An- und Abmeldung von Beobachtern sowie zur Benachrichtigung der Beobachter über Zustandsänderungen.
  • ConcreteSubject (KonkretesSubjekt)
    Implementiert die Schnittstelle des Subjekts. Speichert den Zustand des Objekts und benachrichtigt alle Beobachter über Zustandsänderungen.
  • Observer (Beobachter)
    Definiert eine Schnittstelle zur Benachrichtigung über die Zustandsänderung des Subjekts.
  • ConcreteObserver (KonkreterBeobachter)
    Implementiert die Schnittstelle zur Benachrichtigung über die Zustandsänderung eines konkreten Subjekts.

Quellen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.