Rubyの組み込みライブラリには、「Comparable」モジュールがあります。Comparable、つまり「比較できるもの」を表すモジュールです。
Comparableモジュールは、ComparableモジュールをMixinするクラスに「<=>」メソッドが定義されていることを期待しています。クラスに「<=>」メソッドが定義されていると、Comparableモジュールはクラス側の「<=>」メソッドの結果を使って、「<」「>」「<=」「>=」「==」といったメソッドを自動的にクラスに追加してくれます。つまり、ComparableモジュールをMixinして1個のメソッドを定義するだけで、クラスの機能が大幅に強化されるのです!
では、rabbit.rbに戻ってComparableモジュールをMixinしてみましょう。
class Rabbit include Comparable attr_accessor :name attr_reader :color, :length_of_ears @@count = 0 DEFAULT_NAME = "usachan" DEFAULT_COLOR = :white DEFAULT_LENGTH_OF_EARS = 10 DESCRIPTION = "ウサギは特徴的な耳を持つ可愛い動物です。" def initialize(name: DEFAULT_NAME, color: DEFAULT_COLOR, length_of_ears: DEFAULT_LENGTH_OF_EARS) @name = name @color = color @length_of_ears = length_of_ears @@count += 1 end def jump puts "pyon! pyon!" end def pound_steamed_rice_into_rice_cake puts "pettan! pettan!" end def say_name puts "Hello, I'm #{@name}!" end def print_ears puts "∩_∩" end def print_description puts DESCRIPTION end def to_s "名前: #{@name}, 毛の色: #{@color}, 耳の長さ: #{@length_of_ears}, 全体でのウサギの数: #{@@count}" end def <=>(other) @length_of_ears <=> other.length_of_ears end end
2行目でComparableモジュールをMixinしており、46行目から48行目の間に「<=>」メソッドの定義を追加しました。耳の長さで比較することとして、「<=>」メソッドの中で、インスタンス変数「@length_of_ears」と比較対象のオブジェクト「other」の「length_of_ears」を「<=>」メソッドで比較しています。
require_relative "rabbit" rabbits = Array.new.tap do |array| [7.0, 1.0, 5.0, 1.0].each do |length| array << Rabbit.new(length_of_ears: length) end end rabbits.each_with_index do |rabbit, i| puts "rabbit#{i}: #{rabbit.length_of_ears}" end puts "rabbits[0] < rabbits[1]: #{rabbits[0] < rabbits[1]}" puts "rabbits[0] > rabbits[1]: #{rabbits[0] > rabbits[1]}" puts "rabbits[1] == rabbits[3]: #{rabbits[1] == rabbits[3]}" puts "rabbits[1] <= rabbits[3]: #{rabbits[1] <= rabbits[3]}" puts "rabbits[0] <= rabbits[1]: #{rabbits[0] <= rabbits[1]}" puts rabbits.sort
main06.rbでは、まず3行目から7行目の間で、変数「rabbits」に「7.0」「1.0」「5.0」「1.0」の耳の長さを持つRabbitクラスのオブジェクトの配列を代入しています。
ここでさりげなく「tap」メソッドを使っています。「tap」メソッドはObjectクラスに定義されているメソッドで、「tap」メソッドに渡すブロック変数に、そのオブジェクトそのものが入ります。
また、「tap」メソッドの返り値はそのオブジェクト自身です。ここでは、「Array.new」で生成した空の配列に対して「tap」メソッドを呼び出し、その配列に対してRabbitクラスのオブジェクトを追加しています。
9行目から11行目では、「rabbits」に格納された配列の内容を確認しています。
13〜17行目は、ComparableモジュールをMixinしたことで、きちんと各種比較演算子が定義されているかをチェックしている部分です。
また、比較メソッドが定義されたことによって、Rabbitクラスのオブジェクトを含む配列に対してsortメソッドを使えるようになりました。19行目は実際に耳の長さで配列をソートしています。
では、main06.rbの実行結果を見てみましょう。
$ ruby main06.rb rabbit0: 7.0 rabbit1: 1.0 rabbit2: 5.0 rabbit3: 1.0 rabbits[0] < rabbits[1]: false rabbits[0] > rabbits[1]: true rabbits[1] == rabbits[3]: true rabbits[1] <= rabbits[3]: true rabbits[0] <= rabbits[1]: false 名前: usachan, 毛の色: white, 耳の長さ: 1.0, 全体でのウサギの数: 4 名前: usachan, 毛の色: white, 耳の長さ: 1.0, 全体でのウサギの数: 4 名前: usachan, 毛の色: white, 耳の長さ: 5.0, 全体でのウサギの数: 4 名前: usachan, 毛の色: white, 耳の長さ: 7.0, 全体でのウサギの数: 4
実行結果の6〜10行目で、各種比較演算子が耳の長さを用いて結果を返していることを確認してください。また、11〜14行目では配列rabbitsをソートし、ソートした順にオブジェクトのto_sメソッドが呼ばれています。ここで耳の長さが昇順になっていることから、きちんとソートができていることが分かります。
ここまで、クラスの定義から継承、モジュール定義からMixinまで説明してきましたが、結局継承とMixinは、どのように使い分ければいいのでしょうか。
継承は「is-a関係」を表す場合に使われます。つまり、「ウサギやアヒルは動物である、ニンジンは野菜である」といった具合です。
ウサギやアヒルの場合、Animalクラスを継承する形でRabbitクラスやDuckクラスを構成するのが自然でしょう。つまり、「ウサギやアヒルは、息をして歩くなど、動物が持つ特徴を全て持っている」という意味です。息をする、歩くなどの共通の振る舞いをAnimalクラスにまとめ、その派生クラスでウサギ独自の振る舞いやアヒル独自の振る舞いを記述するわけです。
Mixinは、「has-a関係」を表す場合に使うとよいでしょう。というのも、物事は「is-a関係」だけで記述できるものではないからです。
例えば、ウサギやアヒルは動物であり、何かを食べることで生きています。一方で食虫植物も虫を食べて栄養の一部としていますが、食虫植物は植物であり、動物というには無理があるでしょう。つまり、動物と植物という違いがあれど、「食べる」という共通の性質を持っている(has)わけです。なので、この食べるという振る舞いをモジュールとして共通化し、AnimalクラスやPlantクラスでMixinすればキレイに部品化できます。
Rubyによる大規模なアプリケーション開発では、クラス設計の問題がいつでも付きまといます。継承とMixinの違いをしっかりと理解して、適切に使い分けることはRubyプログラマーとしての重要なスキルです。
ここまで何度か、「Object#tap」など、「Objectクラスにtapメソッドが定義されている」というような説明をしてきました。
しかし実際には、Objectクラスに定義されているメソッド群は、実は「Kernel」モジュールで定義されています。ObjectクラスはKernelモジュールをMixinしているので、tapメソッドなどが使えるわけです。せっかくなので、pryを起動して確かめてみましょう。
[1] pry(main)> Array.new.method(:tap).owner => Kernel
「Array.new」で生成したArrayクラスのオブジェクトに対して、「method」メソッドを呼び出しています。「method」メソッドは、与えられた引数のメソッドオブジェクトを生成します。メソッドオブジェクトに対して「owner」メソッドを呼び出すと、そのメソッドがどのモジュールまたはクラスで定義されているかが分かります。ここでは、Kernelモジュールになっていますね。
[2] pry(main)> Array.new.method(:method).owner => Kernel
ちなみに、「method」メソッドもKernelモジュールで定義されています。「メソッドメソッドメソッド……」と、メソッド続きでちょっとややこしいですね。
メソッドオブジェクトについては、以降の連載で詳しく触れることにします。
以上、簡単にクラスの定義や利用法を説明しました。アクセス制御の説明で述べたように、クラスについて深く理解するためには、さらなる前提知識を必要とします。ですので、以降の連載のメソッドやブロックの解説やメタプログラミングの解説で、クラスの仕組みの深い部分についてあらためて触れることとします。
連載第7回に当たる今回は、クラスやモジュールについて基本的な事柄について解説しました。今回の内容を理解できれば、クラスやモジュールを使ってコードをコンポーネント化できるようになるでしょう。
次回は、今回何げなく定義して何げなく使っていたメソッドやブロックについて掘り下げていきます。メソッドやブロックの仕組みを理解することで、より高度なコードを読み書きできるようになることでしょう。お楽しみに!
麻田 優真(Rails技術者認定シルバー試験問題作成者)
イタリア、ローマ生まれ。中学生のころHSPに初めて触れ、プログラミングの楽しさを知る。オープンソースやハッカーカルチャーを好む。C#からRubyに転向したときに、動的型付け言語の柔軟性やメタプログラミングの魅力に感動し、Rubyとともにプログラマーとしての人生を歩む決意を固める。
現在は奈良先端科学技術大学院大学で学生として所属するかたわら、株式会社アジャイルウェアでプログラマーとして従事。Ruby on Railsによるコンシューマー向けのWebサービスの開発などに尽力している。
好きなメソッドは、define_method。
Twitter:@Mozamimy、ブログ:http://blog.quellencode.org/
山根 剛司(Ruby業務開発歴7年)
兵庫県生まれ。1997年からベンチャー系のパッケージベンダーで10年間勤務。当時、使用していた言語はJavaとサーバーサイドJavaScript。
2007年よりITコンサル会社に転職し、Rubyと出会って衝撃を受ける。基幹システムをRuby on Railsで置き換えるプロジェクトに従事。それ以来Ruby一筋で、Ruby on Railsのプラグインやgemも開発。
2013年より、株式会社アジャイルウェアに所属。アジャイルな手法で、Ruby on Railsを使って企業向けシステムを構築する業務に従事。
Ruby関西所属。
Twitter:@spring_kuma、Facebook:山根 剛司
Copyright © ITmedia, Inc. All Rights Reserved.