デザインパターンのFactory methodは、インスタンスするオブジェクトをサブクラスが決めます。インスタンスされるオブジェクトはインターフェースクラスによりI/Fを規定されているため、共通のAPIが実装されます。
このことにより、インスタンスするオブジェクトの種類が増えても、インスタンスの方法をサブクラスに集約するだけで済みます。
たとえば、とあるバッチ処理が排他制御を仕様としてもっているとします。排他制御を実現するために、ここではふたとおりの方法を提案します。
ひとつは、実行開始時にロックファイルを用意して、実行完了時にロックファイルを削除する方法です。実行開始時にロックファイルを検知すると、他に実行中のプロセスがあると判断して処理を中断しますが、いちいちディスクIOが発生します。
もうひとつは、実行開始時にプロセスIDを確認して、カレントのプロセスID以外に実行しているものがあれば多重実行と判断して処理を中断する方法です。こちらの方が高速でIOも発生しませんが、セキュリティによってシステムコールが制限されているとか、ホームディレクトリより上を参照できないような環境ではうまく動かない可能性があります。
汎用性を考慮して、両方の機能を環境条件によって使い分けられるようにしておきたいと思います。
普通にコーディングすると、こういう場合は利用側のスクリプト内に環境条件による場合わけをif文やswitch文などを使って、インスタンスするオブジェクトを選べるようにしていくと思います。
しかしこれだとインスタンスする種類が増えた場合に可読性が落ちていきますし、インスタンスされるオブジェクトのインターフェースが統一されていないと、利用時にまた場合わけしていかなければならなくなります。
Fatcory methodを使うと、場合わけはそれを専門におこなうサブクラスの一箇所に集中できるので、運用中の変更であっても他に予期せぬ影響が発生しづらくなります。
また、インスタンスされるオブジェクトは共通のAPIを持つようインターフェースクラスによる拘束を受けるため、利用側から見ると、どのオブジェクトであっても使用方法が同じとなり、利用側のソースに手を加える必要がなくなります。
ともあれ百聞は一見にしかず。クラス図をみてみまょう。
インスタンスするオブジェクトを条件によって場合わけするサブクラスは、この図の左下の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の基幹システムでもこれが使われていましたが、パターンを知っていると暗黙の共通語として機能するので解析やソースリーディングの助け舟にもなってくれますね(^^)