search
LoginSignup
25

More than 3 years have passed since last update.

posted at

updated at

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

この記事は、C# Advent Calendar 2018 の10日目の記事です。

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

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

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

CSV はこんな感じ ↓
image.png

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

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

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

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

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
キャプチャ2.PNG

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

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

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

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

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] にあるといった知識の把握はこのメソッドにカプセル化します。

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を利用してみます。

// メインメソッド
var data = ReadCSV("forcsts.csv");
var forecasts = Forecststify(data);
var xxx = RevealingReferences(forecasts);
// 明瞭な参照
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
    キャプチャ2.PNG

  • after
    キャプチャ.PNG

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
25