読者です 読者をやめる 読者になる 読者になる

COBOL技術者の憂鬱

COBOLプログラマは不在にしています

手続き型とオブジェクト指向の違いをFizzBuzzで理解する

前回のエントリーでは、手続き型とオブジェクト指向とで、プログラミングの違いが理解できるようになってきたと書きました。
ですが、本当に理解できているのかどうか自分でもあいまいなところがあるので、実際に両者でプログラムを書いて比較してみることで、その違いについて考えていこうと思います。
対象のプログラムですが、まだ簡単なものしか書けないので、FizzBuzzにしましょう。
これは一時、流行りましたね。あの時は私もCOBOLで書いたのですが、今回はRubyでやってみることにします。


まずは、以前に書いたCOBOLソースをそのままRubyで焼き直してみました。



【ソース1】

#手続き型(関数を利用しない構造化プログラミング)

#3の倍数を求める(Fizz)
Fizz = []
for i in 0..100
  if i % 3 == 0
    Fizz[i] = 'Fizz'
  else
    Fizz[i] = ''
  end
end

#5の倍数を求める(Buzz)
Buzz = []
for i in 0..100
  if i % 5 == 0
    Buzz[i] = 'Buzz'
  else
    Buzz[i] = ''
  end
end

#3の倍数と5の倍数を統合する(FizzBuzz)
FizzBuzz = []
for i in 0..100
  FizzBuzz << Fizz[i] + Buzz[i]
end

#結果表示
for i in 1..100
  if FizzBuzz[i] == ''
    puts i
  else
    puts FizzBuzz[i]
  end
end

配列を利用して、まず3の倍数を求め、次に5の倍数を求め、最後にその二つを統合して、結果を表示しています。
順次処理/繰り返し/条件分岐の三つだけで書けましたね。
これが俗にいう構造化プログラミングというやつなのですが、このままだと同じような処理を何度もやっている箇所があるので、機能分割して関数を利用するように修正してみましょう。



【ソース2】

#手続き型(関数を利用した構造化プログラミング)

#xの倍数に文字列hogeを代入した配列を返す
def baisu(x,hoge)
  baisu_array = []
  for i in 0..100
    if i % x == 0
      baisu_array[i] = hoge
    else
      baisu_array[i] = ''
    end
  end
  return baisu_array
end

#配列xと配列yを統合した配列を返す
def merge(x,y)
  merge_array = []
  for i in 0..100
    merge_array << x[i] + y[i]
  end
  return merge_array
end

#配列xを表示する
def show(x)
  for i in 1..100
    if x[i] == ''
      puts i
    else
      puts x[i]
    end
  end
end

#FizzBuzz処理
Fizz = baisu(3,'Fizz')
Buzz = baisu(5,'Buzz')
FizzBuzz = merge(Fizz,Buzz)
show(FizzBuzz)

指定された倍数を求めて配列として返す関数と、二つの配列を統合する関数、それに配列を表示する関数ができました。
そして、これら3つの関数を利用する形で、FizzBuzz処理が進んでいきます。
主要な機能を関数にしておくことで、プログラムの再利用性が高まりましたね。


次に、ここで作成した3つの関数を、そのまま単純にクラスに置き換えてみました。



【ソース3】

#オブジェクト指向(関数をクラスに置き換えただけ)

#xの倍数に文字列hogeを代入した配列を内部に持つクラス
class Baisu
  def initialize(x,hoge)
    @baisu_array = Array.new(101,'')
    i = 0
    while i < @baisu_array.length
      @baisu_array[i] = hoge
      i += x
    end
  end
  def get
    return @baisu_array
  end
end

#配列xと配列yを統合した配列を内部に持つクラス
class Merge
  def initialize(x,y)
    @merge_array = Array.new
    i = 0
    while i < x.get.length
      @merge_array << x.get[i] + y.get[i]
      i += 1
    end
  end
  def get
    return @merge_array
  end
end

#配列xを表示するクラス
class Show
  def initialize(x)
    x.get.each_with_index{|elem,i|
      if i != 0
        if elem == ''
          puts i
        else
          puts elem
        end
      end
    }
  end
end

#FizzBuzz処理
Fizz = Baisu.new(3,'Fizz')
Buzz = Baisu.new(5,'Buzz')
FizzBuzz = Merge.new(Fizz,Buzz)
Show.new(FizzBuzz)

「ソース2」で関数を定義していた部分をクラス定義に置き換え、関数を利用していた箇所をオブジェクトの生成に置き換えました。
このままでも問題なく動くことは動くのですが、オブジェクト指向プログラミングとは言い難い内容になっていますね。
具体的にどこがおかしいのかというと、配列を表示する処理を単独でクラスにしていますが(Showクラス)、この処理は配列を保持しているオブジェクト自身が行うべきですね。


というわけで、Showクラスは削除して、代わりにBaisuクラスとMergeクラスにshowメソッドを追加してみることにします。



【ソース4】>

#オブジェクト指向(オブジェクトを意識)

#xの倍数に文字列hogeを代入した配列を内部に持つクラス
class Baisu
  def initialize(x,hoge)
    @baisu_array = Array.new(101,'')
    i = 0
    while i < @baisu_array.length
      @baisu_array[i] = hoge
      i += x
    end
  end
  def get
    return @baisu_array
  end
  def show
    @baisu_array.each_with_index{|elem,i|
      if i != 0
        if elem == ''
          puts i
        else
          puts elem
        end
      end
    }
  end
end

#配列xと配列yを統合した配列を内部に持つクラス
class Merge
  def initialize(x,y)
    @merge_array = Array.new
    i = 0
    while i < x.get.length
      @merge_array << x.get[i] + y.get[i]
      i += 1
    end
  end
  def get
    return @merge_array
  end
  def show
    @merge_array.each_with_index{|elem,i|
      if i != 0
        if elem == ''
          puts i
        else
          puts elem
        end
      end
    }
  end
end

#FizzBuzz処理
Fizz = Baisu.new(3,'Fizz')
Buzz = Baisu.new(5,'Buzz')
FizzBuzz = Merge.new(Fizz,Buzz)
FizzBuzz.show

いい感じになってきましたね。
Baisuクラスから、「3の倍数オブジェクト」と「5の倍数オブジェクト」を生成して、さらに二つのオブジェクトを統合したオブジェクトを生成し、保持している配列を表示させています。


ところで、BaisuクラスとMergeクラスを見比べてみると、全く同じ内容のメソッドが定義されているので、ここは共通クラスを作っておいて、それを継承させるようにしておいた方がよいですね。
CommonMethodクラスを追加して、getメソッドとshowメソッドをそちらに移しましょう。



【ソース5】

#オブジェクト指向(継承)

#共通メソッドを定義したクラス
class CommonMethod
  def get
    return @common_array
  end
  def show
    @common_array.each_with_index{|elem,i|
      print i," = ",elem,"\n"
    }
  end
  def show
    @common_array.each_with_index{|elem,i|
      if i != 0
        if elem == ''
          puts i
        else
          puts elem
        end
      end
    }
  end
end

#xの倍数に文字列hogeを代入した配列を内部に持つクラス
class Baisu < CommonMethod
  def initialize(x,hoge)
    @common_array = Array.new(101,'')
    i = 0
    while i < @common_array.length
      @common_array[i] = hoge
      i += x
    end
  end
end

#配列xと配列yを統合した配列を内部に持つクラス
class Merge < CommonMethod
  def initialize(x,y)
    @common_array = Array.new
    i = 0
    while i < x.get.length
      @common_array << x.get[i] + y.get[i]
      i += 1
    end
  end
end

#FizzBuzz処理
Fizz = Baisu.new(3,'Fizz')
Buzz = Baisu.new(5,'Buzz')
FizzBuzz = Merge.new(Fizz,Buzz)
FizzBuzz.show

さあ、これでようやく完成しました。


さてここで、手続き型で書いてみた結果とオブジェクト指向で書いてみた結果を比較してみることにします。

ソース2とソース5を見比べてみると、手続き型の場合は、まず始めに処理全体に必要な機能を定義して、それらを関数として実装し、できあがった関数に対してデータを順に渡していくことで処理を進めていくという流れになっているように思います。
ここで理解を深めるための重要なポイントは、関数の中と外で、データが自由に出入りしている様子がイメージできることです。

一方、オブジェクト指向では、処理全体に必要なオブジェクトが何であるかをまず考え、それに基づいたクラス定義を行い、実際にオブジェクトを生成し、それらを連携させることで処理を進めていくという形になっています。
データはオブジェクトの内部に保持されたままで、そこから出入りすることなく、オブジェクト自身がお互いに連携しあって動作しているようなイメージです。

手続き型とオブジェクト指向を分けている最大のポイントは、まさにこの、「処理」と「データ」が分離している状態と、結合している状態との違いなのではないかと私は考えています。


手続き型では分離していたのが、オブジェクト指向では結合しているわけですが、ではそうしておくことで得られるメリットとは何なのかについて、次に考えてみましょう。
よく、「オブジェクト指向の方が仕様変更に強い」ということを耳にするのですが、そういった視点から両者のソースを見比べてみると、関数やクラスを利用する場面については、再利用性という意味で互角ですね。
例えば、「3と5の倍数」の代わりに、「5と7の倍数」でやってみるとか、あるいは「3と5と7の倍数」でやってみるとか、そのあたりの変更にはどちらも柔軟に対応することができると思います。

ただ、使用するデータ構造に変更が発生した場合は、影響分析や対応にかかる負荷が随分違ってくるように思います。
例えば、「1から100まで」ではなく、「1から200まで」に仕様変更する場合に、どちらがその影響を特定して修正しやすいかは一目瞭然でしょう。

FizzBuzzのようなシンプルなプログラムで、オブジェクト指向の持つ様々な恩恵にあずかろうとするのは難しいと思うのですが、これが何千行ものプログラムが組み合わさって構成されているような大規模システムでは、大きく差が出てくることが予想されます。



以上、長々と書いてきましたが、技術書などで学んだ事を、どの程度自分が理解できているかどうかを把握する為には、こうやって実際にプログラムを書いて比較してみるのが一番ですね。
今後もこのような形で、学んだ事をブログにアウトプットしていこうと思います。