クラスインタフェースの活用
「最近のクラス設計はこんな感じ」 に書いたような、 Memcacheにあればそれを、 なければDBからデータを取ってきて使う、 みたいなパターンについて、 最近はまた書き方が変わってきたのでメモしておきます。
標語にすると3つ言いたいことがあって、
- クラスインタフェースを活用せよ。
- コンストラクタは隠蔽せよ。
- 関数はインタフェースと実装の2つの側面を持つ。
ということなんですけど、同じ性質を違う側面から言ってるだけですけどね。
一番古いパターンはこれ。 例では $entryId をキーにして、ブログの記事 $entry を取ってくるのをイメージしています。 記事の入っている箱が Archive です。
class Archive
{
function getEntry($entryId)
{
$memcache = new Memcache(引数省略);
if ($entry = $memcache->get($entryId)) {
return $entry;
}
$pdo = new PDO(引数省略);
$state = $pdo->prepare('SELECT * FROM Archive entryId = ?');
$state->execute(array($entryId));
if ($entry = $state->fetch()) {
$memcache->set($entryId, $entry);
return $entry;
} else {
return false;
}
}
}
$archive = new Archive();
$entry = $archive->getEntry($entryId)
これでも必要十分ではあるのですが、getEntry()が2つの意味で仕事しすぎです。 1つはMemcacheとDBそれぞれの入出力を担当してしまっていること。 もう1つは中でMemcacheとPDOのインスタンスを生成しているので、 モックと挿し替えることができないことです。 この問題点は「ファクトリメソッドが楽しすぎる」でも触れました。 モックと挿し替えられないということはテストがしにくいということで、 モック以外の実用的なものと挿し替えるのも難しいということです。
ただし、呼び出し側が単純なコードで呼べるというのはよいことです。
これを改善したのが「最近のクラス設計はこんな感じ」のスタイルです。
そして最近のパターンだと、 DBから読むのもMemcacheから読むのも 「記事を探してきて返す」という機能は同じだということに注目して、
interface Archive
{
public function getEntry($entryId);
}
class DbArchive implements Archive
{
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function getEntry($entryId)
{
$state = $this->pdo->prepare('SELECT * FROM Archive entryId = ?');
$state->execute(array($entryId));
return $state->fetch();
}
}
class MemcacheArchive implements Archive
{
public function __construct(Memcache $memcache, Archive $backend)
{
$this->memcache = $memcache;
$this->backend = $backend;
}
public function getEntry($entryId)
{
if ($entry = $this->memcache->get($entryId)) {
return $entry;
}
if ($entry = $this->backend->getEntry($entryId)) {
$this->memcache->set($entryId, $entry);
}
return $entry;
}
}
// DBのみを使う場合。
$archive = new DbArchive($pdo);
$entry = $archive->getEntry($entryId);
// MemcacheとDBを併用する場合。
$archive = new MemcacheArchive($memcache, new DbArchive($pdo));
$entry = $archive->getEntry($entryId);
// Memcacheを2つ(local, remote)使い、DBも使う場合。
$archive = new MemcacheArchive($localMemcache,
new MemcacheArchive($remoteMemcache,
new DbArchive($pdo)));
$entry = $archive->getEntry($entryId);
と、こんな感じになります。 クラスインタフェースを定義することで、 DBとMemcacheの担当箇所の切り分けをします。 そして、クラスインタフェースを使い、 バックエンドという抽象的なものを持ち込むことで、 MemcacheとDBの連携が柔軟になります。 これが標語の1つ目の「クラスインタフェースを活用せよ」に対応する話です。
ただしこの書き方だと、 呼出し側がデータの保存の仕組みを把握しておく必要がありますので、 適度に隠蔽する必要があります。 それには例によってファクトリメソッドを使います。 この場合はファクトリ専門のクラスを作った方がよさそうです。
class Blog
{
// 他のメソッドは省略
public function getArchive()
{
return new MemcacheArchive(
$this->localMemcache,
new MemcacheArchive($this->remoteMemcache,
new DbArchive($this->pdo)));
}
}
$archive = $blog->getArchive();
$entry = $archive->getEntry($entryId);
たとえばこんな感じになります。 もし、$archive = new Archive(); という書き方に拘ったとすると、 ちょっと書きにくいなあと思います。 そのあたりが「コンストラクタは隠蔽せよ」にかかってきます。 これは別項を設けた方がいいのかなあ。 モジュールの外部に出すインタフェースとして、 コンストラクタは避けた方がいいと思っています。
このようにすることで、
- 記事を取得する = Memcacheから記事を取得する + DBから記事を取得する
- DBから記事を取得する = DBにSQL文を発行する
と、それぞれの関数は言葉にすれば1つのことをしているとも言えるし(左側)、 複数の処理をまとめたものとも言える(右側)のですが、 その差をできるだけ少なくしていくと読みすいんだろうなと思います。