【ツール作成】最終構成
前置き
読者が誰もいないので、マイペースで書いてます。ついでに【その?】もなくしました。何番まで行くか分からないので。。。
ファイルバックアップツールですが、一応、ちゃんと動作するところまではできました。 ただ、プログラムの構成に自分で納得がいかず、修正を繰り返した末、なんとなくこれでいいかなーというところまで来たので、こちらでも色々アップしてみようと思います。
プログラムの構成
最初に「動くもの」としてできたものは、処理の流れにしたがってプログラミングして、一部を関数化してるだけじゃん、くらいのものでした。これだったら ruby 使った意味ないじゃん・・・ということで。
なので、クラス/オブジェクト化できるものはまとめ、外付けで使うモジュール(I18n, logger など)もこちらで使いやすいようにラッパーもどきなモジュールにまとめたりしました。
最終的な構成は、下記のような感じになりました。
実行ファイル(メイン)
実行開始するファイルで、処理の流れに沿ってメソッドを呼び出すのみ。コピー実行モジュール コピーを実際に実行する際の機能をまとめたモジュール。
Inifile モジュール
iniファイルから設定を読み込むためのモジュール。元々、inifile gem を使う予定だったのですが、「¥」記号の扱いがいまいちだったので、自作しました。Setting クラス iniファイルから読み込んだ設定は、Settingクラスのオブジェクトとして保持して、必要に応じて各メソッドに引き渡すようにしました。今回、iniファイルはシステム関連(system.ini)と、ディレクトリ指定用(setting.ini)に分けていて、それぞれオブジェクト化します。
Message モジュール 今回、I18n で文言をできるだけ集約化したい、メッセージの出力はコンソールのみ、logのみ、コンソールとlogの両方に出す、等々細かくしたい、などのことをしたかったので、I18n, logger の両方を取り込んで、Message モジュールで出力系を任せられるようにしました。(一部、結合度を上げすぎないために Messageモジュールを使わなかったところもあります。)
ProgressBarクラス バックアップの進捗表示を行うプログレスバーを表示するためのクラスです。バックアップ元ディレクトリ毎にオブジェクトを生成し、進捗管理と表示を行います。
続きはまた。
【Ruby】Ruby でファイルバックアップツールを作ってみる その3
処理の流れを書いてみる
そんなに大きなツールではないので、処理の流れを書いて整理してみます。
1 設定ファイルの読み込み # iniファイルから読み込み:コピー元ディレクトリ(複数)、コピー先ディレクトリなど # 設定が取れなかったらエラー 2 読み込んだ設定のチェック # コピー元ディレクトリが存在するか? → パスがないものは除外する # 有効なコピー元ディレクトリがない場合は、エラーで終了する。 # コピー先ディレクトリが存在するか? → コピー先が有効なパスではない場合、エラーで終了する。 3 確認のための情報を表示 4 実行するかどうか、確認( yes / no ) 5 コピー実行 (コピー元ディレクトリの数だけ、以下の処理を繰り返す) # コピー元ディレクトリの情報(総サイズ、ファイル数、ディレクトリ数など) # プログレスバーを生成 # ディレクトリ直下のファイル、ディレクトリを一覧取得 # ディレクトリの場合 # コピー先にディレクトリを作成し、そのディレクトリを対象にして再帰呼び出し # ファイルの場合 # コピー先ディレクトリにファイルをコピー。 # プログレスバーの更新 6 結果表示
必要な機能
処理の流れを踏まえて、必要な機能を考えてみます。
最後の「I18n」の機能は、本来は多言語化対応のためのものですが、勉強のために使ってみます。実装は少し面倒になりますが、固定文字列を一つのファイルにまとめることで、表記揺れや誤記のチェックが簡単になります。
【Ruby】Ruby でファイルバックアップツールを作ってみる その2
動作環境の要件
その1で、動作環境の要件について触れていなかったので、ここで書きます。
これくらいでしょうか。この要件は外さないようにします。
要件を分析する
その1で書き出した要件について、どうやって実現するかを分析してみます。
1.PC 内に入っているファイルのうち、指定したフォルダーの内容を、外付けHDDにコピーする。
2.バックアップ元はフォルダー単位で指定とし、複数指定できるようにしたい。
3. バックアップ元のフォルダーは、実行前に事前に設定できるようにし、フォルダーの追加、変更、削除も自由にできるようにしたい。
1~3 については、バックアップの情報を「事前に設定しておきたい」ということなので、どこかに設定を保管しておく必要があります。 設定の保管方法としては、いくつか考えられますが、
- コードの中に直書きする。
- テキストファイルに設定を書く
- データベースを作って、設定を保管する。
今回は、使用者=開発者なので、コードの中に全部直書きしてしまっても使うことはできますが、普通は使いにくいです。データベースを作って、、、でもできますが、今回のツールくらいの規模では、やり過ぎな感じです。
素直に、テキストの設定ファイルを作って、ツールで読み込ませる方法が良さそうです。
Windows で設定ファイル、というと、iniファイルが真っ先に思いつきます。Ruby でも、gem に inifile というパッケージがありますので、これが使えそうです。
4.バックアップ先は、以前に取得したバックアップが上書きされないように、新しいフォルダーを作成してほしい。また、フォルダー名に「年月日時分秒」を入れて、いつバックアップしたのか分かるようになっていると良い。
ここについては、バックアップ先のフォルダー名の加工が必要です。「年月日時分秒」だけのフォルダー名だと、何のフォルダーか分からなくなるので、固定の接頭辞(prefix)を決めておいて、そこに「年月日時分秒」をつけるのが良いでしょう。設定ファイルを作ることにしたので、prefix も設定できるようにしておくと便利、かもしれません。
5.実行開始直前に、処理対象のコピー元フォルダー、コピー先フォルダーが間違っていないか、目視で確認できるようにしたい。
設定ファイルを読み込んだ後、コピー実行前に情報表示をする、ということですが、設定ファイルに書き込んだパスが間違っていたり、重複したりしているかもしれません。ですので、フォルダーパスのエラーチェック、重複チェックを行い、エラーがあればここで確認できるとうれしいです。
6.バックアップ処理の進捗状況が分かるようにしたい。
ここは少し悩ましいです。
進捗状況が分かるようにするには、事前にバックアップするファイル全体の情報を調べる必要があります。また、進捗をリアルタイムに表示したいとすると、フォルダー丸ごとコピーではなく、中のファイルを一つずつコピーするようにしないと難しいかもしれません。ただ、それをすると、レスポンスは悪くなるのは間違いなさそう・・・
どっちにするかは、一つずつコピー、丸ごとコピーでレスポンスを試してみながら、決めていこうと思います。
7.バックアップ処理結果(ファイル数、フォルダー数、合計サイズなど?)を最後に表示したい。
結果表示は必要だと思うので。エラーが発生した時のエラー情報も出した方がいいのかも。
8.エラーが出たときのために、処理のログを出力したい。
9.コピー中にエラーが発生した場合は、そのファイル、フォルダーは飛ばして処理を続ける。ただし、エラーの数が 10件以上になった場合は、そこで処理を中止する。エラーの内容はログに出力する。
デバッグのためにも、実行結果の詳細をみるためにもログは出るようにしておきたいです。ログ出力用の機能を作ります。エラーの数のカウントも必要ですね。エラー10以上としていますが、設定ファイルでエラー上限数を決められると便利かもしれません。
今日のまとめ
細かいところはまだまだ詰める必要はありますが、今のところの方針は以下のような感じです。
- ini ファイルに設定をまとめる。設定内容は以下の通り。
- コピー実行前、実行後に情報確認用メッセージを表示する。
- コピー方法は、進捗状態を表示するために、「フォルダー、ファイルを一つずつ」コピーする方針で作ってみる。レスポンスに問題が出る場合は、フォルダーごとにコピーする方法に変更する可能性あり。
- エラーの発生回数により、処理を中断する機能をつける。
- ログ出力機能をつける。
【Ruby】Ruby でファイルバックアップツールを作ってみる その1
前置き
表題の通りです。 Ruby を勉強していますが、何か作ってみないとやっぱり感覚が分からないので、とりあえず作ってみることにしました。
何を作るか?
じゃあ、何を作る?ということですが、どうせ作るなら自分で使えるものを作りたい、ということで、今のマシンで便利にしたいことを考えました。ぱっと思いついたのが、外付けハードディスクへのファイルバックアップ。基本的に簡単にバックアップできるように、特定のフォルダーにできるだけ集中して入れているのですが、それでもメールデータだったり、マイドキュメント関連だったり、いくつかの場所に散らばっています。毎回、手動でコピーしていますが、フォルダーを指定しておいて、実行したら自動的にバックアップフォルダーを作ってバックアップを取ってくれると、便利なのかなー、と思ったので。
今の自分のメインマシンは Windows なので、それだったら batファイルで作ったり、Visual Studio系でWindowsアプリケーションにした方が早いのですが、せっかく Ruby を勉強しているので、Ruby で作ってみることにします。
ツールの要件
極端な話、copy コマンドを必要なだけ並べて終わり・・・という作り方もできるわけですが、それでは勉強にならないし、つまらないので、ちゃんと考えてみます。
大げさに言えば、、、要件定義、、、でしょうか。
今回は、使用者:自分、設計者:自分、開発者:自分、テスター:自分 ということになるので、まず、使用者の立場で、「こういうのが欲しい」というのを考えてみます。
【要件】
- PC 内に入っているファイルのうち、指定したフォルダーの内容を、外付けHDDにコピーする。
- バックアップ元はフォルダー単位で指定とし、複数指定できるようにしたい。
- バックアップ元のフォルダーは、実行前に事前に設定できるようにし、フォルダーの追加、変更、削除も自由にできるようにしたい。
- バックアップ先は、以前に取得したバックアップが上書きされないように、新しいフォルダーを作成してほしい。また、フォルダー名に「年月日時分秒」を入れて、いつバックアップしたのか分かるようになっていると良い。
- 実行開始直前に、処理対象のコピー元フォルダー、コピー先フォルダーが間違っていないか、目視で確認できるようにしたい。
- バックアップ処理の進捗状況が分かるようにしたい。
- バックアップ処理結果(ファイル数、フォルダー数、合計サイズなど?)を最後に表示したい。
- エラーが出たときのために、処理のログを出力したい。
- コピー中にエラーが発生した場合は、そのファイル、フォルダーは飛ばして処理を続ける。ただし、エラーの数が 10件以上になった場合は、そこで処理を中止する。エラーの内容はログに出力する。
んー、こんなところでしょうか。
今、ぱっと思いつきで出した要件なので、後になって追加や変更がたぶん出ます。実際のお客様からの要件ヒヤリングだと、色々な人の意見が出てくるので、まるっきり反対の意見が出たり、絶対無理な要件が出てくることは普通に良くあります。お客様の要望を分析し、開発期間やコストも含めて勘案、実際に開発する仕様の「落としどころ」を決めるのが、開発のキモの一つになります。
ここをしくじって、不幸になっていくプロジェクトは本当に多々多々ありました。。。
次回
次回は、出した要件に基づいて、どのような実現方法があるか、分析してみます。
【Ruby v2.6】クラス変数 その2(メソッドで生成した場合)
前置き
クラス変数についてまとめます。 その2は、クラス変数をメソッド内で生成した場合の挙動について。
クラス変数のメソッド内生成
その1でも書きましたが、クラス変数はメソッド外だけでなく、クラスメソッドやインスタンスメソッド内でも生成することができます。
一瞬、便利な感じがしますが、「継承」が絡んでくると、クラス変数がどのクラスで生成されるのか、とても分かり辛くなるので要注意です。
例
以下のようなクラスを作ってみます。
class A def self.class_method_a1 @@a1 = "created by class method a1 in A." end def self.class_method_a2 @@a2 = "created by class method a2 in A." end def self.class_method_a3 @@a3 = "created by class method a3 in A." end end class B < A def self.class_method_a2 @@a2 = "created by class method a2 in B." end end class C < B def self.class_method_a3 @@a3 = "created by class method a3 in C." end end
クラス A では、class_method_a1
、class_method_a2
、class_method_a3
の3つのクラスメソッドを定義し、それぞれでクラス変数を生成しています。
クラス B では、クラス A を継承し、class_method_a2
だけをオーバーライドして、オーバーライド前と同じクラス変数 @@a2
を生成しています。
クラス C では、クラス B を継承し、こちらは class_method_a3
をオーバーライドして、オーバーライド前と同じクラス変数 @@a3
を生成しています。
この状態で、クラス A のクラスメソッド3つを呼び出してみます。
A.class_method_a1 # => "created by class method a1 in A." A.class_method_a2 # => "created by class method a2 in A." A.class_method_a3 # => "created by class method a3 in A." A.class_variables # => [:@@a1, :@@a2, :@@a3] B.class_variables # => [:@@a2, :@@a1, :@@a3] C.class_variables # => [:@@a2, :@@a1, :@@a3]
クラス A で @@a1
、 @@a2
、 @@a3
が生成され、継承しているクラス B、クラス C でも認識されています。
・・・しかし・・・
一度リセットして、クラス A のクラスメソッドを呼び出さずに、クラス B、クラス C でオーバーライドしたクラスメソッドを呼び出してみます。
B.class_method_a2 # => "created by class method a2 in B." C.class_method_a3 # => "created by class method a3 in C." A.class_variables # => [] B.class_variables # => [:@@a2] C.class_variables # => [:@@a3, :@@a2]
今度は、クラス A では @@a2
、 @@a3
が認識されていません。クラス B でも、クラス C でオーバーライドしたメソッドで生成した @@a3
が認識されていません。
・・・さらに・・・
もう一度リセットして、クラス C でオーバーライドしていないクラスメソッドを呼び出してみます。
C.class_method_a1 # => "created by class method a1 in A." C.class_method_a2 # => "created by class method a2 in B." A.class_variables # => [:@@a1] B.class_variables # => [:@@a2, :@@a1] C.class_variables # => [:@@a2, :@@a1]
class_method_a1
は、クラス A からそのまま継承されているので、@@a1
はクラス A で生成されていますが、class_method_a2
の方は、クラス B でオーバーライドしているので、クラス B のクラスメソッドとして実行され、@@a2
はクラス B のクラス変数になってしまっています。
つまり、定義がどのクラスに書かれているか、は関係なく、クラス変数の生成(初期化)が実行されたクラスのクラス変数となる、ということになります。
さらにややこしいこととして、クラス C でクラス変数を生成後、クラス A で同じクラス変数を生成すると。
C.class_method_a3 # => "created by class method a3 in C." C.class_variable_get :@@a3 # => "created by class method a3 in C." A.class_variables # => [] C.class_variables # => [:@@a3] A.class_method_a3 # => "created by class method a3 in A." C.class_variable_get :@@a3 # => "created by class method a3 in A." A.class_variables # => [:@@a3] C.class_variables # => [:@@a3]
クラス C のクラス変数だった @@a3
が、クラス A のクラス変数に置き換わってしまいます。
このように、クラスの継承やメソッドのオーバーライドが絡んでくると、どこで生成されたクラス変数か分かり辛くなり、クラスメソッドの実行タイミングによっても、クラス変数の影響範囲が変わってしまう、ということになってしまいます。
どうしたらいいか?
ですので、その1でも書いたように、クラス変数の場合はクラス定義内の「メソッドの外」で必ず初期化(生成)を行うことを強く推奨します。これだけで、「継承」の問題は解決できます。
【Ruby v2.6】クラス変数 その1(基本的なこと)
前置き
クラス変数について、まとめてみます。個人的には、インスタンス変数、クラスインスタンス変数より注意が必要な感じがしています。
その1は、基本的なこと。
クラス変数
クラス変数は、そのクラス、およびそのクラスから継承されたクラスで共有される変数です。生成したクラス、そのクラスから継承されたクラスのクラスメソッド、インスタンスメソッドのどこからでも参照、値の変更ができます。
継承の階層が深くなったり、インスタンスが増えてきたりすると、どこで値が変更されたかが分かりづらくなるのが欠点です。
使い方
クラス定義の中で、変数名の前に@@
をつけて、値を代入したタイミングで生成されます。通常はメソッド定義の外で生成しますが、クラスメソッド内、インスタンスメソッド内で生成することもできます。
初期化せずに参照しようとすると、nil
を返すのではなく、NameError 例外を返します。
基本的なクラス変数
以下のようなクラス A 、クラス B で確認してみます。
class A @@clsval = "@@classval" def self.class_method_a p "#{@@clsval} : class A self.class_method_a " end def ins_method_a p "#{@@clsval} : class A ins_method_a " end end class B < A def self.class_method_b p "#{@@clsval} : class B self.class_method_b " end def self.change_clsval @@clsval = @@clsval + " changed!!! " end def ins_method_b p "#{@@clsval} : class B ins_method_b " end end
各クラスのクラス変数を確認してみます。
A.class_variables => [:@@clsval] B.class_variables => [:@@clsval]
クラス定義を読み込んだ直後は、クラス A で生成されているのは @@clsval
のみです。また、クラス A を継承しているクラス B でも@@clsval
が見えます。
※ class_variable
メソッドは、そのクラスで使用できるクラス変数が表示されてしまうので、要注意です。このサンプルだと、@@clsval
は、クラス A が大元のクラス変数だとすぐにわかりますが、コードが長く、複雑になってくると、クラス変数の大元がどのクラスなのか、分かり辛くなります。
続いて、各メソッドで、@@classval
を参照してみます。
A.class_method_a # => "@@classval : class A self.class_method_a " B.class_method_b # => "@@classval : class B self.class_method_b " obj_a = A.new obj_a.ins_method_a # => "@@classval : class A ins_method_a " obj_b = B.new obj_b.ins_method_b # => "@@classval : class B ins_method_b " obj_b.ins_method_a # => "@@classval : class A ins_method_a "
クラス A のクラスメソッド、インスタンスメソッド、クラス B のクラスメソッド、インスタンスメソッド、継承されたインスタンスメソッドの全てで、@@classval
にアクセスできています。
クラス B のメソッドで、@@clsval
の値を変更してみます。
B.change_clsval # => 値に" changed!!! "を追加 A.class_method_a # => "@@classval changed!!! : class A self.class_method_a " B.class_method_b # => "@@classval changed!!! : class B self.class_method_b " obj_a.ins_method_a # => "@@classval changed!!! : class A ins_method_a " obj_b.ins_method_b # => "@@classval changed!!! : class B ins_method_b "
値を変更すると、クラス A、クラス B のどこから参照しても、値が変わっています。
その1のまとめ
- クラス変数は、そのクラスと継承したクラスの全体で共有する変数。
- クラス定義内のメソッド外でも、クラスメソッド内でもインスタンスメソッド内でも、代入して初期化したタイミングで生成される。が、生成する場合は必ずメソッド外で初期化するようにすることを強く推奨する
- 代入(初期化)しないで参照しようとすると、NameError 例外が発生する。
- 影響範囲のクラスであれば、どのメソッドからも参照ができ、書き換えも可能。
どこからでも参照、書き換え可能と言われると、変数のスコープを意識しなくていいので、すごく使いやすそうな感じがしますが、どこからでも制限なく書き換えができてしまうので、コードが長く、複雑になってくると、どこからでも書き換えが可能なことがバグの原因となってしまい、デバッグも難しくなってしまうことがあります。基本的には、クラス変数は使わずに、クラスインスタンス変数などの他の手段で変数管理をする方が、予想外の事故を減らせるように思います。
また、初期化するのは必ずメソッド外で行いましょう。メソッドの中で初期化すると、特にクラス継承が絡んでくるととてもややこしい挙動をします。
その2に続きます。
【Ruby v2.6】protected とクラスメソッド、インスタンスメソッドの関係 ※2020.8.25 追記
前置き
C# や JAVA でのメソッドの protected
指定は、「そのクラスと継承したクラスからアクセス可能」という意味ですが、Ruby の場合はちょっと挙動が異なるみたいなので、まとめてみました。
protected
なクラスメソッド、protected
なインスタンスメソッドの関係
結論としては、以下の2点です。
protected
なクラスメソッドは、インスタンスメソッドからは、同じクラスからも継承関係にあるクラスからも呼び出せない。protected
なインスタンスメソッドは、クラスメソッドからは、同じクラスからも継承関係にあるクラスからも呼び出せない。
C# や JAVA の場合、クラスメソッド、インスタンスメソッドの括りなんか関係なく、同じクラス、継承関係にあるクラスのメソッドから呼び出せるのですが、Ruby では、インスタンスメソッド、クラスメソッドの間では protected
はアクセスできないのです。
例えば・・・
ぱっと思いつく例としては、
- クラス A のクラスメソッドから、各インスタンスのインスタンスメソッドにアクセスして、持っている値を収集したい。でも別の関係ないクラスから値を参照されても困るから、インスタンスメソッドを
protected
にしておこう。。。
とか、逆に・・・
- クラス A の各インスタンスから、処理した内容をクラス A の変数に集約するために、クラス A に情報集約用のクラスメソッドを作ろう。でも、クラス A のインスタンス以外からアクセスされたら困るから、クラスメソッドを
protected
にしておこう。。。
ということができないのです。この場合、public のままで妥協するか、別の手段を考える必要があります。
なんでこんな仕様なのか、自分なりに考えてみた。
最初は、すごく不思議な仕様な感じがしましたが、インスタンスメソッド、クラスメソッドの格納先を分けて考えてみると、少しだけ「なるほど」と思えるようになりました。
Ruby では、インスタンスメソッド、クラスメソッドの定義は同じ場所に書きますが、実体としては、インスタンスメソッドはクラスに、クラスメソッドは特異クラスに格納されます。ですので、あえて図でそのように分けて書いてみると、下記のようになります。
この図で、左が「クラス A」、右が「クラス A の特異クラス」です。「クラス A」で定義しているのがインスタンスメソッド、「クラス A の特異クラス」で定義しているのがクラスメソッドになります。
- クラス A のインスタンスメソッド
public_ins_method
から、protected なインスタンスメソッドはアクセス OK、protected なクラスメソッドはアクセス NG です。 - クラス A のクラスメソッド
public_class_method
から、protected なインスタンスメソッドはアクセス NG、protected なクラスメソッドはアクセス OK です。
図で見ると一目瞭然ですが、同じクラス内に格納されていればアクセスOK、そうでなければアクセス NG ということが分かります。
さらに、クラス A を継承したクラス B をつけてみます。
クラス A とクラス B 、クラス A の特異クラスとクラス B の特異クラス は、それぞれ継承関係にあります。 継承関係にあるクラス間であればアクセス OK ですが、やはりタスキがけのアクセスは NG となっています。
このことから。
protected
は、同じクラス、もしくは継承関係にあるクラスのメソッドからアクセスが可能、というのは正しい。- ただ、Ruby の場合は、インスタンスメソッドとクラスメソッドの格納先が異なっている。
- インスタンスメソッドとクラスメソッドの間では「実行しているクラスが異なり」、「クラスと特異クラスは継承関係にない」ので、
protected
ではアクセス NG となる。
ということなのかなー、と勝手に理解してみました。
※ あえてかぎかっこ「」で括った文言は、深くツッコみだすとはまりそうな気がするので、これ以上はツッコまないことにします。
追記(2020.8.25)
「継承関係にないとアクセスできない」というところで、ふと、思い付きがありました。
以前の記事で書いたのですが、特異クラスの親を追っていくと、BasicObjectの特異クラスから Classクラスに回って、最終的にBasicObject にたどりきます。 図で書いたのを再掲しますと。
クラス A を普通に生成した場合、Objectクラスからの派生になるので、クラス A の特異クラスの継承ルートにクラス A は入ってきません。
じゃあ、Objectクラスだったら?Object クラスの特異クラスの場合、継承ルートにObject クラスが入ってきます。
ということは、Objectクラスのクラスメソッドだったら、Objectクラスの protected インスタンスメソッドにアクセスできる?
というわけで、試してみました。 比較のために、Object クラス と クラス A に、インスタンス生成のところだけを変えた同じメソッドを追加してみました。
class Object def self.class_method_test obj = Object.new # <= 違いはここだけ obj.protected_method_test end protected def protected_method_test "called protected_method_test" end end class A def self.class_method_test obj = A.new # <= 違いはここだけ obj.protected_method_test end protected def protected_method_test "called protected_method_test" end end
結果は、こうなりました。
A.class_method_test # => NoMethodError (protected method `protected_method_test' called for #<A:0x0000000005812310>) Object.class_method_test # => "called protected_method_test"
Object クラスの方は、protected なインスタンスメソッドにアクセスできました。やっぱり、継承関係か否か、で見てるっぽいですね。なので、Object クラスやClassクラス、Moduleクラスの Protected インスタンスメソッドなら、同じクラスのクラスメソッドからアクセス可能です。
・ ・ ・
でもこんなことわかっても、役に立ちそうなシーンが思い浮かばないですね・・・ そこまでして protected を使う意味がなさそうです。