Rubyと大クラス主義とダックタイピング、そして名前重要

最近、Javaを勉強したりしていて、RubyJavaとの対比で昔より客観的に見られるようになったので、Rubyの記事を書きたいと思います。自分でも消化しきれていない話なので、反論は大歓迎です。

内容は、大クラス主義とダックタイピング、そして名前重要に関してです。

大クラス主義

Rubyの考えでよく言われるものに、大クラス主義があります。例えば、RubyArrayは配列を表すクラスです。しかし、Arrayができることは、単純に値を複数持てるだけではありません。Arrayを調べてみると、popやpushやshiftやunshiftがあります。つまり、RubyのArrayはキューやスタックとしても使えます。一方、JavaのArrayやArrayListでは、配列の要素を扱うことはできますが、そのままではキューやスタックとしては使えません。Javaでキューやスタックが使いたいのであれば、例えば、QueueクラスやStackクラスが用意されています*1

もう一つ例ですが、RubyのArrayにはeachメソッドがあります*2。一方、Javaでは、繰り返しの処理はIteratorクラスを使ったりします。

Javaでは、ArrayやQueueやStackやIteratorクラスのように、責務によってクラスが別れる傾向があるのに対し、RubyではすべてArrayクラスが引き受けます。このように、Rubyのクラスは、Javaよりもクラスが大きく、担う役割も大きなものになります。このような考え方は、大クラス主義と呼ばれています。

単一責任原則

Javaに多数のクラスがあって、クラスによって役割が異なるのは、単一責任原則によるものです。JavaのArrayクラスは、配列の要素の読み書きをするだけの責務を持っています。Arrayはその責務だけを全うします。もし、(Stackを使わずに)Arrayをスタックとして使う必要があるのならば、Arrayを利用した、あるいは継承したArrayStackのような別クラス作ることになります。

あたらしく作ったArrayStackでは、配列の読み書きにあたる役割はArrayに任せて、スタックとしての役割に専念できます。責務を分割してそれぞれを攻略する分割統治法は、プログラムの世界で重要なことですね。

そして、配列を利用するときにはArray型を指定し、新しく作ったスタックを使うにはArrayStack型を指定することで、安全に利用することができます。

「だとすると、それならRubyはこんなので大丈夫なの?」という気がしてきました。RubyヨリJavaノホウガイインジャナイノ?

そこでいろいろ考えてみると、実は「ダックタイピング」が重要な鍵なんじゃないかと思うようになってきました。

ダックタイピング

ダックタイピング(wikipedia)とは、「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ。」と形容される原理です。それだけだとよく分からないので、例をみてみましょう。

例えば、スタックからpopして1足してからpushするようなプログラムが書きたいとき、Arrayクラスに用意されているpushメソッドとpopメソッドを利用して下のようなプログラムが書けます。

def increment_last_element(array)
  array.push array.pop.to_i + 1
end

increment_last_element [1, 2, 3] # => [1, 2, 4]

この時、increment_last_elementに渡す値は、popとpushさえ持っていればArrayクラスでなくても構いません。この場合、popとpushを持っているのならば、それはArrayと同じように扱えるということです。

例えば、文字列をスタックのように使うクラスMyStringStackを作って、そのインスタンスをincrement_last_elementを渡しても、最後の要素に1を足すことができます。

def increment_last_element(array)
  array.push array.pop.to_i + 1
end

class MyStringStack
  def push(char)
    @string ||= ''  # @stringが定義されていないときに、空文字列を代入する
    @string += char.to_s
  end
  def pop
    ret = @string[-1] #戻り値用の値として、@stringの最後の文字を取得
    @string = @string[0..-2] #@stringの最後の文字を取り除いて@stringに代入
    return ret
  end
  def to_s
    @string
  end
end

stack = MyStringStack.new
stack.push '1' # => "1"
stack.push '2' # => "12"
stack.push '3' # => "123"

increment_last_element stack # => "124"

長い上に分かりにくい例で申し訳ないですが、MyStringStackはpushメソッドとpopメソッドを持ってるので、increment_last_elementに渡すことができます。

私が言いたいことは、Rubyの世界では、オブジェクトを利用する段になったら、どのクラスのインスタンスかはそれほど重要ではないということです。

ダックタイピングの話をしたところで、単一責任原則の話に戻りたいと思います。

メソッド重要

Javaの世界では、クラスによって責務が分かれていました。配列を使うにはArrayクラスを使い、スタックを使うためにはStackクラスを使うのです。一方で、Rubyでは、スタックを使うにはArrayクラスを使うのではなく、pushメソッドとpopメソッドを使います*3

となると、Javaではクラスの責務が重要になるように、Rubyではメソッドの責務が重要になるのではないかと思います。

JavaRubyも同じ純粋なオブジェクト指向の言語ですが、Javaはクラスをより重視しているのに対し、Rubyはメソッドをより重視しているように感じます。

RubyのArrayクラスなんかを見ていると単一責任に見えないかもしれないですが、メソッドごとの責任はシンプルに分割されていると思います。

名前重要

Rubyの世界では、Arrayクラスのインスタンスであることよりも、「pushメソッドをもっている」ことや、「popメソッドをもっている」ことが重要ということでした。そうなると、「ボタンを押すという責務のpushメソッドを作っちゃったらどうするの?」という疑問が湧いてきます。

この疑問から見えてくるのは、メソッド名が非常に重要な要素だということです。Rubyではどの名前のメソッドを持っているかが重要です。そして、そのメソッド名が責務を表します。だからこそ、「名前重要」はRubyの世界では強く活きてくると思います。慎重に名前をつけてあげることで、その名前がプログラムを導いてくれます。

まとめ

慎重にメソッドに名前を付けると、ダックタイピングによってプログラムが動きます。と同時に、その名前が責務を表し、その責務を全うします。そのため、クラスによる単一責任原則が働くJavaとは違って大クラス主義をとるRubyでも、きれいにプログラムが書けるのだと思います。

他にもこの仕組みが成り立つ仕掛けはあると思うのですが、書くのも読むのも疲れるのでここまでにします。最初に述べたように反論大歓迎です。

きれいでたのしいRubyのお話でした。

*1:Javaに詳しくないし、キューやスタックもあまり使ったことないので、このあたりのクラスがどのくらい使われているのかはよく知らないです…。

*2:ArrayクラスはEnumerableモジュールをmixinしていますが、Enumerableモジュールはeachメソッドを使って繰り返し処理を拡張しているだけなので、eachメソッド自体はarrayに定義されています。

*3:pushメソッドやpopメソッドはArrayクラスが持っているので、メソッド探索が行われる際にはやはりクラスは重要になります。