外部のデータ構造にそのまま依存したプログラミングをやめよう

この記事は、[C# Advent Calendar 2018](https://qiita.com/advent-calendar/2018/c-sharp) の１０日目の記事です。

「ASP.NET Core で何か作ったるで！」と息を巻いていましたが、土日含むここ一週間、風邪で記事が全く書けなかったので、
下書きから何とか記事として成り立っていそうなもの選んでを投稿します...:dizzy_face::dizzy_face::dizzy_face:



## 外部のデータ構造にそのまま依存したプログラムは時間経過で破綻する



天気予報 CSV データを読み込んで、それ利用するプログラムがあったとします。

CSV はこんな感じ ↓
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/3950c838-126a-279f-26b0-e6e5d2c64c81.png)

CSV データを読み込む処理はこんな感じ↓

```csharp:CSVを読み込むメソッド
static List<string[]> ReadCSV(string filepath) {
  var lines = File.ReadLines(filepath);
  return lines
    .Skip(1) // 1行目はカラム名
    .Select(line => line.Split(','))
    .ToList();
}
```

`File.ReadLines`は`Enumerable<string>`（ 1 行ごとのシーケンス）を返します。
1行目はカラム名（`日付,天気,最高気温,最低気温`）が入っているのでスキップ`Skip(1)`します。
`Select`で1行`line`（`24(水),曇,20,12`）ごとに`,`で分割します。

読み取った CSV データを利用するクラスはこんな感じ↓

```cs:CSVで読み取ったデータ構造にをそのまま利用したクラス
// 不明瞭な参照
class ObscuringReferences {
  readonly IList<string[]> data;

  // CSVデータをコンストラクタで受け取る
  public ObscuringReferences(IList<string[]> data) {
    this.data = data;
  }

  // 気温差一覧
  public int[] TemperatureDeferences() =>
    data.Select(tokens => {
      var highest = int.Parse(tokens[2]);
      var optimum = int.Parse(tokens[3]);
      return highest - optimum;
    }).ToArray();

  // 平均最高気温
  public double AverageHighTemperature() =>
    data.Average(tokens => int.Parse(tokens[2]));

  /*
   * 他にもインデックスでデータを参照するメソッドがたくさんある...
   */
}
```


CSV データを利用するクラスには気温差の一覧を返す`TemperatureDeferences`や、平均最高気温を返す`AverageHighTemperature`といったメソッドが定義されています。


![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/6a4bbfbd-1c7a-5f1d-4796-7f8a451a1530.png)
![キャプチャ2.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/7deb5c95-4d4e-0faf-691d-0555a36790b6.png)

これらのメソッドでは配列に対してインデックスでアクセス(`tokens[n]`)しています。
そのため何のデータが配列の何番目にあるかを把握しなければなりません。



一度作ってテストしてもう二度度変わらないなら良いですが、プログラムもデータ構造も時間経過で変わっていくものです。
このプログラムには、配列の構造が変わった場合、インデックスを修正する必要がある（複数ある場合にはすべてのインデックスを直して回る必要がある）、修正が漏れるとインデックスの範囲外にアクセスして例外がスローされる、修正が漏れてもコンパイルエラーは発生せず実行時まで気づけない、といった問題があります。

## データは一度オブジェクトにマッピングしよう

まず、天気予報オブジェクトを定義しましょう。

```cs
struct Forecast {
  public Forecast(
    DateTime date,
    string weather,
    int highestTemperature,
    int optimumTemperature
  ) {
    Date = date;
    Weather = weather;
    HighestTemperature = highestTemperature;
    OptimumTemperature = optimumTemperature;
  }

  public DateTime Date { get; }
  public string Weather { get; }
  public int HighestTemperature { get; }
  public int OptimumTemperature { get; }
}
```

これで、例えば最高気温へのアクセスは`tokens[2]`から`Forecast.HighestTemperature`に変わります。

CSV データを `Forecast` 型のシーケンスに変換するメソッドを作ります。
日時が `tokens[0]` にあるといった知識の把握はこのメソッドにカプセル化します。

```cs
static List<Forecast> Forecastifiy(IEnumerable<string[]> data) =>
  data.Select(tokens => new Forecast(
    date: DateTime.Parse(tokens[0]),
    weather: tokens[1],
    highestTemperature: int.Parse(tokens[2]),
    optimumTemperature: int.Parse(tokens[3])
  )).ToList();
```


CSVデータそのままでなく、`Forcast`を利用してみます。

```cs
// メインメソッド
var data = ReadCSV("forcsts.csv");
var forecasts = Forecststify(data);
var xxx = RevealingReferences(forecasts);
```

```cs:
// 明瞭な参照
class RevealingReferences {
  readonly IList<Forecast> forecasts;

  // CSVデータでなくIEnumerable<Forecast>を受け取る
  public RevealingReferences(IEnumerable<Forecast> forecasts) {
    this.forecasts = forecasts.ToList();
  }

  public int[] TemperatureDeferences() =>
    forecasts.Select(f =>
      f.HighestTemperature - f.OptimumTemperature
    ).ToArray();

  public double AverageHighTemperature() =>
    forecasts.Average(f => f.HighestTemperature);
}
```

インデックスによる配列へのアクセスは消え明示的な参照へ置き換わっています。

これで、データ構造が変わってもデータをオブジェクトにパースする`Forecststify`メソッドだけを直せば良くなりました。
またデータに対するアクセスが明瞭（ex：`forecasts.date`）になりました。

並べてみるとずっと読みやすくなっているのが分かると思います。

- before
![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/6a4bbfbd-1c7a-5f1d-4796-7f8a451a1530.png)
![キャプチャ2.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/7deb5c95-4d4e-0faf-691d-0555a36790b6.png)


- after
![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/195024/e600b246-2d77-87ea-2b9c-33d5e470afb3.png)

読んでいただきありがとうございました。