ikenox.info

Naoto Ikeno

Naoto Ikeno

Backend Engineer, Software Architecture & Design, Perl, Golang, GCP

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

January 12, 2019

グローバル変数

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

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

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

そのため開発現場では、「データベースというグローバル変数が、意図せずして雑に扱われている」という現象が起きやすい。 なので気をつけないと、一般にグローバル変数のデメリットとして言われる「いつどこで変数が書き換えられるかの把握が難しく、プログラムの見通しが悪くなる」という問題が容易に発生してしまう。
テーブルの行をupdateするロジックが様々な場所に散らばり、「テーブルXのカラムYは、どういう条件・状態のときにどういう値を取り、どこでどう使われるのか」というのが全く見えてこなくなる。仕様を把握しきれず雰囲気で更新ロジックを新たに追加した結果、気まぐれにしかupdateされないupdated_atが爆誕する。
こういった問題はサービスが成長してコードの規模が大きくなればなるほど深刻になり、開発速度は低下し、開発者は改修のたびに黒ひげ危機一発をしてメンタルをすり減らすことになる。

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パターンで書かれたこのコード片には、以下のような特徴がある。

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

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

  • DBとの接点が最小限に抑えられている。DBとのやり取りの窓口はgetsaveの2箇所のみに限られるため、「いつどこでHogeテーブルが書き換えられたり参照されるか」の把握が容易
  • ロジックが散らばらない。「Hogeテーブルの各値は、どういう条件・状態のときにどういう値に書き換わり、どこでどう使われるのか」は、Hogeオブジェクトのロジックの内部を見れば大体把握できる

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

  • DBが絡むテストが最小限で済む。「Hogeオブジェクトを現在の状態のままきちんと保存できるか」「きちんとHogeオブジェクトに復元し直せるか」のテストだけ書いておけば良い。Hogeオブジェクトの内部状態の更新ロジックはDBには関係ない。

  • 今後の仕様変更などによって内部状態の変化パターンや変化ロジックが増えたとしても、それによってDBへのupdate文を増やす必要はない。

    • HogeRepositoryHogeの現在の状態のままそっくりそのまま保存/取得するので、内部状態がどう複雑に変化しようと関係ない。
  • DBをRDBからKVSとかに変更するようなことがあっても、Repositoryの参照/取得のロジックだけ書き直せば動く。

    • テストの際のDBのモックも容易になる。ただ単にインメモリでオブジェクト保持しておくだけのRepositoryに差し替えたりすればよい。

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

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

Naoto Ikeno

Naoto Ikeno

Backend Engineer, Software Architecture & Design, Perl, Golang, GCP


0 Comments


0 / 1000