ドメイン駆動設計という救世主 - PHPでデータベースを扱う(2)

Posted by Hiraku on 2012-06-10

前回の「DAOの悪夢 - PHPでデータベースを扱う(1)」の続きです。

DAOの問題点整理

前回、ぶくぶく膨れ上がったDAOを前に困っているという話を書きました。何が問題なのか整理するとこんな形になります。

  1. DAOクラスを分割できない。メソッドが増えすぎて管理しきれない
  2. メソッド同士のインターフェースがバラバラで管理しきれない。stringを要求したりintを要求したり、はたまたarray()で名前付き引数のように渡すことを要求したり。
  3. 引数が安全である保証がないので、全メソッドでバリデーションが必要。(日付として正しい文字列か、など。)
  4. ドメインロジックを書く場所がない。書きにくい。

ひとつひとつ対応を考えていきましょう。

クラスが分割できない理由

データベースを扱うのであれば、必ずコネクションをどこかで管理しなければなりません。つまりはPDOのインスタンスDBのリソースですね。DAOクラスがある場合はこのクラスの中でやればよかったのですが、クラスが分割しにくくなってしまいます。

DAOを分割するには、まずDBとの接続情報をどこか別の場所で管理してやる必要があります。大抵の場合、一つのアプリケーションで使うDBは数えるほどしかなく、しかも一度接続すればそのコネクションを使いまわせばいいはずです。要は、PDOのインスタンスが共有できればいいのです。

PDOインスタンスの共有

共有といっても別に難しくなくて、単にDBクラスの中でインスタンス化するのを避けて、アプリケーション全体で参照できる場所に置いておくだけです。

もっとも安直な方法はグローバル変数にしておくことです。$GLOBAL['my_pdo']でPDOのインスタンスがいつでも取り出せます。

<?php
$my_pdo = new PDO(/* ... */);

しかし、グローバル変数だと少々問題があります。

  • 誰でも上書きできるため、変数の中にPDOオブジェクトが入っている保証がない

  • new PDOが必ず実行されるため、DBを使わない場合でも接続してしまい、無駄が発生する

そこで、変数でなく関数にしてみます。一度生成したPDOインスタンスはstatic変数にキャッシュしておくようにしましょう。

<?php
function getPDO() {
  static $pdo;
  if (!isset($pdo)) {
    $pdo = new PDO(/* ... */);
  }
  return $pdo;
}

あとはgetPDO()でいつでもPDOのインスタンスが取り出せます。関数は上書きできないため、戻り値が保証されますし、最初に呼び出した際にインスタンスが生成されるためDBを使わない場合はDBに接続しません。要件をすべて満たします。

本当はDIコンテナを使ったほうがモダンなのですが、今回は依存関係も少ないですし関数でいきましょう。

DBとのコネクションはDAOクラスが管理しなくてよくなったので、分割しやすくなりました。分割の単位としてはテーブル別にするのがわかりやすいと思いますが、特にこれといった決まりはありません。

//Userテーブルに関する操作クラス
class UserMapper {
  private $_pdo;
  function __construct(PDO $pdo) {
    $this->_pdo = $pdo;
  }
  //...
}

//Hogeテーブルに関する操作クラス
class HogeMapper {
...
}

//使い方
$userMapper = new UserMapper(getPDO());
//...

上記のようにインスタンス内で必要なオブジェクトをnewする側が渡して初期化するスタイルはDependency Injection(DI、依存性の注入)パターンの一種で、特にコンストラクタ・インジェクションと呼んだりします。

今回の主題とはあまり関係ないので詳しい説明は省きます。


インターフェースの統一

残りの問題点、インターフェースの統一に関して言えば、解決方法は簡単です。連想配列を使うのをやめてクラス化すればいいのです。

例として、6つのフィールドを持つUserテーブルを扱うコードを考えてみましょう。型がちょっと適当ですが説明のためということで。

userIdINTEGER主キー。AUTO INCREMENT
nicknameTEXT
passwordTEXT
firstnameTEXT
lastnameTEXT
birthdayDATE

Userテーブルに対してデータをinsertするメソッドを考えます。愚直なこんなインターフェースはどうでしょうか?

class UserMapper {
  //...
  function insert(
    $nickname,
    $password,
    $firstname,
    $lastname,
    $birthday
  ) {
    //...
  }
}

…この引数を覚えていられる人はそう多くないと思います。もしプログラムを間違えて、nicknameとfirstnameを逆に指定してしまったら、本名がnicknameとして登録されてしまいます。本名が誤って表示されてしまうという致命的な事故につながります。危なっかしいプログラムですね。

では、順番を考慮しなくていいように連想配列にすればどうでしょうか?

class UserMapper {
//...
  function insert(array $data) {
    //...
  }
}
//--------------
//使いかた
$userMapper->insert(array(
  'nickname' => 'foo',
  'password' => 'foo',
  'firstname' => 'taro',
  'lastname'  => 'tanaka',
  'birthday'  => new DateTime('1980-01-01'),
));

すっきりはしましたが、残念ながら問題の解決にはなっていません。連想配列中に必要要素が全部そろっていると、誰が保証してくれるのでしょうか? 要素が足りなかった場合や、おかしな値が渡ってきた際にどうするべきか、エラー処理をちくちく実装しなければなりません。

それに、このinsertメソッドを使う側からしても、連想配列に何を含めておくべきなのか、わかりにくいです。ドキュメントがない場合はinsertメソッドの実装を読まなければなりません。省略した場合はどうなるのか、致命的なエラーになるのか規定値が勝手に入るのか、そういった仕様が各メソッドに分散してしまいます。


そこで、クラスをきっちり使いましょう、ということになります。

まずやるべきは、テーブルの1行1行に対応するクラスを作ることです。

class User {
  public
    $userId
  , $nickname
  , $password
  , $firstname
  , $lastname
  , $birthday
  ;
}

上のUserクラスは全要素がpublicになっているのでカプセル化できておらず、イケてない実装です。まともな実装は少し複雑になるので次回書きます。

間違えてはいけないのが、あくまで行をクラス化することであって、テーブルをクラス化するのではないということです。よく、テーブル全体をクラス化してデータのやり取りは連想配列で行っている例を見ますが、クラス化の優先度が間違っています。個人的には、テーブルのクラス化なんて行に比べればどうでもよくて、行さえクラス化されていれば、巨大なDAOで全データをやり取りしていても問題ないとさえ思います。

とにかく、行がきっちりクラスとして定義されていれば、インターフェースの統一は非常に簡単になります。insert, update, deleteなどは単にUserクラスを渡すものとしてタイプヒントしておくだけです。

class UserMapper {
  function insert(User $user) { /*...*/ }
  function update(User $user) { /*...*/ }
  function delete(User $user) { /*...*/ }
}

連想配列を使った例とよく似ていますが、メンテのしやすさは桁違いに向上します。

  • 引数の順番…当然考慮しなくてよい
  • 引数の保証…Userクラス側でisValid()のような検証メソッドを用意したり、そもそもsetterで不正なデータをブロックすることができるため、変なデータが入り込む余地を消せる。
  • メソッドのわかりやすさ…Userクラスの定義を読めばよい。どんな値を渡せばいいのかはすべてUserクラスに書いてある。各メソッドごとに実装を読まなくてもよい。

そもそも、「クラス」とはユーザー定義型の一種です。型を定義すればこれらが解決するのは当然ですね。

上のUserMapperクラスには書いてありませんが、DBからデータを取ってくるselect系メソッドに関しても、UserクラスにWrappingして値を返した方がわかりやすくなります。データのやり取りは常にUserクラスに統一しておくのです。

ドメインロジックの実装場所

巨大なるDAOの問題点として、ドメインロジックを書く場所がない、というものもありました。

たとえば、「Userのフルネームを取得する」ことを考えてみます。lastnameとfirstnameを連結するだけですが、

  • 順番はlast→firstでよいのか
  • 間に半角スペースを入れるのか全角スペースを入れるのか

などなど、ちゃんと定義しておいた方が統一感が出るでしょう。このとき、連想配列でデータをやり取りしていれば、こんな書き方になります。

function getFullname($last, $first) {
  return "$last $first";
}
//使い方
getFullname($user['lastname'], $user['firstname']);

もしここで引数をarray $userなどにすれば、また値の保証の問題が発生します。

先ほどのinsert,updateなどと同様に、Userがクラスとしてきっちり定義されていれば、こう書けます。

function getFullname(User $user) {
  return "{$user->lastname} {$user->firstname}";
}
//使い方
getFullname($user);

…というかそもそも、Userはクラスであり、それ自身がメソッドを持てるのですから、こんな書き方もできますよね。

class User {
  //...
  function getFullname() {
    return "{$this->lastname} {$this->firstname}";
  }
}
//使い方
$user->getFullname();

この最後の書き方が一番望ましいです。Userにしか関係ないメソッドなのですから、Userの定義に書いた方がわかりやすいですし、意図も明確になります。そもそもこうやって、データに紐づくロジックを直接、型に書いてしまえるのがオブジェクト指向の最大の特徴だったはずです。

「行をクラス化する」ことには、ロジックを直接クラス中に書けるようになるというメリットもあるのです。

ドメイン駆動設計(Domain Driven Design)

こういう、「行をクラスとして定義し、そこにドメインロジックを集約し、全ての処理は行クラスの単位で執り行う」スタイルをドメイン駆動設計といいます(…と言い切ると、語弊があるんですけど…)。RDBMSを使ったよくあるアプリケーションの場合は「行=ドメインモデル」と見なせることが多いので、「行をクラス化する」≒「ドメイン駆動設計」が成り立ちます。

なんか、ドメイン駆動の話を調べていると抽象的な小難しい解説が多いのですが、大雑把にまとめてしまえば、「ちゃんとオブジェクト指向を使いこなし、ちゃんとMVCアーキテクチャで作りましょう」という当たり前のことを言っているだけです。オブジェクト指向を理解できていれば、実践するのはそんなに難しくありません。


まとめ

いったんまとめます。PHPでデータベースを扱うときの心得。

  • まずDBコネクションの管理を分離すること。クラスを分割しやすくなる。
  • 行をクラスで定義し、連想配列を使わないこと。
  • 要はオブジェクト指向を使いこなしましょうということ。

今回は個別の対応方法ばかり書いたので、ちょっと抽象的になってしまいました。次回、具体的な実装例を書いてみたいと思います。


補足

ちなみに、勘のいい方は「insertやupdateも行クラスのメソッドにできるのでは?」と思うかもしれません。

class User {
  //...
  function insert() {/*...*/}
  function update() {/*...*/}
}

//使い方
$user = new User;
$user->nickname = 'foo';
//...
$user->insert(); //DBに保存

実はその通りで、このような「行クラス自体にデータベースとのやり取りのロジックを含めてしまう」書き方をActive Recordパターンと呼びます。ただし、メリットとデメリットがあるので、これも次回詳しく書きます。

keyword: PHP DAO デザインパターン DDD

PHPの最新記事