Create  Edit  Diff  FrontPage  Index  Search  Changes  Login

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呼び出しに対応できない)。

!感想

リワインド処理は(何を今更という気も自分に対してするのだが)目からウロコだった。