<目次>
- <Rubyリファレンスマニュアル - モジュール定義>
- <クラスとモジュールの共通点>
- <クラスにしかできないこと>
- <モジュールにしかできないこと>
- <そもそも継承とは>
- <多重継承について>
- <Rubyの継承方法>
- まとめ
※この記事はプログラミング初心者が執筆しています。
あくまでも参考程度にご覧ください。
まずは公式マニュアルをみてみます。
<Rubyリファレンスマニュアル - モジュール定義>
モジュール定義
module Foo
def test
# ...
end
# ...
end
文法:
module 識別子
式..
end
文法:
module 識別子
式..
[rescue [error_type,..] [=> evar] [then]
式..]..
[else
式..]
[ensure
式..]
end
説明:
モジュールを定義します。モジュール名はアルファベットの大文字で始まる識別子です。
rescue/ensure 節を指定し、例外処理ができます。例外処理については制御構造/begin参照。
モジュール定義は、識別子で指定した定数へのモジュールの代入になります。 Ruby では、モジュールもオブジェクトの一つで Module クラスのインスタンスです。モジュールが既に定義されいるとき、さらに同じモジュール名でモジュール定義を書くとモジュールの定義の追加になります。
モジュール定義式は、最後に評価した式の結果を返します。最後に評価した式が値を返さない場合は nil を返します。
いかがでしょうか?小難しくてよくわかりません。
続いてクラス定義を見てみましょう。
説明:
クラスを定義します。クラス名はアルファベットの大文字で始まる識別子です。
rescue/ensure 節を指定し、例外処理ができます。例外処理については制御構造/begin参照。
クラス定義は、識別子で指定した定数へのクラスの代入になります (Ruby では、クラスもオブジェクトの一つで Classクラスのインスタンスです)。
クラスが既に定義されているとき、さらに同じクラス名でクラス定義を書くとクラスの定義の追加になります。ただし、元のクラスと異なるスーパークラスを指定すると TypeError が発生します。
なんだかモジュールと解説がとても似ています。
実際機能としてはかなり似通っているようです。
<クラスとモジュールの共通点>
1. モジュールの定義:
- クラスもモジュールもメソッドを定義できます。
class Dog
def bark
puts "Woof!"
end
end
module Swimmable
def swim
puts "Swimming!"
end
end
2. 定数の定義:
- クラスもモジュールも定数を定義できます。
class Dog
LEGS = 4
end
module Constants
PI = 3.14159
end
3. 例外処理:
- クラスもモジュールもrescueやensureを使って例外処理を定義できます。
ここまではクラスとモジュールの共通点でした。
続いて、クラスとモジュールの違いについてみていきます。
<クラスにしかできないこと>
1. インスタンスの生成:
-
クラスはインスタンス(オブジェクト)を生成できますが、モジュールはできません。
dog = Dog.new # クラスのインスタンスを生成
2. 継承:
- クラスは他のクラスを継承できますが、モジュールは継承できません。
class Animal #Animalクラスを定義
end
class Dog < Animal # Animalクラスを継承し、Dogクラスを定義
end
3. インスタンス変数とアクセサメソッド:
- クラスはインスタンス変数やアクセサメソッドを持つことができますが、モジュールは持てません。
class Dog
attr_accessor :name
def initialize(name)
@name = name
end
end
<モジュールにしかできないこと>
1.ミックスイン:
- モジュールはクラスにインクルードして、そのクラスにモジュールのメソッドを追加できます。
module Swimmable
def swim
puts "Swimming!"
end
end
class Dog
include Swimmable
end
2. 名前空間の提供:
- モジュールは名前空間を提供して、クラスやメソッドの名前の衝突を避けることができます。
module Animals
class Dog
def bark
puts "Woof!"
end
end
end
dog = Animals::Dog.new
ここまで、クラスとモジュールの違いを見てきました。
ですが、これだけだと機能を箇条書きされただけで実際なぜモジュールが必要なのかピンとこない方も多いと思います。
以下は、Rubyにおいてモジュールとはどのような立ち位置なのか、私個人の見解を記します。
モジュールを理解するには、まず継承とは何かを考える必要があると思います。
<そもそも継承とは>
継承は、あるクラス(親クラスまたはスーパークラス)の特性や動作を別のクラス(子クラスまたはサブクラス)に引き継ぐための仕組みです。これにより、コードの再利用性が高まり、共通の機能を持つクラスを簡単に作成できます。
また、この継承はオブジェクト指向言語で使用されます。
Rubyは、単一継承はできますが多重継承は行えません。
オブジェクト指向言語
プログラムをオブジェクトという部品に分けて作る言語です。これにより、プログラムが整理され、再利用しやすくなります。Rubyは完全なオブジェクト指向言語です。
<多重継承について>
そして、複数の親クラスから継承することを多重継承と言います。
多重継承することで、コードはより柔軟でさまざまなことができるようになると思いませんか?
実際多重継承はC++などの言語で使用でき、より自由にコードを書くことができるようになります。
しかし、実際使用する上では以下のようなデメリットがあり、直接的な多重継承は避けられています。
(ここでは解説しませんが、C++などは多重継承でき、仮想継承などの機能で問題を解決しています。)
多重継承のデメリット
-
ダイヤモンド問題:
複数の親クラスが同じ祖先クラスを持つ場合、どの親クラスのメソッドや属性を継承するかが曖昧になります。
構造としては以下のような感じです。
クラス D がクラス B と C を継承し、さらに B と C が共通の親クラス A を持つ場合、D が A のメソッドや属性をどのように継承するかが曖昧になります。A / \ B C \ / D
-
可読性の低下:
継承関係が複雑になると、コードの理解が難しくなる。
個人の開発であれば、多重継承を使っていても全体の構造を把握できるかもしれませんが、一般的に開発は複数人で行われます。その際、他人の書いたコードを読み解く必要がしばしばありますが、クラスが幾重にも多重継承されていると構造を理解するのが非常に困難になることは想像に難くありません。
これにより、コードの可読性やメンテナンス性が著しく低下する可能性があります。
そのため、多くの言語(Java、Ruby、Python、C#、Goなど)では、クラスの直接的な多重継承をサポートしていません。
<Rubyの継承方法>
直接的なクラスの多重化は、先ほど挙げたように大きいデメリットがあります。
しかし、多重継承はコードをより柔軟に、自由に書けるというメリットもあるため、
多くの言語では、デメリットを解消しつつ、擬似的に多重継承を実現する方法を用意しています。
それが、インターフェースやモジュールというものであり、Rubyではモジュールを使用できます。
Rubyにおけるモジュールの役割
モジュールは、関連するメソッドや定数をまとめることができます。
クラスと違って、モジュールはインスタンスを作成することはできませんが、クラスに機能を追加するために使われます。
共通の機能(メソッドや定数)を複数のクラスで使用することができ、擬似的な多重継承を実現しています。
また、名前空間の提供といった役割もあります。
これにより、クラスやメソッドの名前の衝突を避けることができます。
module Animals
class Dog
def bark
puts "Woof!"
end
end
end
dog = Animals::Dog.new
dog.bark # => "Woof!"
以下はモジュールがどのようにしてダイヤモンド問題を回避しているのかの解説です。
モジュールによるダイヤモンド問題の回避方法
Rubyでは、モジュールを使って多重継承のような機能を実現しますが、モジュールのミックスインはダイヤモンド問題を回避するように設計されています。
具体的には、Rubyのメソッド解決順序(MRO: Method Resolution Order)によって、どのメソッドが呼び出されるかが明確に定義されています。
**例:モジュールのミックスイン
(**ミックスインとは、モジュールをクラスにインクルードすることで、そのクラスにモジュールのメソッドを追加することです。)
module A
def hello
puts "Hello from A"
end
end
module B
include A
def hello
puts "Hello from B"
end
end
module C
include A
def hello
puts "Hello from C"
end
end
class D
include B
include C
end
d = D.new
d.hello # => "Hello from C"
メソッド解決順序(MRO)
Rubyでは、モジュールのミックスインによるメソッド解決順序は以下のように決まります:
- クラス自身
- 最後にインクルードされたモジュール
- それ以前にインクルードされたモジュール
- 親クラス
- 親クラスのモジュール
上記の例では、クラス D はモジュール B と C をインクルードしていますが、最後にインクルードされた C のメソッドが優先されます。これにより、ダイヤモンド問題が回避されます。
まとめ
<クラスとモジュール>
- クラスはオブジェクトの生成や継承、インスタンス変数の管理ができます。
- モジュールはミックスインや名前空間の提供ができます。
- 両者ともメソッドや定数の定義、例外処理が可能です。
<継承について>
- 継承は、コードの再利用性を高めるためのオブジェクト指向の基本概念です。
- 多重継承はC++でサポートされていますが、複雑さや曖昧さを引き起こす可能性があります。
- Rubyはモジュール、Javaはインターフェースを使って擬似的な多重継承を実現しています。
- これらのアプローチは、コードの可読性とメンテナンス性を向上させるために採用されています。
<多重継承の必要性>
- 多重継承をサポートしていないメジャーな言語には、Java、C#、Swift、Kotlin、Goなどがあります。
- 多重継承が主流でない理由は、コードの複雑さを回避し、設計の明確化を高めるためです。
- インターフェースやモジュールを使うことで、擬似的な多重継承を実現し、これらの問題を解決しています。
<どうしてC++が多重継承を取り入れているのか>
- C++が多重継承をサポートしているのは、プログラマに対して最大限の柔軟性と設計の自由度を提供するためです。しかし、多重継承にはダイヤモンド問題や可読性の低下といったデメリットも存在します。これらの問題を解決するために、C++では仮想継承などの機能が提供されています。
<Rubyにおけるモジュールとは>
- Rubyのモジュールは、クラスに機能を追加するための強力なツールです。モジュールを使うことで、コードの再利用性と柔軟性を高め、名前空間を提供し、多重継承の問題を回避することができます。これにより、Rubyはシンプルで理解しやすいコードを保ちながら、強力な機能を提供しています。
<Rubyにおけるダイヤモンド問題の解決策>
- Rubyのモジュールは、メソッド解決順序(MRO)を明確に定義することで、ダイヤモンド問題を回避しています。これにより、複数のモジュールをクラスにミックスインしても、どのメソッドが呼び出されるかが明確になり、コードの可読性とメンテナンス性が向上します。