【C#】Head First デザインパターン2章　Observerパターン


##Observerパターン

https://www.oreilly.co.jp/books/4873112494/

2章まで進みました。今回はObserverパターンです。1対他の構造をとるので、購読システム等を作るときに使うとよいです。観察者(Observer)が観察対象者(Subject)の変化を観察します。
良い点として挙げられているのは、疎結合です。
観察対象者が観察者がどのようなクラスであるか等を知る必要はなく、そのクラスが特定のインターフェイスを持っていることだけを知っています。
そのほかにもいろいろありますが、疎結合であるとうれしいことは一方を変更しても他方に影響がないことです。

2章では、気象台が出す気象データ（観察対象: WeatherData）を登録されているデバイス（観察者: Displays）に送信します。

###インターフェイスを使わないパターン
StatisticsDisplayは[ここ](https://resources.oreilly.com/examples/9780596007126/-/tree/master/examples)から持ってきました。
気象台が温度計等から観測したデータをすべて集め、WeatherDataにセットします。データが新しくセットされるたびに、各ディスプレイはアップデートされます。ここではWeatherDataが現在の状態と統計を表す二つのディスプレイの更新をします。

CurrentConditionsDisplayでは現在の気温と湿度を、StatisticsDisplayでは今まで受け取った結果の平均気温、最大気温、最低気温を表示します。

```plantuml
class WeatherData{
setMeasurements()
measurementsChanged()    
}
class CurrentConditionsDisplay{
update()
display()
}
class StatisticsDisplay{
update()
display()
}
WeatherData--[hidden]CurrentConditionsDisplay
WeatherData--[hidden]StatisticsDisplay
CurrentConditionsDisplay-[hidden]StatisticsDisplay

WeatherData-->CurrentConditionsDisplay
WeatherData-->StatisticsDisplay

```
図はシンプルですが、クラス間に依存があります。

```BadExample.cs

class Program
    {
        static void Main(string[] args)
        {
            WeatherData weatherData = new WeatherData();
            weatherData.setMeasurements(80, 65, 30.4f);
            weatherData.setMeasurements(90, 80, 60.4f);
            weatherData.setMeasurements(20, 45, 20.4f);
        }
    }


//新しくディスプレイを追加するたびにこのWeatherDataを書き換える必要がある。
    public class WeatherData
    {
        private float temperature;
        private float humidity;
        private float pressure;

        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay();
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay();

        public void measurementsChanged()
        {
            currentDisplay.update(temperature,humidity,pressure);
            statisticsDisplay.update(temperature, humidity, pressure);
            statisticsDisplay.display();
        }

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


    }

    public class CurrentConditionsDisplay
    {

        private float temperature;
        private float humidity;
        public void update(float temperature, float humidity, float pressure)
        {
            this.temperature = temperature;
            this.humidity = humidity;
            display();
        }

        public void display()
        {
            Console.WriteLine("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
        }

    }

    public class StatisticsDisplay
    {
       
        private float maxTemp = 0.0f;
        private float minTemp = 200;//set intial high so that minimum 
                                    //is set first invokation
        private float temperatureSum = 0.0f;
        private int numReadings = 0;       
        
        public int NumberOfReadings
        {
            get
            {
                return numReadings;
            }
        }
        
        public void update(float temperature, float humidity, float pressure)
        {
            temperatureSum += temperature;
            numReadings++;

            if (temperature > maxTemp)
            {
                maxTemp = temperature;
            }

            if (temperature < minTemp)
            {
                minTemp = temperature;
            }
           
        }
        
        public void display()
        {
            Console.WriteLine("Avg /Max/Min temperature = " + RoundFloatToString(temperatureSum / numReadings) +
                "F/" + maxTemp + "F/" + minTemp + "F");
        }
       
        public static string RoundFloatToString(float floatToRound)
        {
            System.Globalization.CultureInfo cultureInfo = new System.Globalization.CultureInfo("en-US");
            cultureInfo.NumberFormat.CurrencyDecimalDigits = 2;
            cultureInfo.NumberFormat.CurrencyDecimalSeparator = ".";
            return floatToRound.ToString("F", cultureInfo);
        }
        

    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64828/0d511709-4170-e2dd-afb5-8a0360cb9632.png)

ここでの問題点は新しくディスプレイが増えるたびにWeatherDataを書き換えなければいけなくなることです。この密結合を疎結合にするためにインターフェイスを導入します。

```plantuml

interface ISubject{
registerObserver()
removeObserver()
notifyObservers()
}
  

  class WeatherData {
registerObserver()
removeObserver()
notifyObservers()
setMeasurements()
measurementsChanged()    
}


interface IObserver{
update()
}

interface IDisplayElement{
display()
}

class CurrentConditionsDisplay{
update()
display() //現在の観測結果を表示
}

class StatisticsDisplay{
update()
display() //気温の平均、最大、最小を表示
}

ISubject--[hidden]WeatherData
ISubject<|..WeatherData
IObserver--[hidden]CurrentConditionsDisplay
IDisplayElement--[hidden]StatisticsDisplay

IObserver<|..CurrentConditionsDisplay
IDisplayElement<|..CurrentConditionsDisplay
IObserver<|..StatisticsDisplay
IDisplayElement<|..StatisticsDisplay
WeatherData-[hidden]CurrentConditionsDisplay
CurrentConditionsDisplay-[hidden]StatisticsDisplay
ISubject-[hidden]IObserver
IObserver-[hidden]IDisplayElement

IObserver <- WeatherData 
ISubject <- CurrentConditionsDisplay
ISubject <- StatisticsDisplay
```
図は複雑になりましたが、インターフェイスを通しているのでクラス間に依存はありません。
ディスプレイを追加したい場合、簡単にmainの中で追加することができ、追加の際にコードの変更をする必要はありません。

```WithInterface.cs
class Program
    {
        static void Main(string[] args)
        {
            WeatherData weatherData = new WeatherData();

            CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
            
            StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
            
            weatherData.setMeasurements(80, 65, 30.4f);
            weatherData.setMeasurements(90, 80, 60.4f);
            weatherData.setMeasurements(20, 45, 20.4f);
            statisticsDisplay.display();
            

        }
    }

    public interface ISubject
    {
        public void registerObserver(IObserver o);
        public void removeObserver(IObserver o);
        public void notifyObservers();
    }

    public interface IObserver
    {
        public void update(float temp, float humidity, float pressure);
    }

    public interface IDisplayElement
    {
        public void display();
    }

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

        public WeatherData()
        {
            observers = new List<IObserver>();
        }

        public void registerObserver(IObserver o)
        {
            observers.Add(o);
        }

        public void removeObserver(IObserver o)
        {
            int i = observers.IndexOf(o);
            if(i>= 0)
            {
                observers.RemoveAt(i);
            }
        }

        public void notifyObservers()
        {
            for(int i = 0; i < observers.Count; i++)
            {
                IObserver observer = (IObserver)observers[i];
                observer.update(temperature, humidity, pressure);
            }
        }

        public void measurementsChanged()
        {
            notifyObservers();
        }

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

    public class CurrentConditionsDisplay : IObserver, IDisplayElement
    {
        private float temperature;
        private float humidity;
        private ISubject weatherData;

        public CurrentConditionsDisplay(ISubject weatherData)
        {
            this.weatherData = weatherData;
            weatherData.registerObserver(this);
        }

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

        public void display()
        {
            Console.WriteLine("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
        }
    }

   

    public class StatisticsDisplay : IObserver, IDisplayElement
    {
        #region Members
        private float maxTemp = 0.0f;
        private float minTemp = 200;//set intial high so that minimum 
                                    //is set first invokation
        private float temperatureSum = 0.0f;
        private int numReadings = 0;
        private ISubject weatherData;
        #endregion//Members

        #region NumberOfReadings Property
        public int NumberOfReadings
        {
            get
            {
                return numReadings;
            }
        }
        #endregion//NumberOfReadings Property

        #region Constructor
        public StatisticsDisplay(ISubject weatherData)
        {
            this.weatherData = weatherData;
            weatherData.registerObserver(this);
        }
        #endregion//Constructor

        #region IObserver Members

        public void update(float temperature, float humidity, float pressure)
        {
            temperatureSum += temperature;
            numReadings++;

            if (temperature > maxTemp)
            {
                maxTemp = temperature;
            }

            if (temperature < minTemp)
            {
                minTemp = temperature;
            }
            //display();
        }

        #endregion

        #region IDisplayElement Members

        public void display()
        {
            Console.WriteLine("Avg /Max/Min temperature = " + RoundFloatToString(temperatureSum / numReadings) +
                "F/" + maxTemp + "F/" + minTemp + "F");
        }

        #endregion

        #region RoundFloatToString
        public static string RoundFloatToString(float floatToRound)
        {
            System.Globalization.CultureInfo cultureInfo = new System.Globalization.CultureInfo("en-US");
            cultureInfo.NumberFormat.CurrencyDecimalDigits = 2;
            cultureInfo.NumberFormat.CurrencyDecimalSeparator = ".";
            return floatToRound.ToString("F", cultureInfo);
        }
        #endregion//RoundFloatToString

    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64828/86e61e1b-e1e6-21f0-ff5c-10ab03fdc1cb.png)

結果です。

###一言
次回は3章です。UML図いまだに慣れません…