ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【Ruby】Ruby でファイルバックアップツールを作ってみる その2

動作環境の要件

その1で、動作環境の要件について触れていなかったので、ここで書きます。

  • 自分のPCで動作すること(Windows 10 環境)
  • Ruby で作ること
  • gem 等、必要があればインストールするが、あまり多くならないようにする。

これくらいでしょうか。この要件は外さないようにします。

要件を分析する

その1で書き出した要件について、どうやって実現するかを分析してみます。

1.PC 内に入っているファイルのうち、指定したフォルダーの内容を、外付けHDDにコピーする。
2.バックアップ元はフォルダー単位で指定とし、複数指定できるようにしたい。
3. バックアップ元のフォルダーは、実行前に事前に設定できるようにし、フォルダーの追加、変更、削除も自由にできるようにしたい。

1~3 については、バックアップの情報を「事前に設定しておきたい」ということなので、どこかに設定を保管しておく必要があります。 設定の保管方法としては、いくつか考えられますが、

  • コードの中に直書きする。
  • テキストファイルに設定を書く
  • データベースを作って、設定を保管する。

今回は、使用者=開発者なので、コードの中に全部直書きしてしまっても使うことはできますが、普通は使いにくいです。データベースを作って、、、でもできますが、今回のツールくらいの規模では、やり過ぎな感じです。

素直に、テキストの設定ファイルを作って、ツールで読み込ませる方法が良さそうです。

Windows で設定ファイル、というと、iniファイルが真っ先に思いつきます。Ruby でも、gem に inifile というパッケージがありますので、これが使えそうです。

4.バックアップ先は、以前に取得したバックアップが上書きされないように、新しいフォルダーを作成してほしい。また、フォルダー名に「年月日時分秒」を入れて、いつバックアップしたのか分かるようになっていると良い。

ここについては、バックアップ先のフォルダー名の加工が必要です。「年月日時分秒」だけのフォルダー名だと、何のフォルダーか分からなくなるので、固定の接頭辞(prefix)を決めておいて、そこに「年月日時分秒」をつけるのが良いでしょう。設定ファイルを作ることにしたので、prefix も設定できるようにしておくと便利、かもしれません。

5.実行開始直前に、処理対象のコピー元フォルダー、コピー先フォルダーが間違っていないか、目視で確認できるようにしたい。

設定ファイルを読み込んだ後、コピー実行前に情報表示をする、ということですが、設定ファイルに書き込んだパスが間違っていたり、重複したりしているかもしれません。ですので、フォルダーパスのエラーチェック、重複チェックを行い、エラーがあればここで確認できるとうれしいです。

6.バックアップ処理の進捗状況が分かるようにしたい。

ここは少し悩ましいです。

進捗状況が分かるようにするには、事前にバックアップするファイル全体の情報を調べる必要があります。また、進捗をリアルタイムに表示したいとすると、フォルダー丸ごとコピーではなく、中のファイルを一つずつコピーするようにしないと難しいかもしれません。ただ、それをすると、レスポンスは悪くなるのは間違いなさそう・・・

どっちにするかは、一つずつコピー、丸ごとコピーでレスポンスを試してみながら、決めていこうと思います。

7.バックアップ処理結果(ファイル数、フォルダー数、合計サイズなど?)を最後に表示したい。

結果表示は必要だと思うので。エラーが発生した時のエラー情報も出した方がいいのかも。

8.エラーが出たときのために、処理のログを出力したい。
9.コピー中にエラーが発生した場合は、そのファイル、フォルダーは飛ばして処理を続ける。ただし、エラーの数が 10件以上になった場合は、そこで処理を中止する。エラーの内容はログに出力する。

デバッグのためにも、実行結果の詳細をみるためにもログは出るようにしておきたいです。ログ出力用の機能を作ります。エラーの数のカウントも必要ですね。エラー10以上としていますが、設定ファイルでエラー上限数を決められると便利かもしれません。

今日のまとめ

細かいところはまだまだ詰める必要はありますが、今のところの方針は以下のような感じです。

  • ini ファイルに設定をまとめる。設定内容は以下の通り。
    • バックアップ元:絶対パスで指定。10個くらい指定できるようにする。
    • バックアップ先:絶対パスで指定。1個だけ。
    • バックアップフォルダーにつける prefix名
    • ログ出力先:相対パスで指定。デフォルトは実行ファイルと同じ場所で。
    • ログファイルにつける prefix名:バックアップフォルダーと同じように「年月日時分秒」を付けることにします。
    • エラー上限数
  • コピー実行前、実行後に情報確認用メッセージを表示する。
  • コピー方法は、進捗状態を表示するために、「フォルダー、ファイルを一つずつ」コピーする方針で作ってみる。レスポンスに問題が出る場合は、フォルダーごとにコピーする方法に変更する可能性あり。
  • エラーの発生回数により、処理を中断する機能をつける。
  • ログ出力機能をつける。

【Ruby】Ruby でファイルバックアップツールを作ってみる その1

前置き

表題の通りです。 Ruby を勉強していますが、何か作ってみないとやっぱり感覚が分からないので、とりあえず作ってみることにしました。

何を作るか?

じゃあ、何を作る?ということですが、どうせ作るなら自分で使えるものを作りたい、ということで、今のマシンで便利にしたいことを考えました。ぱっと思いついたのが、外付けハードディスクへのファイルバックアップ。基本的に簡単にバックアップできるように、特定のフォルダーにできるだけ集中して入れているのですが、それでもメールデータだったり、マイドキュメント関連だったり、いくつかの場所に散らばっています。毎回、手動でコピーしていますが、フォルダーを指定しておいて、実行したら自動的にバックアップフォルダーを作ってバックアップを取ってくれると、便利なのかなー、と思ったので。

今の自分のメインマシンは Windows なので、それだったら batファイルで作ったり、Visual Studio系でWindowsアプリケーションにした方が早いのですが、せっかく Ruby を勉強しているので、Ruby で作ってみることにします。

ツールの要件

極端な話、copy コマンドを必要なだけ並べて終わり・・・という作り方もできるわけですが、それでは勉強にならないし、つまらないので、ちゃんと考えてみます。

大げさに言えば、、、要件定義、、、でしょうか。

今回は、使用者:自分、設計者:自分、開発者:自分、テスター:自分 ということになるので、まず、使用者の立場で、「こういうのが欲しい」というのを考えてみます。

【要件】

  1. PC 内に入っているファイルのうち、指定したフォルダーの内容を、外付けHDDにコピーする。
  2. バックアップ元はフォルダー単位で指定とし、複数指定できるようにしたい。
  3. バックアップ元のフォルダーは、実行前に事前に設定できるようにし、フォルダーの追加、変更、削除も自由にできるようにしたい。
  4. バックアップ先は、以前に取得したバックアップが上書きされないように、新しいフォルダーを作成してほしい。また、フォルダー名に「年月日時分秒」を入れて、いつバックアップしたのか分かるようになっていると良い。
  5. 実行開始直前に、処理対象のコピー元フォルダー、コピー先フォルダーが間違っていないか、目視で確認できるようにしたい。
  6. バックアップ処理の進捗状況が分かるようにしたい。
  7. バックアップ処理結果(ファイル数、フォルダー数、合計サイズなど?)を最後に表示したい。
  8. エラーが出たときのために、処理のログを出力したい。
  9. コピー中にエラーが発生した場合は、そのファイル、フォルダーは飛ばして処理を続ける。ただし、エラーの数が 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_a1class_method_a2class_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 では、インスタンスメソッド、クラスメソッドの定義は同じ場所に書きますが、実体としては、インスタンスメソッドはクラスに、クラスメソッドは特異クラスに格納されます。ですので、あえて図でそのように分けて書いてみると、下記のようになります。

f:id:gyobo:20200824215801p:plain
protected メソッドの呼び出し可否

この図で、左が「クラス A」、右が「クラス A の特異クラス」です。「クラス A」で定義しているのがインスタンスメソッド、「クラス A の特異クラス」で定義しているのがクラスメソッドになります。

  • クラス A のインスタンスメソッドpublic_ins_method から、protected なインスタンスメソッドはアクセス OK、protected なクラスメソッドはアクセス NG です。
  • クラス A のクラスメソッドpublic_class_method から、protected なインスタンスメソッドはアクセス NG、protected なクラスメソッドはアクセス OK です。

図で見ると一目瞭然ですが、同じクラス内に格納されていればアクセスOK、そうでなければアクセス NG ということが分かります。

さらに、クラス A を継承したクラス B をつけてみます。

f:id:gyobo:20200824221118p:plain
継承したクラスからのアクセス可否

クラス A とクラス B 、クラス A の特異クラスとクラス B の特異クラス は、それぞれ継承関係にあります。 継承関係にあるクラス間であればアクセス OK ですが、やはりタスキがけのアクセスは NG となっています。

このことから。

  • protected は、同じクラス、もしくは継承関係にあるクラスのメソッドからアクセスが可能、というのは正しい。
  • ただ、Ruby の場合は、インスタンスメソッドとクラスメソッドの格納先が異なっている。
  • インスタンスメソッドとクラスメソッドの間では「実行しているクラスが異なり」、「クラスと特異クラスは継承関係にない」ので、protected ではアクセス NG となる。

ということなのかなー、と勝手に理解してみました。

※ あえてかぎかっこ「」で括った文言は、深くツッコみだすとはまりそうな気がするので、これ以上はツッコまないことにします。

追記(2020.8.25)

「継承関係にないとアクセスできない」というところで、ふと、思い付きがありました。

以前の記事で書いたのですが、特異クラスの親を追っていくと、BasicObjectの特異クラスから Classクラスに回って、最終的にBasicObject にたどりきます。 図で書いたのを再掲しますと。

f:id:gyobo:20200802223927p:plain
クラス階層

クラス 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 を使う意味がなさそうです。

【Ruby v2.6】クラスメソッド その3(public, protected, private)

前置き

Ruby のクラスメソッドについてまとめます。 その3は、クラスメソッドの public, protected, private について

書き方

クラス定義内で、デフォルトでクラスメソッドを定義すると、public のクラスメソッドとなります。

ただ、インスタンスメソッドと同じように、クラス定義内に privateprotected キーワードの後にクラスメソッドの定義を記述しても、プライベート、パブリックなクラスメソッドにはなりません。

書き方は、2通りあります。

  1. public_class_methodprivate_class_method を使って設定する。(但し、protected_class_method がないので、protected なクラスメソッドはこの方法では作れません。。。)

  2. クラスメソッドは、特異クラス内でのインスタンスメソッドなので、特異クラスをオープンし、そこでprivateprotected キーワードで設定する。

private_class_method を使う場合

private_class_method を使う場合は、先にクラスメソッドを定義した後で、private_class_method :[メソッド名] の形式で記述します。

class A
    # self をつけて定義すると、全て public なクラスメソッドになる。
    def self.public_method
        p "called self.public_method."
    end

    def self.private_method
        p "called self.private_method."
    end

    # private_class_method を使って、プライベート化する。
    private_class_method :private_method

    # 各メソッドを呼び出すためのクラスメソッド
    def self.call_public_method
        A.public_method
    end

    def self.call_private_method
        private_method     # レシーバーはつけてはいけない
    end
end

# クラス A を継承するクラス B
class B < A
    # 継承されたクラス B から、クラス A のメソッドを呼び出してみる。
    def self.call_public_method
        A.public_method
    end

    def self.call_private_method
        A.private_method
    end

    # クラス B のインスタンスから呼び出せるか?
    def call_public_method
        A.public_method
    end

    def call_private_method
        A.private_method
    end
end

動作を確認してみます。

A.methods(false)           # => [:public_method, :call_public_method, :call_private_method]
A.private_methods(false)   # => [:private_method, :inherited, :initialize]

確かに、クラスメソッド private_method がプライベートと認識されています。 続いて、クラスA, クラスB のクラスメソッドから呼び出してみます。

A.call_public_method       # => "cataed self.public_method."
A.call_private_method      # => "called self.private_method."
B.call_public_method       # => "called self.public_method."
B.call_private_method      # => NoMethodError (private method `private_method' called for A:Class)

クラス A からはプライベートメソッドが呼び出せて、クラス B からはクラス A のプライベートメソッドは呼び出せていません。さらに、B のインスタンスから呼び出せるか、を確認します。

obj = B.new
obj.call_public_method     # => "called self.public_method."
obj.call_private_method    # => NoMethodError (private method `private_method' called for A:Class)

クラス B のインスタンスからも、クラス A のプライベートメソッドの呼び出しができなくなっています。

特異クラスを開いて定義する場合

特異クラスをオープンし、特異クラスのインスタンスメソッドとしてクラスメソッドを定義する方法です。この場合は、インスタンスメソッドの定義方法と同じように、 protectedprivate の後にメソッド定義をすることで、protected, private の定義ができます。

class A
    # クラス A の特異クラスをオープンする。
    class << self
        def public_method
            p "called public_method."
        end

        protected       # ここ以降は protected なメソッド

        def protected_method
            p "called protected_method."
        end

        private         # ここ以降は private なメソッド

        def private_method
            p "called private_method"
        end
    end     # 特異クラスはここで終わり

    # 各メソッドを呼び出すためのクラスメソッド
    def self.call_public_method
        A.public_method
    end

    def self.call_protected_method
        A.protected_method
    end

    def self.call_private_method
        private_method     # レシーバーはつけてはいけない
    end

    # クラス A のインスタンスから呼び出せるか?
    def call_public_method
        A.public_method
    end

    def call_protected_method
        A.protected_method
    end
    
    def call_private_method
        A.private_method
    end
end

# クラス A を継承するクラス B
class B < A
    # 継承されたクラス B から、クラス A のメソッドを呼び出してみる。
    def self.call_public_method
        A.public_method
    end

    def self.call_protected_method
        A.protected_method
    end

    def self.call_private_method
        A.private_method
    end

    # クラス B のインスタンスから呼び出せるか?
    def call_public_method
        A.public_method
    end

    def call_protected_method
        A.protected_method
    end
    
    def call_private_method
        A.private_method
    end
end

同じように確認してみます。まず、メソッド一覧を確認します。

A.methods(false)            # => [:call_private_method, :public_method, :protected_method, :call_public_method, :call_protected_method]
A.private_methods(false)    # => [:private_method, :inherited, :initialize]
A.protected_methods(false)  # => [:protected_method]

private_methodprotected_method が、ちゃんと認識されています。 続けて、クラスメソッドからの呼び出しです。

A.call_public_method        # => "called public_method."
A.call_protected_method     # => "called protected_method."
A.call_private_method       # => "called private_method"

B.call_public_method        # => "called public_method."
B.call_protected_method     # => "called protected_method."
B.call_private_method       # => NoMethodError (private method `private_method' called for A:Class)

クラス A を継承しているクラス B からは、protected は呼び出せていますが、private は呼び出せません。 さらに、クラス A と無関係なクラス C から、クラス A のprotected_method が呼び出せるか?

class C
    def self.call_protected_method                                              
        A.protected_method
    end
end

C.call_protected_method      # => NoMethodError (protected method `protected_method' called for A:Class)

たしかに、継承関係にないクラス C からは、protected_method は呼び出せていません。

また、インスタンスメソッドからの呼び出しですが、同じクラスのインスタンスでも、継承されたクラスのインスタンスでも、protected なクラスメソッドは呼び出せません。

obj_A = A.new
obj_A.call_public_method        # => "called public_method."
obj_A.call_protected_method     # => NoMethodError (protected method `protected_method' called for A:Class)
obj_A.call_private_method       # => NoMethodError (private method `private_method' called for A:Class)

obj_B = B.new
obj_B.call_public_method        # => "called public_method."
obj_B.call_protected_method     # => NoMethodError (protected method `protected_method' called for A:Class)
obj_B.call_private_method       # => NoMethodError (private method `private_method' called for A:Class)                                        

ちょっと意外です。直観的には呼び出せそうな感じがしてたんですが。。

どうやって使い分けるか?

外から呼び出す必要のあるものは、デフォルトの public で定義し、クラス内の内部処理でしか使えない、外から呼び出されるとかえって問題になるような場合は private にする、という使い分けだと思います。

protected に関しては、特異クラスで定義することはできますが、使わない方法を考える方が良いような感じです。( protected_class_method で定義できないので、使う想定をあまりされてないんじゃないかなーと。)

その3のまとめ

  • クラスメソッドもインスタンスメソッドと同様に、public, protected, private の3種類の定義ができる。
  • 通常の定義では public となる。
  • private にするには、private_class_method :[クラスメソッド名] とするか、特異メソッド内で定義する。
  • protectedにするには、特異メソッド内で定義する方法しかない。
  • クラスメソッドの protected は、そのクラスか、継承されたクラスの「クラスメソッド」からしか呼び出せない。同じクラスでも、インスタンスメソッドからは呼び出せない。

「JavaScript Primer 迷わないための入門書」

Ruby 関連を調べていて知った、伊藤淳一さん(Ruby のチェリー本の著者の方です)のはてなブログを読んでいたら、JavaScript の本についての紹介記事がありました。

blog.jnito.com

その中で、「JavaScript Primer 迷わないための入門書」という本が、Web上で無料公開されていることを知り、今読んでいます。サイトはこちら。

jsprimer.net

今、ちょうど時間が少しあって、色々と勉強しようとしているのですが、JavaScript については、どの本が良いのかいまいちよく分からなかったんですよね。JavaScript を使って本格的にコーディングしていたのは 10年以上前で、最近どう変わってるのか全然しらないものの、じゃあ初心者向けの本買ったら満足できるのか?でも上級者向けの本を買っても理解できるのか、というのが心配で、躊躇していました。

「はじめに」の読者対象として、「この書籍は、プログラミング経験のある人がJavaScriptという言語を新たに学ぶことを念頭に書かれています。 ・・・・・・JavaScriptを書いたことはあるが最近のJavaScriptがよくわからないという人も、この書籍の読者対象です。 ・・・・・」と書かれていて、実際に中身を読んでみると、ドンピシャでした。自分からすると、とても読みやすくて分かりやすい。この仕様は「ECMAScript 2015」から、とか、旧仕様との整合性とかしっかり書かれているので、とてもちょうどいいレベルで、勉強になります。Ruby の勉強もしつつ、こっちも読み進めてみようと思います。

でも、せっかく出版して販売してるのに、Web に全内容を公開しちゃってたら儲からないんじゃない?と心配したりしています。あんまり儲けるとか考えてないんだと思いますが。

あと、勝手にリンク付けちゃったけど、いいのかな・・・ダメなら消します。