デザインパターン Factory method (php&ruby)

デザインパターンのFactory methodは、インスタンスするオブジェクトをサブクラスが決めます。インスタンスされるオブジェクトはインターフェースクラスによりI/Fを規定されているため、共通のAPIが実装されます。
このことにより、インスタンスするオブジェクトの種類が増えても、インスタンスの方法をサブクラスに集約するだけで済みます。
たとえば、とあるバッチ処理が排他制御を仕様としてもっているとします。排他制御を実現するために、ここではふたとおりの方法を提案します。
ひとつは、実行開始時にロックファイルを用意して、実行完了時にロックファイルを削除する方法です。実行開始時にロックファイルを検知すると、他に実行中のプロセスがあると判断して処理を中断しますが、いちいちディスクIOが発生します。
もうひとつは、実行開始時にプロセスIDを確認して、カレントのプロセスID以外に実行しているものがあれば多重実行と判断して処理を中断する方法です。こちらの方が高速でIOも発生しませんが、セキュリティによってシステムコールが制限されているとか、ホームディレクトリより上を参照できないような環境ではうまく動かない可能性があります。
汎用性を考慮して、両方の機能を環境条件によって使い分けられるようにしておきたいと思います。
普通にコーディングすると、こういう場合は利用側のスクリプト内に環境条件による場合わけをif文やswitch文などを使って、インスタンスするオブジェクトを選べるようにしていくと思います。
しかしこれだとインスタンスする種類が増えた場合に可読性が落ちていきますし、インスタンスされるオブジェクトのインターフェースが統一されていないと、利用時にまた場合わけしていかなければならなくなります。
Fatcory methodを使うと、場合わけはそれを専門におこなうサブクラスの一箇所に集中できるので、運用中の変更であっても他に予期せぬ影響が発生しづらくなります。
また、インスタンスされるオブジェクトは共通のAPIを持つようインターフェースクラスによる拘束を受けるため、利用側から見ると、どのオブジェクトであっても使用方法が同じとなり、利用側のソースに手を加える必要がなくなります。
ともあれ百聞は一見にしかず。クラス図をみてみまょう。
Factory_method_pattern_2
インスタンスするオブジェクトを条件によって場合わけするサブクラスは、この図の左下のLockFactoryクラスです。どういうロック(排他制御)を提供するかは、このLockFactoryクラスだけが知っています。
逆に言えば、ロックの選定方法を修正したい場合は、ここだけ手を加えればよい、ということになります。
一方、インスタンスされるオブジェクトは右下のLockFileControlクラス(ファイルによる排他制御)とLockPidControlクラス(プロセスIDによる排他制御)になります。このふたつのクラスはインターフェースクラスのProductクラスに、実装しなければならないインターフェースの拘束を受けているため、必然的に共通のインターフェース(listen()、exec()、close())が実装されることになります。
たとえばここに、実行履歴の管理も加えたいからTokyoCabinetを利用して排他制御をおこなおう、という仕様変更が発生しても、LockTcControlクラスのようなものを新規で用意して、その採用条件をLockFactoryクラスに追加えてあげればよい、ということになります。
Factoryは工場という意味なので、ここでは「なにをつくるか」だけに責任を持ち、「どうやってつくるか」は責任範囲外です。
Productは生産品という意味なので、ここでは「どうやってつくるか」という、その生産品を生産品たらしめん構成要素だけに着目します。
たとえばお菓子工場であれば、クッキーやチョコレートを並行製造していますが、クッキーやチョコレートそれ自体は自分以外にどんなお菓子がつくられているかということは知っておく必要はありません。クッキーはクッキーたらしめん構成要素として、小麦粉や砂糖の含有率や使用される香料に責任を持っていればいいわけです。
一方、工場はクッキーがどんな香料を使用されているか管理する必要はありません。クッキーのつもりでチョコレートを作らないよう、条件にそった生産ラインを維持すればよい、ということになります。
クッキーやチョコレートは、今回のケースでいうところのファイルによる排他制御か、プロセスIDによる排他制御か、ということになります。その排他制御の実現方法はそれぞれのクラスだけが知っていればいいことです。
一方、場合わけクラスは工場として、その環境条件に合致した排他制御を提供してあげればよい、ということです。
こうしてお互いの責任範囲を明確にしてソースコードを分けることで、依存関係が緩くなり(疎結合)、改修や機能拡張に強い仕組みになります。
こうした仕組みでつくられたシステムを、堅牢性の高いシステムと呼び、顧客やエンドユーザ、しいては開発者を幸せにしていきます。
長くなりました。
さっそく実際のコードをみてみましょう。例によってphpとrubyで実装しています。
まずphpです。
Factory.class.php (Factoryクラス)

<?php
Abstract class Factory
{
public final function create(Array $argv) {
return $this->createProduct($argv);
}
protected abstract function createProduct(Array $argv);
}

LockFactory.class.php (LockFactoryクラス)

<?php
Class LockFactory extends Factory
{
const FILE_MODE = 1;
const PID_MODE = 2;
protected function createProduct(Array $argv) {
$mode = 1;
if (isset($argv[1]) && intval($argv[1]) > 0) {
$mode = intval($argv[1]);
}
switch ($mode) {
case self::FILE_MODE:
$lock = new LockFileControl($argv[0]);
break;
case self::PID_MODE:
$lock = new LockPidControl($argv[0]);
break;
default:
echo "invalid mode: ${mode}\n";
exit;
}
return $lock;
}
}

Product.class.php (Productクラス)

<?php
Interface Product
{
public function listen();
public function exec();
public function close();
}

LockFileControl.class.php (LockFileControlクラス)

<?php
Class LockFileException extends Exception {}
Class LockFileControl implements Product
{
const LIMIT_TERM = 10;
private $_file = null;
private $_limit_term = null;
public function __construct($file, $limit_term = null) {
$this->_file = $file . '.lock';
$this->_limit_term = $limit_term ? $limit_term : self::LIMIT_TERM;
}
public function listen() {
if (($work_time = $this->_getWorkTime()) !== false) {
if ($work_time > $this->_limit_term) {
try {
$ret = $this->close();
} catch (LockFileException $e) {
throw new LockFileException('deadlock: ' . $e->getMessage());
}
if (! $ret) {
throw new LockFileException('deadlock: ' . $this->_file);
}
return false;
}
}
return (bool)$work_time;
}
public function exec() {
$ret = touch($this->_file);
if (! $ret) {
throw new LockFileException('failed to lock: ' . $this->_file);
}
return true;
}
public function close() {
$ret = unlink($this->_file);
if (! $ret) {
throw new LockFileException('failed to unlock: ' . $this->_file);
}
return true;
}
private function _getWorkTime() {
clearstatcache();
if (! is_file($this->_file)) {
return false;
}
return time() - filemtime($this->_file);
}
}

LockPidControl.class.php (LockPidControlクラス)

<?php
Class LockPidException extends Exception {}
Class LockPidControl implements Product
{
const LIMIT_TERM = 10;
private $_pname = null;
private $_limit_term = null;
private $_pid = null;
public function __construct($pname, $limit_term = null) {
$this->_pname = $pname;
$this->_limit_term = $limit_term ? $limit_term : self::LIMIT_TERM;
}
public function listen() {
if (($work_time = $this->_getWorkTime()) !== false) {
if ($work_time > $this->_limit_term) {
exec("kill -KILL " . $this->_pid);
if ($this->_getWorkTime()) {
throw new LockFileException('deadlock: ' . $this->_pid);
}
return false;
}
}
return (bool)$work_time;
}
public function exec() {
return true;
}
public function close() {
return true;
}
private function _getWorkTime() {
$cmd = "ps x | grep " . $this->_pname . " | grep -v grep | awk '{print \$1}'";
exec($cmd, $pids);
if (count($pids) == 1 && intval($pids[0]) == getmypid()) {
return false;
}
$pids = array_diff($pids, array(getmypid()));
$this->_pid = intval(array_shift($pids));
return time() - filemtime('/proc/' . $this->_pid);
}
}

factory_method_client.php (利用側)

<?php
require 'Factory.class.php';
require 'LockFactory.class.php';
require 'Product.class.php';
require 'LockFileControl.class.php';
require 'LockPidControl.class.php';
$factory = new LockFactory();
$lock = $factory->create($argv);
try {
if ($lock->listen()) {
echo "quit executing!\n";
exit;
}
if ($lock->exec()) {
echo "start working...\n";
#job some
sleep(5);
}
if ($lock->close()) {
echo "...end working\n";
}
} catch (Exception $e) {
echo $e->getMessage() . "\n";
}

phpは以上です。
次にrubyを見てみましょう。
Factory.class.rb (Factoryクラス)

class NotImplements < Exception;end
class Factory
def create(argv)
createProduct(argv)
end
def createProduct(argv)
raise NotImplements
end
protected :createProduct
end

LockFactory.class.rb (LockFactoryクラス)

class LockFactory < Factory
FILE_MODE = 1
PID_MODE = 2
def createProduct(argv)
mode = 1
mode = argv[1].to_i if argv[1].to_i > 0
case mode
when FILE_MODE
lock = LockFileControl.new(argv[0])
when PID_MODE
lock = LockPidControl.new(argv[0])
else
print "invalid mode: #{mode}\n"
exit
end
lock
end
protected :createProduct
end

Product.class.rb (Productクラス)

class NotImplements < Exception;end
module Product
def listen
raise NotImplements
end
def exec
raise NotImplements
end
def close
raise NotImplements
end
end

LockFileControl.class.rb (LockFileControlクラス)

class LockFileException < Exception;end
class LockFileControl
include Product
LIMIT_TERM = 10
def initialize(file, limit_term = nil)
@_file = file + '.lock'
@_limit_term = limit_term ? limit_term : LIMIT_TERM
end
def listen
if (work_time = getWorkTime) != false
if work_time > @_limit_term
begin
ret = close
rescue
raise "deadlock: #{$!}"
end
unless ret
raise "deadlock: #{@_file}"
end
return false
end
end
return false if work_time == 0 || work_time == false
true
end
def exec
begin
File.open(@_file, 'w').close
rescue
raise "failed to lock: #{@_file}"
end
true
end
def close
begin
File.unlink(@_file)
rescue
raise "failed to unlock: #{@_file}"
end
true
end
def getWorkTime
return false unless File.exist?(@_file)
Time.now.to_i - File.mtime(@_file).to_i
end
private :getWorkTime
end

LockPidControl.class.rb (LockPidControlクラス)

class LockPidException < Exception;end
class LockPidControl
include Product
LIMIT_TERM = 10
def initialize(pname, limit_term = nil)
@_pname = pname
@_limit_term = limit_term ? limit_term : LIMIT_TERM
@_pid = nil
end
def listen
if (work_time = getWorkTime) != false
if work_time > @_limit_term
`kill -KILL #{@_pid}`
raise "deadlock: #{@_pid}" if getWorkTime
return false
end
end
return false if work_time == 0 || work_time == false
true
end
def exec
true
end
def close
true
end
def getWorkTime
ret = `ps x | grep #{@_pname} | grep -v grep | awk '{print \$1}'`
pids = ret.split
return false if pids.size == 1 && pids[0].to_i == $$
pids = pids - [$$]
@_pid = pids.shift
Time.now.to_i - File.mtime("/proc/#{@_pid}").to_i
end
private :getWorkTime
end

factory_method_client.rb (利用側)

require 'Factory.class.rb'
require 'LockFactory.class.rb'
require 'Product.class.rb'
require 'LockFileControl.class.rb'
require 'LockPidControl.class.rb'
factory = LockFactory.new
lock = factory.create(ARGV.unshift(__FILE__))
begin
if lock.listen
print "quit excuting!\n"
exit
end
if lock.exec
print "start working...\n"
#job some
sleep 5
end
if lock.close
print "...end working\n"
end
rescue
print "#{$!}\n"
exit
end

ruby は以上です。
排他制御の方法を選定する環境条件は、ここではコマンドラインに与えた引数としています。1でファイルによる排他制御、2でプロセスIDによる排他制御です。
一度実行すると、このバッチ処理は実行時間に5秒を要します。そのあいだの多重実行は排他制御により防止されます。デッドロック判定は10秒です。
phpの実行結果: (rubyも結果は一緒)

php ./factory_method_client.php 1 &
[1] 22747
start working...
php ./factory_method_client.php 1 &
[2] 22748
quit executing!
[2]+  Done                    php ./factory_method_client.php 1
...end working
[1]+  Done                    php ./factory_method_client.php 1
php ./factory_method_client.php 2 &
[1] 22750
start working...
php ./factory_method_client.php 2 &
[2] 22756
quit executing!
[2]+  Done                    php ./factory_method_client.php 2
...end working
[1]+  Done                    php ./factory_method_client.php 2

まとめ:
そしてrubyってabstractとかfinalとかの修飾子って使えないんでしょうか。ちょっと調べてもよく分からなかったのでスルーしました。アジャイル開発向けということで僕自身もアジャイルに(笑)。
あと、開発時にシステム要件って決まってると思うので、あんまりこういう例のような使い方しないかもしれませんね。でも自分のつくったモジュールをいろんな開発案件で使いまわして開発コストをさげる、という意味では有効だと思います。ファイルによる排他制御しか使わないなら、LockFactoryクラスの条件分岐を固定してしまえばいいわけですから。
排他制御だけでなく、他にも複数の読み込むファイル形式による違い、複数のHTTP通信先による違い、複数の課金APIによる違い、などで加工方法が分かれるような場合にもこのパターンが使えます。
某ISPの基幹システムでもこれが使われていましたが、パターンを知っていると暗黙の共通語として機能するので解析やソースリーディングの助け舟にもなってくれますね(^^)