Шаблон наблюдателя в C#


Я просматриваю (фантастическую) книгу Head First Design Patterns и нуждаюсь в некоторых пояснениях относительно модели наблюдателя. Следующий фрагмент кода имитирует устройство (CurrentConditionDisplay), которое прослушивает обновления о погодных условиях.

Интерфейсы:

public interface ISubject
{
    void RegisterObserver(IObserver obs);
    void RemoveObserver(IObserver obs);
    void NotifyObservers();
}
public interface IDisplay
{
    string Display();
}
public interface IObserver
{
    void Update(float temperature, float humidity, float pressure);
}

Наблюдатель

public class CurrentConditionDisplay : IObserver, IDisplay
{
    private float temperature;
    private float humidity;
    private float pressure;
    private ISubject weatherData;
    public CurrentConditionDisplay(ISubject weatherData)
    {
        this.weatherData = weatherData;
        this.weatherData.RegisterObserver(this);

    }
    public string Display()
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("Welcome to Current Condition Display...");
        sb.AppendLine(this.temperature.ToString());
        sb.AppendLine(this.humidity.ToString());
        sb.AppendLine(this.pressure.ToString());
        return sb.ToString();
    }

    public void Update(float temperature, float humidity, float pressure)
    {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
    }
}

Субъект

public class WeatherData : ISubject
{
    private List<IObserver> observersList;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData()
    {
        observersList = new List<IObserver>();
    }
    public void RegisterObserver(IObserver obs)
    {
        observersList.Add(obs);
    }

    public void RemoveObserver(IObserver obs)
    {
        int index = observersList.IndexOf(obs);
        if (index >= 0)
        {
            observersList.RemoveAt(index);
        }
    }
    public void MeasurementsChanged()
    {
        Console.WriteLine("There is new data available...");
        NotifyObservers();
    }
    public void NotifyObservers()
    {
        foreach (IObserver observer in observersList)
        {
            observer.Update(temperature, humidity, pressure);
        }
    }
    public void SetMeasurements(float temperature, float humidity, float pressure)
    {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        MeasurementsChanged();
    }
}

Использовать эти классы в моей программе.cs я создаю один раз instnce из WeatherData и передаю этот объект в качестве параметра конструктору CurrentConditionDisplay. Проблема, которую я вижу при этом текущая настройка заключается в том, что IObserver имеет один метод Update, который принимает temperature, humidity, pressure в качестве параметров. Я не вижу никакой гарантии, что субъект (WeatherData) должен иметь эти поля в первую очередь. Должен ли я добавить другой интерфейс или абстрактный базовый класс, чтобы гарантировать, что при вызове SetMeasurements все поля, обновляемые в этом методе, на самом деле находятся в Observer?

4 3

4 ответа:

Я чувствую то же самое, что вы делаете... имея довольно общий звучащий интерфейс IObserver, есть специфическая сигнатура метода, которая на самом деле применяется только, когда наблюдение WeatherData чувствует себя отвратительно!

Я бы предпочел что-то вроде этого:

public interface IObserver<T>
{
    void Update(T updatedData);
}

С наблюдателем, который выглядел бы примерно так (вырезал здесь дополнительный код):

public class CurrentConditionDisplay : IObserver<WeatherUpdate>, IDisplay
{
    public CurrentConditionDisplay(ISubject<WeatherUpdate> weatherData)
    {
        this.weatherData = weatherData;
        this.weatherData.RegisterObserver(this);   
    }

    public void Update(WeatherUpdate update)
    {
        this.temperature = update.Temperature;
        this.humidity = update.Humidity;
        this.pressure = update.Pressure;
    }
}

И просто чтобы прояснить, мой общий T для IObserver<T> будет объектом, который инкапсулирует погоду обновление:

public WeatherUpdate
{
    public float Temperature;
    public float Humidity;
    public float Pressure;
}

И ISubject придется изменить, чтобы включить также общий параметр:

public interface ISubject<T>
{
    void RegisterObserver(IObserver<T> obs);
    void RemoveObserver(IObserver<T> obs);
    void NotifyObservers();
}

Если вы хотите применить это, то вы можете определить свойства температуры, влажности и давления в интерфейсе ISubject (см. http://msdn.microsoft.com/en-us/library/64syzecx.aspx).

Затем измените метод Update в интерфейсе IObserver (и класс, который его реализует) - вы можете удалить параметры. Измените метод обновления класса CurrentConditionDisplay для поиска значений температуры, влажности, давления из свойств объекта, который реализует ISubject.

Нет, IObserver не должно быть полей для температуры, влажности и давления.

Когда речь заходит об интерфейсах, важно помнить, что интерфейсы более тесно связаны с потребностями своих клиентов (то есть вызывающих устройств, в вашем случае класса WeatherData), а не их реализаций.

Я имею в виду, что вы должны смотреть на интерфейс с точки зрения потребностей класса WeatherData в первую очередь-этот класс использует интерфейс IObserver для уведомления других об изменениях в температура, влажность и давление и ничего больше-ему не нужно получать температуру, влажность и давление от наблюдателей (что он будет делать с этой информацией?).

На самом деле некоторые реализации IObserver могут даже не сохранять эту информацию - например, какой-то наблюдатель протоколирования может регистрировать эти изменения, а затем полностью отбросить эту информацию. В этом случае добавление этих свойств к интерфейсу заставит наблюдателя реализовать элементы что ни наблюдатель, ни реализация на самом деле не нужны!

При определении интерфейсов всегда думайте в терминах методов и свойств, которые должен использовать вызывающий объект - все остальное является деталями реализации класса, реализующего интерфейс.

Ваше право, при текущей реализации нет никакой гарантии, что наблюдатель обладает свойствами температуры, влажности и давления. Это не имеет значения, так как то, что является гарантировано, что вы получите эту информацию с помощью метода update вызывается.

ДОПОЛНИТЕЛЬНОЕ ЧТЕНИЕ

Для ясности рассмотрим пример реального мира :

DoFactory.com: Модель Наблюдателя

Еще один великий ресурс:

PluralSight.com: Библиотека Шаблонов Проектирования