The Backyard - CommonsChains Diff
- Added parts are displayed like this.
- Deleted parts are displayed
like this.
!Rubyを使って試すChains
一連の処理を実行する場合の方法論として、たとえばテンプレートメソッドがあるが、ここではコマンドパターンとして複数のクラスに分割した処理と、全体の制御をチェインオブレスポンサビリティとして実装してみる。
といっても元ネタがあって、そのJavaのコードをRubyに置き換えただけのことだ。といっても、元のコードは読んでいないが。だからホンモノはいろいろと例外時の考慮がされているだろうが、ここでは最低限しかしない。
!!元ネタ
http://www.onjava.com/pub/a/onjava/2005/03/02/commonchains.html?page=1
Jakarta-Commons2004年12月からのメンバー[[Chains|http://jakarta.apache.org/commons/chain/]]についての解説記事。
!!ではなぜRuby
元ネタとして挙げた記事ではさらりと書いてあるが、ソースのzipには(readme.txtなども含めて)100以上のファイルが入っている。
動作原理を見るだけには、いささか大げさじゃないか。というわけで、Rubyを使ってみる。もっとも、Rubyでは複数のクラスを1つのファイルへ入れられるから単純な比較はフェアではないが。
また、構成ファイル(カタログと呼ぶ)としてXMLの代わりに、直接Rubyの配列とハッシュを使って単純化している部分もある。
とはいえ、結構、クリアではなかろうか?
!実行すべきもの
例は、元ネタとほとんど同じにしてある。車の購入処理フローだ。
下のリストは処理と対応するクラスを示したもの。ただし、WikiNameになることを防ぐために先頭文字を小文字にしてある)
#顧客情報の取得(getCustomreInfo)
#試乗(testDriveVehicle)
#商談(negotiateSale)
#支払方法の決定(arrangeFinacing)
#商談成立(closeSale)
を順に実行する。
また、Chainsは終了時に、それまでに実行した処理を逆順に呼び出すことで、退出時処理を可能としている。ここでは、(逆順に呼ばれることを利用して)最初に正常処理時には何もしないかわりに退出時に例外が発生していたらその情報を出力する処理(exceptionHandler)[[Professional resumes|http://cvresumewritingservices.org/professional-resume.php]] を先頭に置く。これも元ネタと同様である。
!!アプリケーション
sample.rb
#!/usr/bin/ruby -Ks
require 'chain'
class ExceptionHandler
def execute(c)
puts 'Filter.execute'
end
def post_execute(cx, e)
p e if !e.nil?
end
end
class GetCustomerInfo
def execute(c)
c.customer_name = 'Foo the Man'
puts 'GetCustomerInfo'
end
end
class TestDriveVehicle
def execute(c)
puts 'TestDriveVehicle'
end
end
class NegotiateSale
def execute(c)
puts 'NegotiateSale'
end
end
class CloseSale
def execute(c)
puts 'CloseSale'
end
end
class ArrangeFinancing
def execute(c)
raise RuntimeError, c.customer_name + ' is blacklisted'
end
end
class Context < Hash
def initialize()
@customer_name = nil
end
attr_accessor :customer_name
end
if $0 == __FILE__
c = Catalog.load_catalog('config.rb')
chain = c.get_chain("sell-vehicle")
chain.execute(Context.new)
end
各処理(コマンドと呼ぶ。というかコマンドパターンなのでそのまんまというか)が明示的にtrue(TrueClassのインスタンス)を返すか、例外を投げない限り、指定されたシーケンスに従ってChainsは呼び出しを行う(このシーケンスをチェインと呼ぶ)。
この呼び出しは、executeという名前のメソッドにコンテキストを与えることで行われる。コンテキストは文字通り実行時コンテキストを保持するオブジェクトである。例では、Contextとしてハッシュの派生クラスとして実装している。
""ちなみに、DIは、明示的なコンテキストをパラメータとして与える代わりに直接オブジェクトの属性を設定する方法という言い方もできるだろう。コンテキストを利用する場合の問題点は何が入っているかわからなくなること、属性を設定するDIと異なり、書き込みを許すために途中の変更や追加が見えにくくなること、それを防ぐためには明示的なメソッドが必要となり(例では、customer_nameというアクセサで示されている。ここでは単なるアクセサだが、まともなたとえばログ付きメソッドにすれば途中の変更や読み込みを監視することが可能となる)、結果的に頻繁な変更がコンテキスト自身に必要になることなどが挙げられる。(ここは書き方が甘いので追及は不可)
ここでは各クラスのexecuteの実装は基本的に自分のクラス名を出力するだけだ。
呼び出すべきチェインのすべてのコマンドが完了するか、または例外で中断された場合、Chainsは逆順に終了後処理を呼び出す。このメソッドはpost_executeという名称である。Rubyによる実装ではrespond_to?を利用して定義されていれば呼ぶということにしてある。引数はコンテキストと例外オブジェクト。正常に終了した場合、例外オブジェクトにはnilが設定されることになる。
実際のアプリケーションでは、例外時には部分トランザクション(既にコミット済み)の明示的なロールバック処理や、取消メッセージの送信の実行となるだろう。
ちなみに、exceptionHandler(WikiName防御だ。本当はEで始まる)のexecuteで出力されているFilterというのは、CommonsChainsで、post_executeメソッドを持つクラスが実装するインターフェイス名である。
!!構成ファイル
既に書いたように構成ファイル(カタログ)自身はRubyのスクリプトとして実装されている。
カタログの構成は、トップレベルのカタログ(カタログ名はシンボルnameで示す)の中に、文字列(チェイン名となる)をキーとした複数のチェインだ。また、各チェインは配列として実装され、コマンドを示すハッシュを要素とする。コマンドは実行すべきコマンドクラスのクラス名か、またはネストして呼び出されるカタログのチェイン名を保持する。
config.rb
catalog = {
:name => 'auto-sales',
'sell-vehicle' => [
{
:id => 'exception_handler',
:class => ExceptionHandler,
},
{
:id => 'get_customer_info',
:class => GetCustomerInfo,
},
{
:id => 'test_drive_vehicle',
:class => TestDriveVehicle,
},
{
:id => 'negotiate_sale',
:class => NegotiateSale,
},
{
:catalog => 'auto-sales',
:name => 'arrange-financing',
:optional => true,
},
{
:id => 'close_sale',
:class => CloseSale,
},
],
'arrange-financing' => [
{
:id => 'arrange_financing',
:class => ArrangeFinancing,
},
],
}
(解説が必要そうなら後で書くつもり)
この実装では、「支払方法の決定」は別チェインとして示される。
!!Chains実装
Chainsは2つのクラス(と1つの補助的なクラス)から構成されている。
:Catalog:カタログを示す。クラスメソッドとしてカタログの読み込み処理(とその保持)であるload_catalogと、指定されたカタログのインスタンスを返すget_catalogを持つ。インスタンスメソッドには、チェインのインスタンスを返すget_chainだけである。なお、ここではCatalogはあるカタログのインスタンスのファクトリを兼ねさせているためコンストラクタそのものはプライベートとして実装している。
:Chain:チェインを示すクラスである。ネストしたチェインの場合、例外の動作や退出呼び出しの動作が異なるため、それを示す属性を持つ。
:Command:チェインの内部クラスとして、コマンドのインスタンス化を担当する。
chain.rb
class Catalog
@@catalog = {}
def get_chain(name, nested = false)
chain = @catalog[name]
if chain.nil?
raise RuntimeError, name + ' not found'
end
Chain.new(chain, nested)
end
def self.load_catalog(catfile)
catalog = nil
File.open(catfile, 'r') do |f|
eval(f.read)
end
if catalog.nil? || catalog[:name].nil?
raise RuntimeError, catfile + ' is not a valid catalog'
else
@@catalog[catalog[:name]] = catalog
end
Catalog.new(catalog)
end
def self.get_catalog(name)
Catalog.new(@@catalog[name])
end
private
def self.new(c)
super(c)
end
def initialize(cat)
@catalog = cat
end
end
class Chain
class Command
def initialize(c)
@obj = c[:class].new
@id = c[:id]
end
def execute(ctx)
@obj.execute(ctx)
end
def post_execute(cx, e)
@obj.post_execute(cx, e) if @obj.respond_to?(:post_execute)
end
end
def initialize(cmd, nested)
@nested = nested
@commands = []
cmd.each do |c|
if Class === c[:class]
@commands << Command.new(c)
else
jump = nil
if !c[:catalog].nil?
cat = Catalog.get_catalog(c[:catalog])
if !cat.nil? && !c[:name].nil?
jump = cat.get_chain(c[:name], true)
end
end
if jump.nil?
if !c[:ooptional]
raise RuntimeError, c.inspect + ' is not valid chain'
end
else
@commands << jump
end
end
end
@bt = nil
end
def execute(cx)
e = nil
result = nil
@bt = []
@commands.each do |c|
@bt << c
begin
result = c.execute(cx)
break if result == true
rescue StandardError => e
raise e if @nested
break
end
end
post_execute(cx, e) unless @nested
result
end
def post_execute(cx, e)
@bt.reverse_each do |x|
begin
x.post_execute(cx, e) if x.respond_to?(:post_execute)
rescue
# ignore exception
end
end
end
end
!!実行例
もちろん、こうなる。各コマンド実装クラスの中身を替えたりして試してみよう。たとえば、trueを返すようにするとか。
C:\Home\arton\test\chain>ruby sample.rb
ruby sample.rb
Filter.execute
GetCustomerInfo
TestDriveVehicle
NegotiateSale
#<RuntimeError: Foo the Man is blacklisted>
!!ソースファイル
[[chain.zip|http://arton.no-ip.info/data/chain.zip]]
このzipの中身はちょっと古い。Command#executeとCommand#initializeが異なる(@btの初期化が1回だけのため、複数回のexecute呼び出しに対応できない)。
!感想
リワインド処理は(何を今更という気も自分に対してするのだが)目からウロコだった。
一連の処理を実行する場合の方法論として、たとえばテンプレートメソッドがあるが、ここではコマンドパターンとして複数のクラスに分割した処理と、全体の制御をチェインオブレスポンサビリティとして実装してみる。
といっても元ネタがあって、そのJavaのコードをRubyに置き換えただけのことだ。といっても、元のコードは読んでいないが。だからホンモノはいろいろと例外時の考慮がされているだろうが、ここでは最低限しかしない。
!!元ネタ
http://www.onjava.com/pub/a/onjava/2005/03/02/commonchains.html?page=1
Jakarta-Commons2004年12月からのメンバー[[Chains|http://jakarta.apache.org/commons/chain/]]についての解説記事。
!!ではなぜRuby
元ネタとして挙げた記事ではさらりと書いてあるが、ソースのzipには(readme.txtなども含めて)100以上のファイルが入っている。
動作原理を見るだけには、いささか大げさじゃないか。というわけで、Rubyを使ってみる。もっとも、Rubyでは複数のクラスを1つのファイルへ入れられるから単純な比較はフェアではないが。
また、構成ファイル(カタログと呼ぶ)としてXMLの代わりに、直接Rubyの配列とハッシュを使って単純化している部分もある。
とはいえ、結構、クリアではなかろうか?
!実行すべきもの
例は、元ネタとほとんど同じにしてある。車の購入処理フローだ。
下のリストは処理と対応するクラスを示したもの。ただし、WikiNameになることを防ぐために先頭文字を小文字にしてある)
#顧客情報の取得(getCustomreInfo)
#試乗(testDriveVehicle)
#商談(negotiateSale)
#支払方法の決定(arrangeFinacing)
#商談成立(closeSale)
を順に実行する。
また、Chainsは終了時に、それまでに実行した処理を逆順に呼び出すことで、退出時処理を可能としている。ここでは、(逆順に呼ばれることを利用して)最初に正常処理時には何もしないかわりに退出時に例外が発生していたらその情報を出力する処理(exceptionHandler)
!!アプリケーション
sample.rb
#!/usr/bin/ruby -Ks
require 'chain'
class ExceptionHandler
def execute(c)
puts 'Filter.execute'
end
def post_execute(cx, e)
p e if !e.nil?
end
end
class GetCustomerInfo
def execute(c)
c.customer_name = 'Foo the Man'
puts 'GetCustomerInfo'
end
end
class TestDriveVehicle
def execute(c)
puts 'TestDriveVehicle'
end
end
class NegotiateSale
def execute(c)
puts 'NegotiateSale'
end
end
class CloseSale
def execute(c)
puts 'CloseSale'
end
end
class ArrangeFinancing
def execute(c)
raise RuntimeError, c.customer_name + ' is blacklisted'
end
end
class Context < Hash
def initialize()
@customer_name = nil
end
attr_accessor :customer_name
end
if $0 == __FILE__
c = Catalog.load_catalog('config.rb')
chain = c.get_chain("sell-vehicle")
chain.execute(Context.new)
end
各処理(コマンドと呼ぶ。というかコマンドパターンなのでそのまんまというか)が明示的にtrue(TrueClassのインスタンス)を返すか、例外を投げない限り、指定されたシーケンスに従ってChainsは呼び出しを行う(このシーケンスをチェインと呼ぶ)。
この呼び出しは、executeという名前のメソッドにコンテキストを与えることで行われる。コンテキストは文字通り実行時コンテキストを保持するオブジェクトである。例では、Contextとしてハッシュの派生クラスとして実装している。
""ちなみに、DIは、明示的なコンテキストをパラメータとして与える代わりに直接オブジェクトの属性を設定する方法という言い方もできるだろう。コンテキストを利用する場合の問題点は何が入っているかわからなくなること、属性を設定するDIと異なり、書き込みを許すために途中の変更や追加が見えにくくなること、それを防ぐためには明示的なメソッドが必要となり(例では、customer_nameというアクセサで示されている。ここでは単なるアクセサだが、まともなたとえばログ付きメソッドにすれば途中の変更や読み込みを監視することが可能となる)、結果的に頻繁な変更がコンテキスト自身に必要になることなどが挙げられる。(ここは書き方が甘いので追及は不可)
ここでは各クラスのexecuteの実装は基本的に自分のクラス名を出力するだけだ。
呼び出すべきチェインのすべてのコマンドが完了するか、または例外で中断された場合、Chainsは逆順に終了後処理を呼び出す。このメソッドはpost_executeという名称である。Rubyによる実装ではrespond_to?を利用して定義されていれば呼ぶということにしてある。引数はコンテキストと例外オブジェクト。正常に終了した場合、例外オブジェクトにはnilが設定されることになる。
実際のアプリケーションでは、例外時には部分トランザクション(既にコミット済み)の明示的なロールバック処理や、取消メッセージの送信の実行となるだろう。
ちなみに、exceptionHandler(WikiName防御だ。本当はEで始まる)のexecuteで出力されているFilterというのは、CommonsChainsで、post_executeメソッドを持つクラスが実装するインターフェイス名である。
!!構成ファイル
既に書いたように構成ファイル(カタログ)自身はRubyのスクリプトとして実装されている。
カタログの構成は、トップレベルのカタログ(カタログ名はシンボルnameで示す)の中に、文字列(チェイン名となる)をキーとした複数のチェインだ。また、各チェインは配列として実装され、コマンドを示すハッシュを要素とする。コマンドは実行すべきコマンドクラスのクラス名か、またはネストして呼び出されるカタログのチェイン名を保持する。
config.rb
catalog = {
:name => 'auto-sales',
'sell-vehicle' => [
{
:id => 'exception_handler',
:class => ExceptionHandler,
},
{
:id => 'get_customer_info',
:class => GetCustomerInfo,
},
{
:id => 'test_drive_vehicle',
:class => TestDriveVehicle,
},
{
:id => 'negotiate_sale',
:class => NegotiateSale,
},
{
:catalog => 'auto-sales',
:name => 'arrange-financing',
:optional => true,
},
{
:id => 'close_sale',
:class => CloseSale,
},
],
'arrange-financing' => [
{
:id => 'arrange_financing',
:class => ArrangeFinancing,
},
],
}
(解説が必要そうなら後で書くつもり)
この実装では、「支払方法の決定」は別チェインとして示される。
!!Chains実装
Chainsは2つのクラス(と1つの補助的なクラス)から構成されている。
:Catalog:カタログを示す。クラスメソッドとしてカタログの読み込み処理(とその保持)であるload_catalogと、指定されたカタログのインスタンスを返すget_catalogを持つ。インスタンスメソッドには、チェインのインスタンスを返すget_chainだけである。なお、ここではCatalogはあるカタログのインスタンスのファクトリを兼ねさせているためコンストラクタそのものはプライベートとして実装している。
:Chain:チェインを示すクラスである。ネストしたチェインの場合、例外の動作や退出呼び出しの動作が異なるため、それを示す属性を持つ。
:Command:チェインの内部クラスとして、コマンドのインスタンス化を担当する。
chain.rb
class Catalog
@@catalog = {}
def get_chain(name, nested = false)
chain = @catalog[name]
if chain.nil?
raise RuntimeError, name + ' not found'
end
Chain.new(chain, nested)
end
def self.load_catalog(catfile)
catalog = nil
File.open(catfile, 'r') do |f|
eval(f.read)
end
if catalog.nil? || catalog[:name].nil?
raise RuntimeError, catfile + ' is not a valid catalog'
else
@@catalog[catalog[:name]] = catalog
end
Catalog.new(catalog)
end
def self.get_catalog(name)
Catalog.new(@@catalog[name])
end
private
def self.new(c)
super(c)
end
def initialize(cat)
@catalog = cat
end
end
class Chain
class Command
def initialize(c)
@obj = c[:class].new
@id = c[:id]
end
def execute(ctx)
@obj.execute(ctx)
end
def post_execute(cx, e)
@obj.post_execute(cx, e) if @obj.respond_to?(:post_execute)
end
end
def initialize(cmd, nested)
@nested = nested
@commands = []
cmd.each do |c|
if Class === c[:class]
@commands << Command.new(c)
else
jump = nil
if !c[:catalog].nil?
cat = Catalog.get_catalog(c[:catalog])
if !cat.nil? && !c[:name].nil?
jump = cat.get_chain(c[:name], true)
end
end
if jump.nil?
if !c[:ooptional]
raise RuntimeError, c.inspect + ' is not valid chain'
end
else
@commands << jump
end
end
end
@bt = nil
end
def execute(cx)
e = nil
result = nil
@bt = []
@commands.each do |c|
@bt << c
begin
result = c.execute(cx)
break if result == true
rescue StandardError => e
raise e if @nested
break
end
end
post_execute(cx, e) unless @nested
result
end
def post_execute(cx, e)
@bt.reverse_each do |x|
begin
x.post_execute(cx, e) if x.respond_to?(:post_execute)
rescue
# ignore exception
end
end
end
end
!!実行例
もちろん、こうなる。各コマンド実装クラスの中身を替えたりして試してみよう。たとえば、trueを返すようにするとか。
C:\Home\arton\test\chain>ruby sample.rb
ruby sample.rb
Filter.execute
GetCustomerInfo
TestDriveVehicle
NegotiateSale
#<RuntimeError: Foo the Man is blacklisted>
!!ソースファイル
[[chain.zip|http://arton.no-ip.info/data/chain.zip]]
このzipの中身はちょっと古い。Command#executeとCommand#initializeが異なる(@btの初期化が1回だけのため、複数回のexecute呼び出しに対応できない)。
!感想
リワインド処理は(何を今更という気も自分に対してするのだが)目からウロコだった。