データベースという名のグローバル変数との向き合い方(Repository Pattern)

グローバル変数

スコープ関係なく、プログラム中のどこからでもアクセスが可能な変数のこと。 その性質上、扱いには気をつけないといけない。扱いを間違えると、「いつどこで変数が書き換えられるかの把握が難しく、プログラムの見通しが悪くなる」といった問題が生じる場合がある。

データベースはグローバル変数

MySQL などのデータベースに格納されたデータはグローバル変数と同じ性質を持っている。SQL クエリ等を介すことでプログラム中のどこからでもアクセスでき、書き換えが可能である。 「グローバル変数の扱いには十分気をつけないとヤバいことになる」というのは広く共通認識として存在するが、一方で「データベースはグローバル変数である」ということが常に意識の片隅にあるかと言われると案外そうではない気がする。

そのため開発現場では、「データベースというグローバル変数が、意図せずして雑に扱われている」という現象が起きやすい。 なので気をつけないと、一般にグローバル変数のデメリットとして言われる「いつどこで変数が書き換えられるかの把握が難しく、プログラムの見通しが悪くなる」という問題が容易に発生してしまう。 結果、テーブルの行を update するロジックが様々な場所に散らばり、「テーブル X のカラム Y は、どういう条件・状態のときにどういう値を取るべきで、どこでどう使われているのか」というのが全く見えてこなくなる。仕様を把握しきれず雰囲気で更新ロジックを新たに追加した結果、意図せずしてデータの整合性を崩してしまったり、気まぐれにしか update されない updated_at を爆誕させたりする。

こういった問題はサービスが成長してコードの規模が大きくなればなるほど深刻になる。開発者は改修のたびにコード全体を grep することに作業時間の大半を費やすようになり、開発速度は低下し、コードをいじるたびに黒ひげ危機一発をしてメンタルをすり減らすことになる。

Repository Pattern

この問題の一つの解決策となりうるのが Repository パターンである。 Repository パターンを用いた簡単な疑似コードを書いてみる。

class HogeRepository {

  public Hoge get(int id) {
    Row row = db.execute("SELECT id, a, b FROM hoge where id=?", id);
    return new Hoge(row.getInt('id'), row.getInt('a'), row.getStr('b'));
  }

  public void save(Hoge hoge) {
    db.execute("INSERT INTO hoge VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE ...", hoge.id, hoge.a,
        hoge.b);
  }
}

class Hoge {

  int id;
  int a;
  String b;

  public Hoge(int id, int a, String b) {
    this.id = id;
    this.a = a;
    this.b = b;
  }

  public void changeState() {
    this.a = this.a * 10;
    this.b = this.b + " foo";
  }
}

// Repositoryの利用例
class SomeApplicationService {

  HogeRepository hogeRepository;

  public void sampleProcess() {
    Hoge hoge = hogeRepository.get(id);
    hoge.changeState();
    hogeRepository.save(hoge);
  }
}

Repository の役割は、「とあるオブジェクトの取得と保存」のみである。 Repository は、オブジェクトの状態変化などのロジックには一切関与せず、「オブジェクトをどうやってデータベースに保存するか」「データベースからどうやってオブジェクトを復元するか」のみを完ぺきにこなすようにする。 やってることの概念は、オブジェクトと JSON 間のシリアライズとデシリアライズのようなものに近い。

Repository パターンで書かれたこのコード片には、以下のような特徴がある。

  • Hogeの保存/取得(DB⇔ オブジェクトの変換ロジック)」と「Hogeの内部状態の変化(ドメインロジック)」を切り離せている。
  • HogeオブジェクトのchangeState()によって変化するのはHogeオブジェクトの内部状態のみ。オブジェクトの内部状態が変わるだけなので DB は全く関与しておらず、当然 DB の更新は行われない。
  • Repository は単にHogeオブジェクトを現在の状態のまま DB に突っ込んだり取り出したりするだけである。そのため、Hoge の内部状態がいつどのように変化するかなどは Repository は全く知らなくてよい。

そして、これらの特徴は、データベースがグローバル変数であるがゆえの弊害を以下のように緩和してくれる。

  • DB との接点が最小限に抑えられている。DB とのやり取りの窓口はgetsaveの 2 箇所のみに限られるため、「どこでどうHoge テーブルが参照/更新されるのか」がすぐ把握できる。
  • データの整合性を保つ役割はHogeクラスの内部に閉じており、外からどうHogeオブジェクトやHogeRepository をいじっても、データの整合性が崩れることはなくなっている。
  • Hogeテーブルの各値は、どういう条件・状態のときにどういう値を取りうるのか」は、Hoge オブジェクトのロジックの内部だけ見れば把握できる。

そのほか、Repository パターンには以下のようなメリットもある。

  • DB が絡むテストが最小限で済む。「Hogeオブジェクトを現在の状態のままきちんと保存できるか」「きちんとHoge オブジェクトに復元し直せるか」のテストだけ書いておけば良い。Hogeオブジェクトの内部状態の更新ロジックは Repository には関係ない。
  • 今後の仕様変更などによって内部状態の変化パターンや変化ロジックが増えたとしても、それによって DB への update 文を増やす必要はない。
    • HogeRepositoryHogeの現在の状態のままそっくりそのまま保存/取得するので、内部状態がどう複雑に変化しようと関係ない。
  • DB を RDB から KVS とかに変更するようなことがあっても、Repository の参照/取得のロジックだけ書き直せば動く。
    • テストの際の DB のモックも容易になる。ただ単にインメモリでオブジェクト保持しておくだけの Repository に差し替えたりすればよい。

このように、Repository パターンをうまく用いることで、アプリケーション上で扱う物事について、その複雑な内部状態の変化を DB から切り離して柔軟に取り扱うことが可能となる。 Repository パターンは、DB が本質的にはグローバル変数であるがゆえの辛さを緩和してくれる効果がある。

なお逆に、ログデータみたいに一回書き込んだら不変だし複雑なロジックも持ちませんみたいな場合には Repository パターンの恩恵はあまり享受できないと思われる。 Repository パターンを適材適所でうまく使って、柔軟で見通しの良いアプリケーションを作りましょう。