初心者が考えるオブジェクト指向設計のポイント(Ruby)

Rubyでコードを書く上で、オブジェクト指向設計というのがとても大事らしい。

オブジェクトとは「モノ」のこと。Rubyでは、全てがオブジェクトである。

最初聞いたときは「???」だったが、何回も戻りながら勉強を続けてきてほんの少しだけわかってきたので、オブジェクト指向設計でコードを書くときのポイントをまとめておく。

オブジェクト指向設計とは

プログラムには将来変更が必ずあるものとして、その変更をできるだけ容易に受け入れられるように設計する必要がある。

プログラムの部品である各オブジェクトがお互いに作用し合うことでアプリケーションとして動作するが、お互いに依存しすぎると変更が極めて難しいプログラムとなってしまうため、依存関係を最低限とするよう適切に管理しなければならない。

参考書籍:『オブジェクト指向設計実践ガイド』

自分だけかもしれないが、初心者は、せっかくだから一つのメソッドにたくさん機能をつけた方がいいと考えがちだ。そのメソッドを使えば、一発で処理が終わるようなスーパーメソッドだ。小さいメソッドがたくさんある方がおかしいとさえ感じていた。

今思えば、それは完全に間違いだった。

「1メソッド5行まで」というルールまであると知って驚いた。そこまで厳密に考えなくてもいいかもしれないが、小さく単純なメソッドを組み合わせることで大きなプログラムを作っていくのがオブジェクト指向だ。

コードを書くときのポイント

クラスがないとはじまらない

ただの計算式なんてものは存在しない。

とにかくクラスを作って、その中にメソッドを書き、それを呼び出すことで動作を実現させる。

クラスは、モノ(オブジェクト)を作るための設計図・型である。

クラスやメソッドは、できるだけ最小で単一の責任を持つように設計する。

このクラスの役割は何か?このメソッドの役割は何か?

クラスやメソッドを単一責任にするためには、この問いを自分の中で繰り返すのがいい。一文で簡潔に答えられればOK。

たとえば、「このメソッドの役割は何か?」に対する回答が、「〇〇と□□を計算してその結果を出力する。」というように長くなる場合は、単一責任になっていない。回答の中に「〜と」が出てくるのは複数仕事をしている証拠。

〇〇を計算するメソッド、□□を計算するメソッド、結果を出力するメソッド、と分けるべきではないか、と考えていく。

コメントなしでコードの意味がわかるか?

オブジェクト指向設計になっているかどうかを判断するには、初めて見る人の立場で「コメントなしでコードの意味がわかるか?」を考える。

後からコードを見る誰かのために、または数週間後の自分のためにコメントを入れたくなることがある。それは、メソッドを分割せよというサインだ。

その変数が表すものは何か?計算の目的は?コメントを書くようにコードを書く。

メソッド内で変数への代入が出てくるのも分割せよというサインのことが多い。

メソッドの主語は合っているか?

メソッドを分けていったら、今度は、その仕事をするのはそのクラスでいいのか考えてみる。

「〇〇クラスは、□□(メソッド名)をします。」という文が成り立てばOK。

主語がおかしいメソッドがあったら、適切なクラスに移すか新しいクラスを作る。

インスタンス変数は直接参照しない

インスタンス変数は@lengthのように変数名の頭に@がついていてインスタンスが持つ変数こと。ローカル変数は別メソッドから呼び出せないが、インスタンス変数は同じインスタンスであれば他メソッドからも呼び出せる。

インスタンス変数は、@lengthのようにして直接呼び出すことが可能である。

しかし、『オブジェクト指向設計実践ガイド』によると、@をつけて直接参照することは避けるべきとのこと。 理由は、もしここに修正が必要になったときに直接参照しているところがたくさんあると変更が困難になるため。

アクセサメソッドattr_readerを使えば@なしで呼び出せるようになる。

attr_reader :lengthは、以下のメソッドを定義しているのと同じ。

def length
  @length
end

将来もしlengthの中身が変わることになったときは、lengthメソッド一つを再定義するだけで済む。

例で考える

文章だけだとわかりにくいので、長方形の面積を計算するコードを書いてみる。

縦と横の長さを受け取り、縦と横の長さが同じなら正方形、異なれば長方形として面積を出力するというコード。

length = 5
width = 3
form = length == width ? "正方形" : "長方形"
area = length * width
puts "#{form}の面積は#{area}です。"
#=> 長方形の面積は15です。

実行すると、正しい答えは得られる。

しかし、これは全くオブジェクト指向設計になっていない。

何はともあれクラスを作ってみる。2週間前の自分ならこれでOKにしただろう。

class Area
  def initialize(length, width)
    @length = length
    @width = width
  end

  def execute
    form = @length == @width ? "正方形" : "長方形"
    area = @length * @width
    puts "#{form}の面積は#{area}です。"
  end
end

area = Area.new(5, 3)
area.execute

executeメソッドは最初のコードとほぼ同じ。

まずメソッドの役割を一文で説明できるか?への回答。

「executeメソッドの役割は、正方形か長方形かを判断することと、面積を計算すること、そして結果を出力することです」

簡潔じゃないのでNG。明らかに複数の役割を持っている。

executeメソッドの中には、form と area という2つの変数が登場している。変数への代入はメソッドを分割せよというサインの可能性が高い。

それから、インスタンス変数(ここでは@length@width)を直接参照してしまっている。

2週間勉強後の自分は、こんな風に書き直した。

class Area
  attr_reader :length, :width

  def initialize(length, width)
    @length = length
    @width = width
  end

  def execute
    puts "#{form}の面積は#{area}です。"
  end

  private
    def form
      length == width ? "正方形" : "長方形"
    end

    def area
      length * width
    end
end

area = Area.new(5, 3)
area.execute

上のコードでは、executeメソッドだけ見るとformareaは変数名のように見えるが、実際にはformメソッドやareaメソッドを呼び出している。

form や area が変数名でかつインスタンス変数でなくローカル変数だったとしたら、(ローカル変数は他のメソッドから参照できないため)1つ前のコードのようにexecuteメソッド内に書くしかないが、このようにインスタンスメソッドとして独立させることで、他のメソッドから呼び出せるようになったのだ。

隠せるメソッドは全部隠す

最後に、formメソッドの上にあるprivateについて。

メソッドは、何もしなければpublicである(initializeメソッドを除く)。public(パブリック)の反対がprivate(プライベート)。

privateと書くと、それより下に書くメソッドをprivateメソッドにすることができる。

publicなメソッドは、クラスの外部からオブジェクト名.メソッド名という形で特定のオブジェクトに対して呼び出すことができる。

上の例では最終行area.executeの部分。areaという名前をつけたAreaクラスのモノ(オブジェクト)に対して、executeメソッドを呼び出している。

もしexecuteメソッドがprivateメソッドだったら、このarea.executeはエラーになる。privateメソッドは、クラスの外で特定のモノを指定して.メソッド名で呼び出すような使い方ができない。ではどうやって使うか?

クラス内のメソッド定義で、他のメソッドから呼び出すような使い方だ。上の例でも、executeメソッドからprivateメソッドを呼び出して使っている。

このformメソッドやareaメソッドは、executeメソッドとは違いクラスの外から呼び出されることが想定されていない。そういうメソッドはすべてprivateメソッドにして、その設計の意図を明確にしておくことが望ましい。

(備考)この文脈ではモノのことを、オブジェクト、インスタンス、レシーバと呼ぶことがあるが、すべて同じ意味である(関連記事)。

初心者による初心者のためのRuby記事まとめ - No Solution for Life