ruby ことはじめ - 第5回「'プログラム'クラスを設計&実装してみよう」
こんにちは。先日ジムのプールでオバちゃんにナンパされそうになったkyagiです。
今回は実際のお仕事で書いているコードから引用して話を進めます(例外処理がまだまだ未実装なのでそのへんのツッコミはご容赦ください(>_<))。現在kyagiはあるプロジェクトでプログラムを管理して実行するスクリプトをスクラッチから作成しています(LTPもautotestもテストプログラムの集合があり、それを一括して実行するスクリプトがありますね)。
設計にあたり、まず何がクラスの単位になるのかを考えてみました。これはもう「プログラム」しかないでしょう(あたりまえのように書いていますが実はこの単位を決めるのに結構悩みました (^_^;) 。
次に「プログラム」クラスのメンバとメソッドを考えてみました。このクラスのインスタンスはひとつひとつのプログラムです。プログラムがもつ属性と してexecve(2)を参考に「起動されたディレクトリ」「自分自身のファイル名(パス)」「引数」としてみました。そうすると「コマンドライン」も自動的に生成できちゃうのでこれも追加です。メソッドとしては「自分を実行す る」ことですね。ここではシンプルにKernel#systemを使うことにしました。「プログラムの出力はどうするんぢゃ」というそこの人、慌てないで ください。お楽しみはあ・と・で(はあと)。
class Program
attr_accessor:dir_root
attr_accessor:filename
attr_accessor:arguments
attr_accessor:commandline
def initialize(filename, *arguments)
@dir_root = Dir.pwd
@filename = filename
@arguments = arguments
@commandline = [filename, arguments].flatten.join(" ")
end
def execute
system(commandline)
end
end
さらにこのクラスを継承したサブクラスを作成し、executeメソッドをログ取得用にオーバーライドしてしてみます。標準出力、標準エラー両方のログを別々にとるには open3ライブラリを使うと非常に簡単です*1。他にも自分が取得したい情報をメンバとして追加しておきます。
require 'open3'
class CjkProgram < Program
#FIXME: TestProgram Class doesn't need kv_expected
#FIXME: CompareProgram Class doesn't need kv_now
attr_accessor:kv_now # kernel_version_now
attr_accessor:kv_expected # kernel_version_expected
attr_accessor:dir_result
attr_accessor:dir_testcases
attr_accessor:log_dir
attr_accessor:log_stdout
attr_accessor:log_stderr
attr_accessor:log_status
attr_accessor:syscallname
def initialize(filename, *arguments)
super
@kv_now = `uname -r`.chomp!
@kv_expected = nil
@dir_result = [@dir_root, "results"].join('/')
@dir_testcases = [@dir_root, "testcases"].join('/')
@log_dir = nil
@log_stdout = nil
@log_stderr = nil
@log_status = nil
@syscallname = File.dirname(@filename).sub(@dir_testcases, "")
end
#FIXME: Replace this code, because 'open3' library cannot get exit_status.
def execute
Open3.popen3(@commandline) do |stdin, stdout, stderr|
stdin.close
File.open(@log_stdout, "w") do |f|
f.puts stdout.read
end
File.open(@log_stderr, "w") do |f|
f.puts stderr.read
end
end
end
end
最後にこのサブクラスを継承したサブクラスを作り(実際に使用するのはこのクラスになります)、実行速度の測定やインスタンス数の記録もできるように拡張します。またこのクラスのみで使用するメソッドも追加し、外部 から呼び出す必要のないものはついでに private 属性にしておきます(ちょっとかっこいい)。ここまででお気付きのように上位のクラスで定義したメンバやメソッドはオーバーライドしない限り有効で super で呼び出せます。Program > CjkProgram > TestProgram と3世代にわけてクラスを設計したのは後の再利用と拡張性を考え、上位のクラスは汎用的に使えるようにしたかったからです。
require 'fileutils'
class TestProgram < CjkProgram
attr_accessor:time_start
attr_accessor:time_end
@@count_instance = 0
@@count_execute = 0
@@time_start_first = 0
def initialize(filename, *arguments)
super
@log_stdout = "tout.log"
@log_stderr = "terr.log"
@log_status = "tstatus"
@time_start = nil
@time_end = nil
@@count_instance += 1
end
def execute
set_log_dir()
make_result_dir()
@log_stdout = [@log_dir, @log_stdout].join('/')
@log_stderr = [@log_dir, @log_stderr].join('/')
@log_status = [@log_dir, @log_status].join('/')
@@count_execute += 1
print "Executing #{@@count_execute}/#{@@count_instance}\n"
print "TestProgram: #{@filename}\n"
@time_start = Time.now
if (@@count_execute == 1)
@@time_start_first = @time_start
end
super
@time_end = Time.now
print "ExecutionTime: ", @time_end - @time_start, "\n\n"
if (@@count_execute == @@count_instance)
print "Total ExecutionTime: ", @time_end - @@time_start_first, "\n\n"
end
end
(...snip...)
def get_time_str
Time.now.strftime("%Y%m%d%H%M%S")
end
private:get_time_str
(...snip...)
def set_log_dir
@log_dir = [@dir_result, @kv_now, get_time_str(), @syscallname].join("/")
end
private:set_log_dir
def make_result_dir
FileUtils.mkdir_p(@log_dir)
end
private :make_result_dir
end
ここで実際にクラスを使用している箇所をあげます。以下は TestProgramクラスのインスタンスを次々に作成してリストにした後で一括実行しています。オブジェクトが持つべき情報、やるべき仕事はクラスで定義してあるのでコード がとてもシンプルになりました。このへんオブジェクト指向の醍醐味を肌で感じている今日このごろです。:-)
Class Framework
(...snip...)
def self.create_tp_list(path)
tp_list = Array.new()
begin
f = open(path)
while line = f.gets
path = File.expand_path(line.chomp!)
if File.exists?(path) then
tp = TestProgram.new(path)
tp_list.push(tp)
end
end
rescue Errno::ENOENT => err
errmsg err
exit 1
ensure
f.close
end
tp_list
end # def
def self.exe_tp_list(tp_list)
tp_list.each do |tp|
tp.execute
end
end
(...snip...)
end
*1 でも open3 ライブラリでは終了ステータスが取れない仕様だったりするので現在他の方法を模索中です(T_T)




コメント