手続き型とオブジェクト指向の違いを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のようなシンプルなプログラムで、オブジェクト指向の持つ様々な恩恵にあずかろうとするのは難しいと思うのですが、これが何千行ものプログラムが組み合わさって構成されているような大規模システムでは、大きく差が出てくることが予想されます。
以上、長々と書いてきましたが、技術書などで学んだ事を、どの程度自分が理解できているかどうかを把握する為には、こうやって実際にプログラムを書いて比較してみるのが一番ですね。
今後もこのような形で、学んだ事をブログにアウトプットしていこうと思います。