システム分析法のひとつに構造化分析があります。その中心となるのがデータフローダイアグラムです。おもにコンピュータシステムを構築する際のシステム分析に使われているようですが、ルールが簡潔なので広く色々な分野に応用できそうな気がします。このデータフローダイアグラムはオブジェクト指向プログラムと相性がよく、 Ruby が活用できるかも知れません。データフローダイアグラムはシステムをプロセスとプロセスをデータの流れで繋いだネットワークとして表現します。したがって、プロセスをオブジェクトに置き換え、オブジェクト間のメッセージによる連絡をプログラムで実現できれば、データフローダイアグラムをプログラムすることができます。
オブジェクトとオブジェクトをメッセージで連絡することができれば、事は簡単なのですが、Ruby ではオブジェクトへのメッセージはメソッド呼び出しの形で行われます。この場合メソッド呼び出しの後は処理が必ず元のオブジェクトに帰ってくるので、オブジェクト間の従属関係が発生し、独立したオブジェクト間のメッセージ通信という訳にはいきません。
これを解決するためには、ちょっとした工夫が必要です。その方法はオブジェクト間の通信をメッセージキューを利用することによって実現することです。つまり、各オブジェクトがメッセージを " object.method " の形式の文字列として発信し、メッセージキューにいれていったん処理を終了します。キューに入れられたメッセージはメインループで取り出され、eval( "object.method" )が実行されることによって目的のオブジェクトに渡されることになります。
それでは先ずメッセージキューを作ってみましょう。次のようなスクリプトを queue.rb というファイル名で作成します。
class Queue def initialize @queue = [] end def push( object ) @queue.push( object ) end def pop @queue.shift end end
これで準備 OK です。次のようなテストプログラムでキューがきちんと働くかどうか見てみましょう。テストプログラムのファイル名を q_test.rb とします。
require "queue.rb" q = Queue.new q.push( "a" ) q.push( "b" ) puts q.pop puts q.pop
実行すると次のようになります。うまく働くようです。
$ ruby q_test.rb a b
それでは、上で作成したメッセージキューを使ってオブジェクト間通信をさせてみましょう。次のような shopping.rb というプログラムを作ります。
require "queue.rb" $q = Queue.new class Kitchen def Kitchen.get_potato puts "in Kitchen: Get potato." $q.push( "Grocery.potato" ) end def Kitchen.got_potato puts "in Kitchen: Ok, I'll cook." end end class Grocery def Grocery.potato puts "in Grocery: Potato!" $q.push( "Kitchen.got_potato" ) end end message = "Kitchen.get_potato" while( message ) eval( message ) message = $q.pop end
このプログラムには Kitchen というオブジェクトと、Grocery というオブジェクトがあります。最初にユーザが Kitchen に get_potato というメッセージを送ります。するとそれを受けて Kitchen が Grocery に potato というメッセージを送ります。メッセージ potato を受け取った Grocery はそれに対して got_potato というメッセージをKitchen に送ります。got_potato を受け取った Kitchen は "OK, I'll cook" と表示してプログラムは終了します。
プログラムの説明は次のようになります。まず、キュークラスを定義した queue.rb を読み込み、キューオブジェクト $q を作ります。$ が先頭についているので $q はグローバル変数ですから、スクリプトのどのスコープからも参照することができます。
次に Kitchen と Grocery の2つのクラスを定義します。Kitchen のメソッドの定義のとき Kitchen.get_potato のように先頭にクラス名のレシーバがつけられていますが、このような形で定義されたメソッドはクラスメソッドです。クラスメソッドはオブジェクトに関係なく Classname.methodname というふうにクラスをレシーバにしてメソッドを呼び出すことができます。Classname.methodname は Classname::methodname というふうに書くこともできます。
Kitchen や Grocery の個々のメソッドは、簡単なコメントを表示した後、別のオブジェクトにメッセージを送っているだけです。メッセージの送り方は $q.push( "message" ) で文字列のメッセージを キュー $q に送ります。このプログラムで Kitchen クラスや、Grocery クラスのオブジェクトがないので変に思われた方もあるかも知れません。クラスメソッドを定義すると、レシーバにクラスを持って来ることができるので、クラスをまるでオブジェクトのように扱うことができます。
最後の部分がメインループです。最初に message に "Kitchen.get_potato" を代入してメインループに入ります。メインループでは eval( message )でメッセージを実行します。eval( message )はまたメッセージなので、これが nil になるまでループを回ることになります。
実行例は次のようになります。
$ ruby shopping.rb in Kitchen: Get potato. in Grocery: Potato! in Kitchen: Ok, I'll cook.
ここで使った方法ではメッセージを上位クラスに送ったり、エラーメッセージを発生させたりはできませんが、自由に抽象的なメッセージを送ることができます。もう気づかれた方もあると思いますが、上のように簡単なキューを使ったオブジェクト間の通信を定義すると、Ruby 自身がデータフローダイアグラムの記述言語に変身してしまいます。したがって、データフローダイアグラムをそのままプログラムできますから、ウォークスルーを自動的に行うことができます。階層化データフローダイアグラムの実現も実に簡単に行えます。
データフローダイアグラムについては、google 検索をしましたが、解説記事を見つけることができませんでした。Web ページのトム・デマルコに発案者の紹介と参考文献が出ています。
データフローダイアグラムは情報処理技術者試験にも出るくらい一般的なもののようです。門外漢なので知りませんでした。おそらく、データフローダイアグラム専用のソフトウェアーもあるのではないでしょうか。でも、Ruby でやればただですし、プロセスにロジックを組み込むことができます。
データフローダイアグラムはシステム分析をデータの流れに注目して行う方法です。データの流れを矢印で表します。また、あるデータが処理されて別のデータが発生する場合その場所を丸で表します。丸はデータが処理されるプロセスです。また丸で表すためバブルとも呼ばれます。データフローダイアグラムをプログラム化するためのデータフローダイアグラムは次のようになります。
データフローダイアグラムの原図の情報を、ダイアグラム分析プロセスに渡すと、分析済ダイアグラムがダイアグラムシミュレーションプロセスに渡されます。また、ダイアグラムシミュレーションプロセスには外部入力が入力され、シミュレーションが開始されます。その結果、ダイアグラムシミュレーションプロセスはシミュレーションの結果を出力します。
このダイアグラムをプログラム化したのが次のプログラムです。メッセージキューを定義した queue.rb と、バブル(プロセス)クラスを定義した bubble.rb と本体の dfd_sample.rb の3つのファイルからなっています。
queue.rb については既に述べていますので説明は省略します。
class Queue def initialize @queue = [] end def push( object ) @queue.push( object ) end def pop @queue.shift end end
bubble.rb はデータフローダイアグラムのバブル(プロセス)に共通な性質を定義したクラス Bubble を定義しています。Bubble にはバブルで行われる入出力に関するメソッドの定義はされていません。したがって、クラス Bubble から直接にバブルのオブジェクトを作ることはしません。まず、各バブルに対応するサブクラスを Bubble クラスから導出します。次にサブクラスでは各バブル固有の処理をおこなうメソッドを追加定義します。そうして、そのサブクラスから各バブルのオブジェクトを作成するようにします。つまりこのプログラムはクラスの継承の例題なのです。
Bubble クラスのインスタンス変数は、バブルのID @id、バブルの名称 @name、バブルへのデータの入力があったかどうかを保持するフラグのハッシュ @flag、フラグの作業用の変数 @o_flag からなっています。@flag ハッシュを用意する理由は、データ入力の AND 処理をするためです。上のダイアグラムの例でいうと、バブル「ダイアグラムのシミュレート」には「分析済ダイアグラム」と「外部入力」の2つのデータ入力があります。この2つが揃わなければ出力の「シミュレーション出力」を行うことができません。この AND 処理を行うための変数が @flag と @o_flag です。
show_info メソッドはバブルオブジェクトの ID と名称を表示します。
flag_clear メソッドの引数には複数の入力データの名称をとります。入力データの値毎に @flag[ keyname ] = false として、キーに該当する入力がまだ入力されていないことを示すようにします。メソッドの引数に *input のように * がつくと、メソッドに渡された複数のデータが配列の要素となった配列が input に代入されます。input.each の each はイテレータでブロックを引数に取ります。イテレータを使うとループ処理が非常に簡潔に表現できます。このプログラムでもイテレータを多用しています。
flag_set メソッドは引数にバブルへの入力の名称をとり、@flag[ input ] = true として、その名前の入力があったことを示します。
flag_set メソッドは flag_set メソッドの反対で、入力をキャンセルします。
flag_check メソッドは、@flag の全ての要素を検査してフラグの値が全て true の時だけ true を返します。つまり入力が全てあったかどうかを検査する AND 処理を行っています。このメソッドもイテレータ each_value を使うことによって簡潔に表現することができます。each_vlaue はハッシュの全ての要素の値を取り出して順に処理をする働きがあります。
class Bubble def initialize( id, name ) @id = id @name = name @flag = {} @o_flag = true end def show_info print "#{@id}) #{@name}\n" end def flag_clear( *inputs ) inputs.each {|input| @flag[ input ] = false} end def flag_set( input ) @flag[ input ] = true end def flag_unset( input ) @flag[ input ] = false end def flag_check @o_flag = true @flag.each_value {|t| @o_flag &&= t} @o_flag end end
dfd_sample.rb がメインプログラムです。requier "queue.rb" と$q = Queue.new でメッセージキュー $q を作成します。$ のついた変数 $q はグローバル変数でどのスコープからも参照することができますが、グローバル変数を多用すると、変更がやりづらい汚いプログラムになる場合があります。
require "bubble.rb"でバブルクラスを読み込みます。
class Dfd_analize < Bubble で Bubble クラスのサブクラス Dfd_analize クラスを作成します。このクラスは上のデータフローダイアグラムの「ダイアグラム分析」バブルのオブジェクトを作るためのクラスです。< Bubble が Dfd_analize クラスが Bubble クラスを継承していることを表しています。Ruby の表現は簡潔で、直観的です。
Dfd_analize のinitialize メソッドは id と name を引数に取りますが、それをそのまま super メソッドに引き渡しています。super メソッドはそれを使って記述しているメソッドと同名のスーパークラスのメソッド(この場合は Bubble クラスの initialize メソッド)を表します。したがって、super( id, name )によって、@id = id、@name = name が実行されることになります。
dfd_input メソッドは上の図の「データフローダイアグラムの原図」のデータが入力されたときの処理をします。その処理は "dfd_simulate.analized_dfd" メッセージをメッセージキューに入れ、"in Dfd_analize: sent analized_dfd to Dfd_simulate" を表示するだけです。上のデータフローダイアグラムで言うと「ダイアグラム分析」バブルに「データフローダイアグラムの原図」が入力されたとき「分析済ダイアグラム」が「ダイアグラムシミュレーション」バブルに送られることになります。
最後に dfd_analize = Dfd_analize.new( "b1", "Dfd_analize" ) で上図の「ダイアグラム分析」バブルに相当する dfd_analize オブジェクトを作成します。
次に上図の「ダイアグラムシミュレーション」バブルに対応する Dfd_simulate クラスを定義します。
external_input メソッドでは上図の「外部入力」が「ダイアグラムシミュレーション」バブルに入力されたときの処理を記述します。まず、flag_set( "external_input" ) で外部入力があったことを知らせるフラグを立てます。処理内容を表示した後 output メソッドを呼びます。output メソッドは次に説明しますが、「ダイアグラムシミュレーション」バブルへの入力が全て揃ったときに「シミュレーション出力」を出力するメソッドです。
analized_dfd メソッドは「ダイアグラム分析」バブルからの「分析済ダイアグラム」の入力が「ダイアグラムシミュレーション」バブルに入力されたときの処理です。これは external_input とほとんど同じ処理なので説明は省略します。
Dfd_simulate クラスの最後のメソッド output メソッドは「シミュレーション出力」を出力するための処理を行います。if( flag_check ) の flag_check メソッドはスーパークラス Bubble のメソッドです。Dfd_simulate クラスは Bubble クラスのサブクラスなので Bubble クラスのインスタンス・メソッドはレシーバ名なしで使うことができます。flag_chek は「ダイアグラムシミュレーション」バブルへの入力が全て揃ったときにのみ true になりますから、output メソッドはそのときだけ、外部へ出力をおこないます。このように各バブルに共通の性質はスーパークラスに定義しておき、サブクラスではそのクラスに特異的な処理のみ記述することができます。便利なものです。
最後に「ダイアグラムシミュレーション」を表すオブジェクト dfd_simulate を作成します。これで準備完了です。上の図で言うと「ダイアグラム分析」が完成し、「分析済ダイアグラム」が作成されたことになります。
さていよいよ実行です。まず、メッセージ "dfd_analize.dfd_input" と "dfd_simulate.external_input" をメッセージキューに積みます。これは、オブジェクト dfd_analize にメッセージ dfd_input を送り、オブジェクト dfd_simulate にメッセージ external_input を送ったのと同じことになります。つまり「ダイアグラム分析」バブルに「データフローダイアグラム原図」を入力し、「ダイアグラムシミュレート」バブルに「外部入力」を入力したことと同じです。
次のメインループでシミュレーション開始です。変数 message にメッセージキューからメッセージを取り出し、eval( message )で実行します。それをメッセージキューが空になるまで続けるのです。
このようにオブジェクト指向プログラムは現実の物をイメージしながらプログラムを作っていくことができます。また、Ruby では型のない変数や、イテレータなどのおかげで、イメージをそのままコードにしていくことができるのです。実際このプログラムを作成するときもほとんどマニュアルを見直す必要を感じませんでした。できあがったプログラムが実用的かどうかは別として、素人にとってプログラムを作る楽しさをほんとうに味合わせてくれるのが Ruby の良さだと思います。
require "queue.rb" $q = Queue.new require "bubble.rb" class Dfd_analize < Bubble def initialize( id, name ) super( id, name ) end def dfd_input $q.push( "dfd_simulate.analized_dfd" ) puts "in Dfd_analize: sent analized_dfd to Dfd_simulate" end end dfd_analize = Dfd_analize.new( "b1", "Dfd_analize" ) class Dfd_simulate < Bubble def initialize( id, name ) super( id, name ) flag_clear( "external_input", "analized_dfd" ) end def external_input flag_set( "external_input" ) puts "in Dfd_simulate: recieved external_input" output end def analized_dfd flag_set( "analized_dfd" ) puts "in Dfd_simulate: received analized_dfd" output end def output if (flag_check) print "Simulation: success\n" if flag_check flag_clear( "external_input", "analized_dfd" ) end end end dfd_simulate = Dfd_simulate.new( "b2", "Dfd_simulate" ) $q.push( "dfd_analize.dfd_input" ) $q.push( "dfd_simulate.external_input" ) message = $q.pop while( message ) eval( message ) message = $q.pop end
それでは実行してみましょう、本当にわくわくする一瞬です。
$ ruby dfd_sample.rb in Dfd_analize: sent analized_dfd to Dfd_simulate in Dfd_simulate: recieved external_input in Dfd_simulate: received analized_dfd Simulation: success
大成功!!